@skewedaspect/sage 0.9.0 → 0.9.2
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/classes/gameLevel.d.ts +9 -8
- package/dist/interfaces/level.d.ts +9 -0
- package/dist/sage.d.ts +1 -0
- package/dist/sage.es.js +232 -192
- package/dist/sage.es.js.map +1 -1
- package/dist/sage.umd.js +1 -1
- package/dist/sage.umd.js.map +1 -1
- package/dist/utils/vectors.d.ts +35 -0
- package/package.json +1 -1
package/dist/sage.umd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sage.umd.js","names":[],"sources":["../src/utils/version.ts","../src/classes/gameEngine.ts","../src/classes/loggers/consoleBackend.ts","../src/classes/loggers/nullBackend.ts","../src/utils/logger.ts","../src/classes/eventBus.ts","../src/engines/audio.ts","../src/engines/scene.ts","../src/managers/asset.ts","../src/interfaces/binding.ts","../src/interfaces/input.ts","../src/classes/bindings/trigger.ts","../src/classes/bindings/toggle.ts","../src/classes/bindings/value.ts","../src/classes/input/readers/keyboard.ts","../src/classes/input/readers/mouse.ts","../src/classes/input/readers/gamepad.ts","../src/managers/binding.ts","../src/utils/capabilities.ts","../src/managers/game.ts","../node_modules/hexoid/dist/index.mjs","../src/utils/id.ts","../src/classes/entity.ts","../src/managers/entity.ts","../src/handlers/postProcessingPipeline.ts","../src/handlers/postProcessingFrameGraph.ts","../src/handlers/postProcessing.ts","../src/classes/level.ts","../src/managers/outline.ts","../src/classes/gameLevel.ts","../src/managers/level.ts","../src/managers/audio.ts","../src/managers/save.ts","../src/classes/input/keyboard.ts","../src/classes/input/mouse.ts","../src/classes/input/gamepad.ts","../src/managers/input.ts","../src/utils/graphics.ts","../src/utils/physics.ts","../src/utils/raycast.ts","../src/utils/timer.ts","../src/interfaces/logger.ts","../src/behaviors/sound.ts","../src/utils/stateMachine.ts","../src/behaviors/stateMachine.ts","../src/handlers/collider.ts","../src/handlers/lod.ts","../src/handlers/occluder.ts","../src/utils/metadata.ts","../src/handlers/sound.ts","../src/handlers/trigger.ts","../src/handlers/visible.ts","../src/handlers/index.ts","../src/sage.ts"],"sourcesContent":["//----------------------------------------------------------------------------------------------------------------------\n// Version utility for SAGE\n//----------------------------------------------------------------------------------------------------------------------\n\ndeclare const __APP_VERSION__ : string;\n\nexport const VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0-dev';\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Game Engine\n//\n// WARNING: This module must be imported as `import type` everywhere except sage.ts. A regular import\n// would create a circular runtime dependency (managers import GameEngine, GameEngine imports managers).\n// The `import type` is erased at compile time, keeping the runtime graph acyclic.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { AbstractEngine, HavokPlugin } from '@babylonjs/core';\n\n// Interfaces\nimport type { GameCanvas } from '../interfaces/game.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\n\n// Engines\nimport { SceneEngine } from '../engines/scene.ts';\nimport type { AudioEngine } from '../engines/audio.ts';\n\n// Managers\nimport { AssetManager } from '../managers/asset.ts';\nimport { BindingManager } from '../managers/binding.ts';\nimport { GameManager } from '../managers/game.ts';\nimport { GameEntityManager } from '../managers/entity.ts';\nimport { LevelManager } from '../managers/level.ts';\nimport { SaveManager } from '../managers/save.ts';\nimport { UserInputManager } from '../managers/input.ts';\nimport type { AudioManager } from '../managers/audio.ts';\n\n// Utils\nimport { type GameEvent, GameEventBus, type GameEventCallback, type Unsubscribe } from './eventBus.ts';\nimport type { ActionPayload, LibraryEventPayloadMap } from '../events/payloads.ts';\nimport type { WildcardPattern } from '../events/types.ts';\nimport { LoggingUtility } from '../utils/logger.ts';\nimport { RaycastHelper } from '../utils/raycast.ts';\nimport { GameTimer } from '../utils/timer.ts';\nimport { VERSION } from '../utils/version.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Type definition for game lifecycle hook functions\n */\n \nexport type GameHook = (gameEngine : GameEngine) => Promise<void>;\n\n/**\n * Interface representing the engines used in the game.\n * All engines must implement Disposable for proper cleanup.\n */\ninterface Engines extends Record<string, Disposable | undefined>\n{\n sceneEngine : SceneEngine;\n audioEngine ?: AudioEngine;\n}\n\n/**\n * Interface representing the managers used in the game.\n * All managers must implement Disposable for proper cleanup.\n */\ninterface Managers extends Record<string, Disposable | undefined>\n{\n assetManager : AssetManager;\n bindingManager : BindingManager;\n gameManager : GameManager;\n entityManager : GameEntityManager;\n inputManager : UserInputManager;\n levelManager : LevelManager;\n saveManager : SaveManager;\n audioManager ?: AudioManager;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Central hub that owns the render loop, physics, event bus, and all managers.\n * Created via `createGameEngine()` in sage.ts.\n */\nexport class GameEngine\n{\n public canvas : GameCanvas;\n public renderEngine : AbstractEngine;\n public physics : HavokPlugin;\n public managers : Managers;\n public engines : Engines;\n public eventBus : GameEventBus;\n public logger : LoggingUtility;\n public raycast : RaycastHelper;\n public timer : GameTimer;\n public largeWorldRendering : boolean;\n public started = false;\n\n private _log : LoggerInterface;\n\n // Lifecycle hooks\n private _beforeStartHook : GameHook | null = null;\n private _onStartHook : GameHook | null = null;\n private _onTeardownHook : GameHook | null = null;\n\n /**\n * Creates an instance of GameEngine.\n * @param canvas\n * @param renderEngine\n * @param physics\n * @param eventBus\n * @param logger\n * @param engines\n * @param managers\n */\n constructor(\n canvas : GameCanvas,\n renderEngine : AbstractEngine,\n physics : HavokPlugin,\n eventBus : GameEventBus,\n logger : LoggingUtility,\n raycast : RaycastHelper,\n timer : GameTimer,\n engines : Engines,\n managers : Managers,\n largeWorldRendering = false\n )\n {\n this.canvas = canvas;\n this.renderEngine = renderEngine;\n this.physics = physics;\n this.eventBus = eventBus;\n this.logger = logger;\n this.raycast = raycast;\n this.timer = timer;\n this.engines = engines;\n this.managers = managers;\n this.largeWorldRendering = largeWorldRendering;\n\n this._log = logger.getLogger('GameEngine');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a function to be called before the game engine starts\n * @param hook\n * @throws Error if a hook is already registered\n */\n onBeforeStart(hook : GameHook) : void\n {\n if(this._beforeStartHook !== null)\n {\n throw new Error('A beforeStart hook is already registered. Only one hook is allowed per lifecycle event.');\n }\n this._beforeStartHook = hook;\n }\n\n /**\n * Register a function to be called after the game engine starts\n * @param hook\n * @throws Error if a hook is already registered\n */\n onStart(hook : GameHook) : void\n {\n if(this._onStartHook !== null)\n {\n throw new Error('An onStart hook is already registered. Only one hook is allowed per lifecycle event.');\n }\n this._onStartHook = hook;\n }\n\n /**\n * Register a function to be called when the game engine stops\n * @param hook\n * @throws Error if a hook is already registered\n */\n onTeardown(hook : GameHook) : void\n {\n if(this._onTeardownHook !== null)\n {\n throw new Error('An onTeardown hook is already registered. Only one hook is allowed per lifecycle event.');\n }\n this._onTeardownHook = hook;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Event Subscriptions\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Subscribe to an event on the event bus.\n *\n * This is a convenience method that forwards to `eventBus.subscribe()`.\n *\n * @param eventType - Exact event type string, wildcard pattern (e.g. 'input:*'), or RegExp\n * @param callback\n * @returns A function that removes this subscription when called\n *\n * @example\n * // Subscribe to a specific event\n * const unsub = engine.subscribe('level:complete', (event) => {\n * console.log('Level complete:', event.payload.levelName);\n * });\n *\n * // Subscribe to all input events\n * engine.subscribe('input:*', (event) => {\n * console.log('Input event:', event.type);\n * });\n */\n subscribe<T extends keyof LibraryEventPayloadMap & string>(\n eventType : T | WildcardPattern | RegExp,\n callback : GameEventCallback<T, LibraryEventPayloadMap>\n ) : Unsubscribe\n {\n return this.eventBus.subscribe(eventType, callback);\n }\n\n /**\n * Subscribe to an action event with full type safety.\n *\n * Action events are fired by the binding system when inputs trigger registered actions.\n * All action events have the same payload structure (`ActionPayload`), containing:\n * - `value`: The action value (number for analog, boolean for digital)\n * - `deviceId`: The ID of the input device that triggered the action\n * - `context`: The binding context that was active (optional)\n *\n * @param actionName - The action name (without the 'action:' prefix)\n * @param callback\n * @returns A function that removes this subscription when called\n *\n * @example\n * // Subscribe to a specific action\n * engine.subscribeAction('jump', (event) => {\n * console.log('Jump triggered with value:', event.payload.value);\n * console.log('From device:', event.payload.deviceId);\n * });\n *\n * // Subscribe to movement action\n * engine.subscribeAction('moveForward', (event) => {\n * const velocity = Number(event.payload.value);\n * player.move(velocity);\n * });\n */\n subscribeAction(\n actionName : string,\n callback : (event : GameEvent<`action:${ string }`, Record<`action:${ string }`, ActionPayload>>) => void\n ) : Unsubscribe\n {\n return this.eventBus.subscribe(\n `action:${ actionName }` as keyof LibraryEventPayloadMap & string,\n callback as unknown as GameEventCallback<keyof LibraryEventPayloadMap & string, LibraryEventPayloadMap>\n );\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Start the engine: runs beforeStart hook, starts the game manager, then runs onStart hook.\n */\n async start() : Promise<void>\n {\n if(!this.started)\n {\n this._log.info(`Starting SkewedAspect Game Engine (Version: ${ VERSION })...`);\n\n // Execute beforeStart hook if registered\n if(this._beforeStartHook)\n {\n this._log.debug('Executing beforeStart hook...');\n await this._beforeStartHook(this);\n }\n\n // Start the game manager\n await this.managers.gameManager.start();\n\n this._log.info('Game engine started successfully');\n\n // Execute onStart hook if registered\n if(this._onStartHook)\n {\n this._log.debug('Executing onStart hook...');\n await this._onStartHook(this);\n }\n\n // Mark game as started\n this.started = true;\n }\n else\n {\n this._log.warn('Game engine is already started. Skipping start.');\n }\n }\n\n /**\n * Stop the engine: runs the teardown hook, then tears down all managers and engines in order.\n */\n async stop() : Promise<void>\n {\n if(this.started)\n {\n this._log.info(`Stopping SkewedAspect Game Engine (Version: ${ VERSION })...`);\n\n let teardownError : Error | null = null;\n\n // Execute onTeardown hook if registered\n if(this._onTeardownHook)\n {\n try\n {\n this._log.debug('Executing onTeardown hook...');\n await this._onTeardownHook(this);\n }\n catch (err)\n {\n this._log.error(`Error in onTeardown hook: ${ err }`);\n teardownError = teardownError || (err as Error);\n }\n }\n\n // Teardown all managers\n for(const managerKey of Object.keys(this.managers) as (keyof Managers)[])\n {\n const manager = this.managers[managerKey];\n if(manager)\n {\n try\n {\n this._log.debug(`Tearing down manager: ${ managerKey }`);\n // eslint-disable-next-line no-await-in-loop\n await manager.$teardown();\n }\n catch (err)\n {\n this._log.error(`Error tearing down manager ${ managerKey }: ${ err }`);\n teardownError = teardownError || (err as Error);\n }\n }\n }\n\n // Teardown all engines\n for(const engineKey of Object.keys(this.engines) as (keyof Engines)[])\n {\n const engine = this.engines[engineKey];\n if(engine)\n {\n try\n {\n this._log.debug(`Tearing down engine: ${ engineKey }`);\n // eslint-disable-next-line no-await-in-loop\n await engine.$teardown();\n }\n catch (err)\n {\n this._log.error(`Error tearing down engine ${ engineKey }: ${ err }`);\n teardownError = teardownError || (err as Error);\n }\n }\n }\n\n this.started = false;\n this._log.info('Game engine stopped successfully');\n\n // If we encountered any errors during teardown, throw the first one\n if(teardownError)\n {\n throw teardownError;\n }\n }\n else\n {\n this._log.warn('Game engine is not started. Skipping stop.');\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Console Logger Backend\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { LogLevel, LoggingBackend } from '../../interfaces/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ntype ColorLevel = LogLevel | 'timer';\n\n/**\n * A logging backend that logs to the console.\n */\nexport class ConsoleBackend implements LoggingBackend\n{\n private timers : Map<string, number>;\n\n constructor()\n {\n this.timers = new Map();\n }\n\n /**\n * Get the CSS style for a specific log level\n */\n private getStyleForLevel(level : ColorLevel) : string\n {\n // CSS styles for browser console\n const styles : Record<ColorLevel, string> = {\n trace: 'color: #999999', // Dim gray\n debug: 'color: #00AAAA', // Cyan\n info: 'color: #00AA00', // Green\n warn: 'color: #AAAA00', // Yellow\n error: 'color: #AA0000', // Red\n timer: 'color: #AA00AA', // Magenta\n none: 'color: inherit', // Default\n };\n\n return styles[level] || styles.none;\n }\n\n /**\n * Format a log message with category, timestamp and log level\n */\n private formatMessage(category : string, level : ColorLevel) : [string, string, string, string]\n {\n // Format time as HH:MM:SS AM/PM\n const now = new Date();\n const hours = now.getHours() % 12 || 12; // Convert 0 to 12 for 12 AM\n const minutes = now.getMinutes().toString()\n .padStart(2, '0');\n const seconds = now.getSeconds().toString()\n .padStart(2, '0');\n const ampm = now.getHours() >= 12 ? 'PM' : 'AM';\n const timestamp = `[${ hours }:${ minutes }:${ seconds } ${ ampm }]`;\n\n const upperLevel = level.toUpperCase();\n\n // Return format strings and styling that will be used with console methods\n return [\n `${ timestamp } %c${ upperLevel }%c (${ category }): `, // Format string with placeholders\n this.getStyleForLevel(level), // Level style\n '', // Default style\n '', // Reset style\n ];\n }\n\n trace(category : string, message : string, ...args : unknown[]) : void\n {\n const [ format, defaultStyle, levelStyle, resetStyle ] = this.formatMessage(category, 'trace');\n console.debug(format, defaultStyle, levelStyle, resetStyle, message, ...args);\n }\n\n debug(category : string, message : string, ...args : unknown[]) : void\n {\n const [ format, defaultStyle, levelStyle, resetStyle ] = this.formatMessage(category, 'debug');\n console.debug(format, defaultStyle, levelStyle, resetStyle, message, ...args);\n }\n\n info(category : string, message : string, ...args : unknown[]) : void\n {\n const [ format, defaultStyle, levelStyle, resetStyle ] = this.formatMessage(category, 'info');\n console.info(format, defaultStyle, levelStyle, resetStyle, message, ...args);\n }\n\n warn(category : string, message : string, ...args : unknown[]) : void\n {\n const [ format, defaultStyle, levelStyle, resetStyle ] = this.formatMessage(category, 'warn');\n console.warn(format, defaultStyle, levelStyle, resetStyle, message, ...args);\n }\n\n error(category : string, message : string, ...args : unknown[]) : void\n {\n const [ format, defaultStyle, levelStyle, resetStyle ] = this.formatMessage(category, 'error');\n console.error(format, defaultStyle, levelStyle, resetStyle, message, ...args);\n }\n\n /**\n * Start a timer with the specified label.\n */\n time(category : string, label : string) : void\n {\n const timerKey = `${ category }:${ label }`;\n this.timers.set(timerKey, performance.now());\n }\n\n /**\n * End a timer and log the elapsed time.\n */\n timeEnd(category : string, label : string) : void\n {\n const timerKey = `${ category }:${ label }`;\n const startTime = this.timers.get(timerKey);\n\n if(startTime !== undefined)\n {\n const duration = performance.now() - startTime;\n this.timers.delete(timerKey);\n\n const [ format, defaultStyle, levelStyle, resetStyle ] = this.formatMessage(category, 'timer');\n console.info(\n `${ format } Timer '${ label }' completed in ${ duration.toFixed(2) }ms`,\n defaultStyle,\n levelStyle,\n resetStyle\n );\n }\n else\n {\n console.warn(`[${ category }]: Timer '${ label }' does not exist`);\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Null Logger Backend\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { LoggingBackend } from '../../interfaces/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A logging backend that does nothing (discards all logs).\n * Useful for production environments or when you want to completely disable logging.\n */\nexport class NullBackend implements LoggingBackend\n{\n trace(_category : string, _message : string, ..._args : unknown[]) : void\n {\n // Do nothing\n }\n\n debug(_category : string, _message : string, ..._args : unknown[]) : void\n {\n // Do nothing\n }\n\n info(_category : string, _message : string, ..._args : unknown[]) : void\n {\n // Do nothing\n }\n\n warn(_category : string, _message : string, ..._args : unknown[]) : void\n {\n // Do nothing\n }\n\n error(_category : string, _message : string, ..._args : unknown[]) : void\n {\n // Do nothing\n }\n\n time(_category : string, _label : string) : void\n {\n // Do nothing\n }\n\n timeEnd(_category : string, _label : string) : void\n {\n // Do nothing\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Logger Utility\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type {\n LogLevel,\n LoggerInterface,\n LoggingBackend,\n} from '../interfaces/logger.ts';\n\nimport { ConsoleBackend } from '../classes/loggers/consoleBackend.ts';\nimport { NullBackend } from '../classes/loggers/nullBackend.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Implementation of Logger interface.\n * Each instance is bound to a specific category and backend.\n */\nexport class SAGELogger implements LoggerInterface\n{\n private category : string;\n private backend : LoggingBackend;\n private minLevel : LogLevel;\n\n constructor(category : string, minLevel : LogLevel = 'none', backend : LoggingBackend = new NullBackend())\n {\n this.category = category;\n this.backend = backend;\n this.minLevel = minLevel;\n }\n\n /**\n * Update the logger's backend and minimum level\n */\n public updateSettings(backend : LoggingBackend, minLevel : LogLevel) : void\n {\n this.backend = backend;\n this.minLevel = minLevel;\n }\n\n trace(message : string, ...args : unknown[]) : void\n {\n if(this.shouldLog('trace'))\n {\n this.backend.trace(this.category, message, ...args);\n }\n }\n\n debug(message : string, ...args : unknown[]) : void\n {\n if(this.shouldLog('debug'))\n {\n this.backend.debug(this.category, message, ...args);\n }\n }\n\n info(message : string, ...args : unknown[]) : void\n {\n if(this.shouldLog('info'))\n {\n this.backend.info(this.category, message, ...args);\n }\n }\n\n warn(message : string, ...args : unknown[]) : void\n {\n if(this.shouldLog('warn'))\n {\n this.backend.warn(this.category, message, ...args);\n }\n }\n\n error(message : string, ...args : unknown[]) : void\n {\n if(this.shouldLog('error'))\n {\n this.backend.error(this.category, message, ...args);\n }\n }\n\n time(label : string) : void\n {\n if(this.minLevel !== 'none')\n {\n this.backend.time(this.category, label);\n }\n }\n\n timeEnd(label : string) : void\n {\n if(this.minLevel !== 'none')\n {\n this.backend.timeEnd(this.category, label);\n }\n }\n\n /**\n * Determine if a message at the given level should be logged based on the current minimum level.\n */\n private shouldLog(level : LogLevel) : boolean\n {\n if(this.minLevel === 'none')\n {\n return false;\n }\n\n switch (this.minLevel)\n {\n case 'trace':\n return true;\n case 'debug':\n return level !== 'trace';\n case 'info':\n return level === 'info' || level === 'warn' || level === 'error';\n case 'warn':\n return level === 'warn' || level === 'error';\n case 'error':\n return level === 'error';\n default:\n return false;\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A utility for creating loggers and managing logging configuration.\n */\nexport class LoggingUtility\n{\n private backend : LoggingBackend;\n private level : LogLevel;\n private loggers : Map<string, LoggerInterface>;\n\n /**\n * Create a new LoggingUtility.\n *\n * @param level - The minimum logging level (defaults to INFO)\n * @param backend - The logging backend to use (defaults to ConsoleBackend)\n */\n constructor(level : LogLevel = 'debug', backend : LoggingBackend = new ConsoleBackend())\n {\n this.backend = backend;\n this.level = level;\n this.loggers = new Map();\n }\n\n /**\n * Set the logging backend. Updates all existing loggers to use the new backend.\n *\n * @param backend\n */\n public setBackend(backend : LoggingBackend) : void\n {\n this.backend = backend;\n\n // Update all existing loggers to use the new backend\n this.loggers.forEach((logger) =>\n {\n if(logger instanceof SAGELogger)\n {\n logger.updateSettings(this.backend, this.level);\n }\n });\n }\n\n /**\n * Set the minimum logging level. Updates all existing loggers.\n *\n * @param level\n */\n public setLevel(level : LogLevel) : void\n {\n this.level = level;\n\n // Update all existing loggers to use the new level\n this.loggers.forEach((logger) =>\n {\n if(logger instanceof SAGELogger)\n {\n logger.updateSettings(this.backend, this.level);\n }\n });\n }\n\n /**\n * Get the current minimum logging level.\n */\n public getLevel() : LogLevel\n {\n return this.level;\n }\n\n /**\n * Get or create a logger for the specified category. Returns an existing logger if one was already created.\n *\n * @param category - Typically the class or module name\n */\n public getLogger(category : string) : LoggerInterface\n {\n let logger = this.loggers.get(category);\n if(!logger)\n {\n logger = new SAGELogger(category, this.level, this.backend);\n this.loggers.set(category, logger);\n }\n\n return logger;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport { ConsoleBackend } from '../classes/loggers/consoleBackend.ts';\nexport { NullBackend } from '../classes/loggers/nullBackend.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Event Bus\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LibraryEventPayloadMap } from '../events/payloads.ts';\nimport type { WildcardPattern } from '../events/types.ts';\nimport { LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A typed game event with compile-time payload checking.\n *\n * @template T - The event type string (e.g., 'input:changed')\n * @template TPayloadMap - The payload map to resolve types from\n */\nexport interface GameEvent<\n T extends string = string,\n TPayloadMap extends Record<string, unknown> = Record<string, unknown>,\n>\n{\n /** The string identifier/category of the event (e.g. \"input:keydown\"). */\n type : T;\n /** The optional ID of whoever sent this event. */\n senderID ?: string;\n /** The optional ID of who should receive this event. */\n targetID ?: string;\n /** The payload, typed based on the payload map. */\n payload : T extends keyof TPayloadMap ? TPayloadMap[T] : unknown;\n}\n\n/**\n * A callback function that receives a {@link GameEvent} with a given payload type.\n *\n * @template T - The event type string\n * @template TPayloadMap - The payload map to resolve types from\n */\nexport type GameEventCallback<\n T extends string = string,\n TPayloadMap extends Record<string, unknown> = Record<string, unknown>,\n> = (event : GameEvent<T, TPayloadMap>) => void | Promise<void>;\n\n/**\n * Represents a subscription record internally within the EventBus.\n *\n * We store the callback as if it handles `GameEvent<any>`\n * because we can't unify multiple different payload types in the same structure.\n */\ninterface Subscription\n{\n /** Precompiled RegExp if this subscription is pattern-based. */\n pattern ?: RegExp;\n /** The callback, but stored to accept any payload type (due to the union nature). */\n callback : (event : GameEvent<any, any>) => void | Promise<void>;\n}\n\n/**\n * An unsubscribe function, returned by any subscribe method.\n */\nexport type Unsubscribe = () => void;\n\n/**\n * Callback for wildcard/pattern subscriptions.\n * Payload is unknown since multiple event types may match.\n */\nexport type WildcardEventCallback\n = (event : GameEvent<string, Record<string, unknown>>) => void | Promise<void>;\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A high-performance, type-safe event bus that supports exact or pattern-based subscriptions.\n *\n * The generic parameter `TPayloadMap` maps event type strings to their payload types,\n * enabling compile-time checking of event/payload combinations.\n *\n * @template TPayloadMap - Map of event type strings to their payload types.\n * Defaults to LibraryEventPayloadMap for internal library usage.\n *\n * @example\n * // Using library events (default)\n * const bus = new GameEventBus();\n * bus.publish({ type: 'level:progress', payload: { levelName: 'Level1', progress: 50 } });\n *\n * @example\n * // User-defined events with full type safety\n * interface MyEvents {\n * 'game:score-updated': { score: number; player: string };\n * 'game:paused': { reason: string };\n * [key: string]: unknown; // Allow other events\n * }\n *\n * const bus = new GameEventBus<MyEvents>();\n * bus.publish({ type: 'game:score-updated', payload: { score: 100, player: 'alice' } });\n *\n * @example\n * // Merging user events with library events\n * const bus = new GameEventBus<MyEvents & LibraryEventPayloadMap>();\n */\nexport class GameEventBus<TPayloadMap extends Record<string, unknown> = LibraryEventPayloadMap> implements Disposable\n{\n /**\n * For exact event-type matches:\n * - Key = event type string (e.g. \"input:keydown\")\n * - Value = set of subscriptions for that exact type\n */\n private directMap = new Map<string, Set<Subscription>>();\n\n /**\n * For pattern-based (wildcard or RegExp) subscriptions.\n */\n private patternSubs = new Set<Subscription>();\n\n /**\n * Logger instance\n */\n private _log : LoggerInterface;\n\n /**\n * Creates a new GameEventBus instance\n *\n * @param logger\n */\n constructor(logger ?: LoggingUtility)\n {\n this._log = logger?.getLogger('EventBus') || new SAGELogger('EventBus');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Subscribe\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Subscribe with automatic detection:\n * - If `eventTypeOrPattern` is a `RegExp` or a string containing `*`, treat it as a pattern subscription.\n * - Otherwise, treat it as an exact subscription.\n *\n * @template T - The event type string (must be a key in TPayloadMap for type safety)\n * @param eventTypeOrPattern - Exact event type string, wildcard pattern, or RegExp\n * @param callback\n * @returns A function that removes this subscription when called\n */\n public subscribe<T extends keyof TPayloadMap & string>(\n eventTypeOrPattern : T | WildcardPattern | RegExp,\n callback : GameEventCallback<T, TPayloadMap>\n ) : Unsubscribe\n {\n this._log.trace(`Subscribe request for: ${ eventTypeOrPattern.toString() }`);\n\n // If it's explicitly a RegExp or includes '*', treat as pattern:\n if(\n eventTypeOrPattern instanceof RegExp\n || (typeof eventTypeOrPattern === 'string' && eventTypeOrPattern.includes('*'))\n )\n {\n return this.subscribePattern(\n eventTypeOrPattern as WildcardPattern | RegExp,\n callback as WildcardEventCallback\n );\n }\n else\n {\n return this.subscribeExact(eventTypeOrPattern as T, callback);\n }\n }\n\n /**\n * Subscribes to an exact event type (e.g. \"input:keydown\").\n *\n * @template T - The event type string (must be a key in TPayloadMap for type safety)\n * @param eventType\n * @param callback\n * @returns A function that removes this subscription when called\n */\n public subscribeExact<T extends keyof TPayloadMap & string>(\n eventType : T,\n callback : GameEventCallback<T, TPayloadMap>\n ) : Unsubscribe\n {\n this._log.debug(`Adding exact subscription for: ${ eventType }`);\n\n let subs = this.directMap.get(eventType);\n if(!subs)\n {\n subs = new Set();\n this.directMap.set(eventType, subs);\n }\n\n // Store as a Subscription that accepts `GameEvent<any>`\n const subscription : Subscription = {\n callback: (event) => callback(event as GameEvent<T, TPayloadMap>),\n };\n subs.add(subscription);\n\n return () =>\n {\n this._log.debug(`Removing exact subscription for: ${ eventType }`);\n subs.delete(subscription);\n if(subs.size === 0)\n {\n this.directMap.delete(eventType);\n }\n };\n }\n\n /**\n * Subscribes with either:\n * - a wildcard string (contains `*`) that gets converted to a RegExp\n * - a RegExp directly\n *\n * Note: Payload typing is not available for wildcards since multiple event types may match.\n *\n * @param patternOrRegExp - Wildcard string (e.g. `\"input:*\"`) or a RegExp\n * @param callback\n * @returns A function that removes this subscription when called\n */\n public subscribePattern(\n patternOrRegExp : WildcardPattern | RegExp,\n callback : WildcardEventCallback\n ) : Unsubscribe\n {\n let regex : RegExp;\n\n if(typeof patternOrRegExp === 'string')\n {\n // We replace '*' with '.*' for wildcard matching\n const escaped = patternOrRegExp\n .replace(/[-/\\\\^$+?.()|[\\]{}]/g, '\\\\$&') // escape special chars\n .replace(/\\*/g, '.*'); // replace '*' with '.*'\n\n // e.g. \"input:*\" => \"input:.*\" => new RegExp(\"^input:.*$\")\n regex = new RegExp(`^${ escaped }$`);\n this._log.debug(\n `Adding pattern subscription for string: ${ patternOrRegExp }, regex: ${ regex.toString() }`\n );\n }\n else\n {\n // If already a RegExp, use as-is\n regex = patternOrRegExp;\n this._log.debug(`Adding pattern subscription for regex: ${ regex.toString() }`);\n }\n\n const subscription : Subscription = {\n pattern: regex,\n callback: (event) => callback(event as GameEvent<string, Record<string, unknown>>),\n };\n this.patternSubs.add(subscription);\n\n return () =>\n {\n this._log.debug(`Removing pattern subscription: ${ regex.toString() }`);\n this.patternSubs.delete(subscription);\n };\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Publish\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Publishes an event, invoking all matching callbacks.\n *\n * **Async Execution Model:**\n * Callbacks are invoked asynchronously via the microtask queue using `Promise.resolve().then()`.\n * This means `publish()` returns immediately, before any callbacks execute. This design:\n * - Prevents stack overflow from recursive event publishing\n * - Allows multiple events to be queued before processing\n * - Is ideal for fire-and-forget events (notifications, state changes)\n *\n * **Not suitable for request-response patterns.** If you need to wait for a callback's\n * result, consider using a different communication mechanism.\n *\n * @template T - The event type string (must be a key in TPayloadMap for type safety)\n * @param event\n */\n public publish<T extends keyof TPayloadMap & string>(\n event : GameEvent<T, TPayloadMap>\n ) : void\n {\n this._log.trace(`Publishing event: ${ event.type }`, event);\n\n let callbackCount = 0;\n\n // 1) Exact match\n const directSubs = this.directMap.get(event.type);\n if(directSubs)\n {\n callbackCount += directSubs.size;\n for(const sub of directSubs)\n {\n Promise.resolve()\n .then(() => sub.callback(event))\n .catch((err) =>\n {\n this._log.error(`Error in event handler for '${ event.type }':`, err);\n });\n }\n }\n\n // 2) Pattern subscriptions\n for(const sub of this.patternSubs)\n {\n if(sub.pattern && sub.pattern.test(event.type))\n {\n callbackCount++;\n Promise.resolve()\n .then(() => sub.callback(event))\n .catch((err) =>\n {\n this._log.error(`Error in pattern event handler for '${ event.type }':`, err);\n });\n }\n }\n\n if(callbackCount === 0)\n {\n this._log.debug(`No subscribers found for event: ${ event.type }`);\n }\n else\n {\n this._log.trace(`Event ${ event.type } dispatched to ${ callbackCount } subscribers`);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Tears down the event bus and clears all subscriptions.\n * After calling this method, no events will be delivered.\n */\n public async $teardown() : Promise<void>\n {\n this._log.debug('Tearing down EventBus');\n this.directMap.clear();\n this.patternSubs.clear();\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Audio Engine\n//----------------------------------------------------------------------------------------------------------------------\n\nimport {\n type AudioBus,\n type AudioEngineV2,\n CreateAudioEngineAsync,\n type IStaticSoundOptions,\n type MainAudioBus,\n type PrimaryAudioBus,\n type StaticSound,\n} from '@babylonjs/core';\n\n// Interfaces\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\n\n// Utils\nimport type { LoggingUtility } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class AudioEngine implements Disposable\n{\n private _engine : AudioEngineV2 | null = null;\n private _mainBus : MainAudioBus | null = null;\n private _buses = new Map<string, AudioBus>();\n private _log : LoggerInterface;\n\n constructor(logger : LoggingUtility)\n {\n this._log = logger.getLogger('AudioEngine');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n async initialize() : Promise<void>\n {\n this._log.info('Initializing audio engine...');\n\n this._engine = await CreateAudioEngineAsync();\n this._mainBus = await this._engine.createMainBusAsync('main');\n\n this._log.info('Audio engine initialized.');\n }\n\n async $teardown() : Promise<void>\n {\n this._log.info('Tearing down audio engine...');\n\n this._buses.clear();\n this._mainBus = null;\n\n if(this._engine)\n {\n this._engine.dispose();\n this._engine = null;\n }\n\n this._log.info('Audio engine torn down.');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Bus Management\n //------------------------------------------------------------------------------------------------------------------\n\n async createBus(name : string) : Promise<AudioBus>\n {\n if(!this._engine || !this._mainBus)\n {\n throw new Error('AudioEngine not initialized');\n }\n\n this._log.debug(`Creating audio bus: ${ name }`);\n const bus = await this._engine.createBusAsync(name, { outBus: this._mainBus });\n this._buses.set(name, bus);\n return bus;\n }\n\n getBus(name : string) : AudioBus | undefined\n {\n return this._buses.get(name);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Sound Creation\n //------------------------------------------------------------------------------------------------------------------\n\n async createSound(\n name : string,\n url : string,\n bus ?: AudioBus,\n options ?: Partial<IStaticSoundOptions>\n ) : Promise<StaticSound>\n {\n if(!this._engine)\n {\n throw new Error('AudioEngine not initialized');\n }\n\n const outBus : PrimaryAudioBus | undefined = bus ?? this._mainBus ?? undefined;\n\n // Spread user options and inject outBus; the cast is needed because\n // IStaticSoundOptions.outBus accepts Nullable<PrimaryAudioBus>\n const soundOptions = {\n ...options,\n ...(outBus ? { outBus } : {}),\n } as Partial<IStaticSoundOptions>;\n\n return this._engine.createSoundAsync(name, url, soundOptions);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Volume\n //------------------------------------------------------------------------------------------------------------------\n\n setMasterVolume(volume : number) : void\n {\n if(!this._engine)\n {\n throw new Error('AudioEngine not initialized');\n }\n\n this._engine.volume = volume;\n }\n\n getMasterVolume() : number\n {\n if(!this._engine)\n {\n return 1;\n }\n\n return this._engine.volume;\n }\n\n setBusVolume(bus : AudioBus, volume : number) : void\n {\n bus.volume = volume;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Scene Engine\n//\n// Wraps BabylonJS scene lifecycle, mesh loading, physics, cameras, and lights.\n// All built-in loaders (glTF, OBJ, STL, etc.) are registered via registerBuiltInLoaders()\n// and loaded on-demand when first used.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport {\n AbstractEngine,\n AbstractMesh,\n AssetContainer,\n DirectionalLight,\n FreeCamera,\n HavokPlugin,\n HemisphericLight,\n ImportMeshAsync,\n Light,\n LoadAssetContainerAsync,\n Mesh,\n MeshBuilder,\n PhysicsAggregate,\n PhysicsShapeType,\n PointLight,\n RectAreaLight,\n Scene,\n SpotLight,\n Vector3,\n} from '@babylonjs/core';\n\n// Register all built-in loaders (glTF, OBJ, STL, etc.) -- loaded on-demand when first used\nimport { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic';\nregisterBuiltInLoaders();\nimport type { HavokPhysicsWithBindings } from '@babylonjs/havok';\n\n// Interfaces\nimport type { GameCanvas } from '../interfaces/game.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\n\n// Utils\nimport { type LoggingUtility } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class SceneEngine implements Disposable\n{\n private _canvas : GameCanvas;\n private _engine : AbstractEngine;\n private _havok : HavokPhysicsWithBindings;\n private _log : LoggerInterface;\n\n constructor(canvas : GameCanvas, engine : AbstractEngine, havok : HavokPhysicsWithBindings, logger : LoggingUtility)\n {\n this._canvas = canvas;\n this._engine = engine;\n this._havok = havok;\n this._log = logger.getLogger('SceneEngine');\n }\n\n /**\n * Creates a new scene with physics enabled\n */\n public createScene() : Scene\n {\n return new Scene(this._engine);\n }\n\n public enablePhysics(\n scene : Scene,\n gravityVector : Vector3 = new Vector3(0, -9.8, 0),\n floatingOriginWorldRadius ?: number\n ) : void\n {\n this._log.debug(\n `Enabling physics with gravity (${ gravityVector.x }, ${ gravityVector.y }, ${ gravityVector.z })...`\n );\n\n const pluginOptions = floatingOriginWorldRadius !== undefined\n ? { floatingOriginWorldRadius }\n : undefined;\n\n // Each scene gets its own HavokPlugin so that scene.dispose() doesn't corrupt the shared WASM module.\n const plugin = new HavokPlugin(true, this._havok, pluginOptions);\n scene.enablePhysics(gravityVector, plugin);\n }\n\n // Camera Utilities\n /**\n * Creates a free camera at the specified position, targeting the origin. Attaches controls to the provided\n * canvas, or falls back to the engine's default canvas.\n * @param name\n * @param position\n * @param scene\n * @param canvas - Overrides the default canvas for input controls\n */\n public createFreeCamera(\n name : string,\n position : Vector3,\n scene : Scene,\n canvas ?: GameCanvas\n ) : FreeCamera\n {\n const camera = new FreeCamera(name, position, scene);\n camera.setTarget(Vector3.Zero());\n\n canvas = canvas ?? this._canvas;\n\n if(canvas)\n {\n camera.attachControl(canvas, true);\n }\n\n return camera;\n }\n\n // Lighting Utilities\n /**\n * Creates a hemispheric light (ambient lighting)\n * @param name\n * @param direction\n * @param scene\n * @param intensity - Default: 0.7\n */\n public createHemisphericLight(\n name : string,\n direction : Vector3,\n scene : Scene,\n intensity = 0.7\n ) : HemisphericLight\n {\n const light = new HemisphericLight(name, direction, scene);\n light.intensity = intensity;\n return light;\n }\n\n /**\n * Creates a directional light (sun-like lighting)\n * @param name\n * @param direction\n * @param scene\n * @param intensity - Default: 1.0\n */\n public createDirectionalLight(\n name : string,\n direction : Vector3,\n scene : Scene,\n intensity = 1.0\n ) : DirectionalLight\n {\n const light = new DirectionalLight(name, direction, scene);\n light.intensity = intensity;\n return light;\n }\n\n /**\n * Creates a point light (bulb-like lighting)\n * @param name\n * @param position\n * @param scene\n * @param intensity - Default: 1.0\n */\n public createPointLight(\n name : string,\n position : Vector3,\n scene : Scene,\n intensity = 1.0\n ) : PointLight\n {\n const light = new PointLight(name, position, scene);\n light.intensity = intensity;\n return light;\n }\n\n /**\n * Creates a spotlight (flashlight-like lighting)\n * @param name\n * @param position\n * @param direction\n * @param angle - Cone angle in radians\n * @param exponent - Controls light falloff from center to edge of cone\n * @param scene\n * @param intensity - Default: 1.0\n */\n public createSpotLight(\n name : string,\n position : Vector3,\n direction : Vector3,\n angle : number,\n exponent : number,\n scene : Scene,\n intensity = 1.0\n ) : SpotLight\n {\n const light = new SpotLight(name, position, direction, angle, exponent, scene);\n light.intensity = intensity;\n return light;\n }\n\n /**\n * Creates a rectangular area light (emits from a flat rectangle in the -Z direction)\n * @param name\n * @param position\n * @param width\n * @param height\n * @param scene\n * @param intensity - Default: 1.0\n */\n public createRectAreaLight(\n name : string,\n position : Vector3,\n width : number,\n height : number,\n scene : Scene,\n intensity = 1.0\n ) : RectAreaLight\n {\n const light = new RectAreaLight(name, position, width, height, scene);\n light.intensity = intensity;\n return light;\n }\n\n // Mesh Creation Utilities\n /**\n * Creates a sphere mesh. Defaults: diameter 1, 32 segments.\n * @param name\n * @param options\n * @param scene\n */\n public createSphere(\n name : string,\n options : { diameter ?: number; segments ?: number } = {},\n scene : Scene\n ) : Mesh\n {\n const defaultOptions = { diameter: 1, segments: 32 };\n return MeshBuilder.CreateSphere(name, { ...defaultOptions, ...options }, scene);\n }\n\n /**\n * Creates a box mesh. Default size: 1.\n * @param name\n * @param options\n * @param scene\n */\n public createBox(\n name : string,\n options : { size ?: number; width ?: number; height ?: number; depth ?: number } = {},\n scene : Scene\n ) : Mesh\n {\n const defaultOptions = { size: 1 };\n return MeshBuilder.CreateBox(name, { ...defaultOptions, ...options }, scene);\n }\n\n /**\n * Creates a ground mesh. Defaults: 6x6 with 2 subdivisions.\n * @param name\n * @param options\n * @param scene\n */\n public createGround(\n name : string,\n options : { width ?: number; height ?: number; subdivisions ?: number } = {},\n scene : Scene\n ) : Mesh\n {\n const defaultOptions = { width: 6, height: 6, subdivisions: 2 };\n return MeshBuilder.CreateGround(name, { ...defaultOptions, ...options }, scene);\n }\n\n /**\n * Creates a cylinder mesh. Defaults: height 2, diameter 1 (top and bottom).\n * @param name\n * @param options\n * @param scene\n */\n public createCylinder(\n name : string,\n options : { height ?: number; diameterTop ?: number; diameterBottom ?: number } = {},\n scene : Scene\n ) : Mesh\n {\n const defaultOptions = { height: 2, diameterTop: 1, diameterBottom: 1 };\n return MeshBuilder.CreateCylinder(name, { ...defaultOptions, ...options }, scene);\n }\n\n // Physics Utilities\n /**\n * Adds physics to a mesh. Defaults: mass 1, restitution 0.75, friction 0.5.\n * @param mesh\n * @param shapeType\n * @param physicsProps\n * @param scene\n */\n public addPhysics(\n mesh : Mesh,\n shapeType : PhysicsShapeType,\n physicsProps : { mass ?: number; restitution ?: number; friction ?: number } = {},\n scene : Scene\n ) : PhysicsAggregate\n {\n const defaultProps = { mass: 1, restitution: 0.75, friction: 0.5 };\n return new PhysicsAggregate(mesh, shapeType, { ...defaultProps, ...physicsProps }, scene);\n }\n\n // Model Loading Utilities\n /**\n * Loads a 3D model/asset into an AssetContainer without adding it to the scene automatically.\n * @param sceneFilename\n * @param scene\n */\n public async loadModel(\n sceneFilename : string,\n scene : Scene\n ) : Promise<AssetContainer>\n {\n this._log.debug(`Loading model: ${ sceneFilename }`);\n\n try\n {\n const result = await LoadAssetContainerAsync(`${ sceneFilename }`, scene);\n this._log.debug(`Model loaded successfully: ${ sceneFilename }`);\n return result;\n }\n catch (error)\n {\n this._log.error(`Failed to load model: ${ sceneFilename }`, error);\n throw error;\n }\n }\n\n /**\n * Imports meshes from a file directly into the scene.\n * @param meshNames - Specific meshes to import; empty array imports all\n * @param sceneFilename\n * @param scene\n */\n public async importMeshes(\n meshNames : string[],\n sceneFilename : string,\n scene : Scene\n ) : Promise<{\n meshes : AbstractMesh[];\n particleSystems : unknown[];\n skeletons : unknown[];\n animationGroups : unknown[];\n transformNodes : unknown[];\n geometries : unknown[];\n lights : Light[];\n }>\n {\n this._log.debug(`Importing meshes from: ${ sceneFilename }`);\n\n try\n {\n // Note: meshNames as empty array means \"import nothing\", use undefined for \"import all\"\n const options = meshNames.length > 0 ? { meshNames } : undefined;\n const result = await ImportMeshAsync(`${ sceneFilename }`, scene, options);\n this._log.debug(`Meshes imported successfully: ${ meshNames.length > 0 ? meshNames.join(', ') : 'all' }`);\n return result;\n }\n catch (error)\n {\n this._log.error(`Failed to import meshes from: ${ sceneFilename }`, error);\n throw error;\n }\n }\n\n /**\n * Tears down the scene engine and cleans up any resources\n */\n async $teardown() : Promise<void>\n {\n this._log.info('Tearing down SceneEngine');\n\n // Currently no specific resources to clean up\n // The scenes themselves are managed by the LevelManager\n // and physics engine is disposed at the application level\n\n this._log.info('SceneEngine torn down successfully');\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Asset Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { AbstractMesh, AssetContainer, InstancedMesh, Mesh } from '@babylonjs/core';\n\nimport type { GameEventBus } from '../classes/eventBus.ts';\nimport type { SceneEngine } from '../engines/scene.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport { type LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface CachedContainer\n{\n container : AssetContainer;\n refCount : number;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Central asset cache with preloading, GLB fragment extraction, mesh instancing, and reference counting.\n *\n * Supports fragment syntax (`models/props.glb#chest_lid`) to extract individual meshes from a shared container.\n * Containers are reference-counted and only disposed when all consumers release them.\n */\nexport class AssetManager implements Disposable\n{\n private _eventBus : GameEventBus;\n private _sceneEngine : SceneEngine;\n private _log : LoggerInterface;\n\n /** Full GLB/GLTF asset containers, keyed by base path (without fragment) */\n private _containers = new Map<string, CachedContainer>();\n\n /** Extracted fragment meshes (source meshes for instancing), keyed by full fragment path */\n private _sourceMeshes = new Map<string, AbstractMesh>();\n\n //------------------------------------------------------------------------------------------------------------------\n\n constructor(eventBus : GameEventBus, sceneEngine : SceneEngine, logger ?: LoggingUtility)\n {\n this._eventBus = eventBus;\n this._sceneEngine = sceneEngine;\n this._log = logger?.getLogger('AssetManager') ?? new SAGELogger('AssetManager');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Helpers\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Split a path on `#` into base path and optional fragment name.\n */\n private _parsePath(path : string) : { basePath : string; fragment ?: string }\n {\n const hashIdx = path.indexOf('#');\n if(hashIdx === -1)\n {\n return { basePath: path };\n }\n\n return {\n basePath: path.substring(0, hashIdx),\n fragment: path.substring(hashIdx + 1),\n };\n }\n\n /**\n * Load (or retrieve from cache) the AssetContainer for a base path.\n * Increments refCount.\n */\n private async _loadContainer(basePath : string) : Promise<AssetContainer>\n {\n const cached = this._containers.get(basePath);\n if(cached)\n {\n cached.refCount++;\n return cached.container;\n }\n\n this._log.debug(`Loading asset container: ${ basePath }`);\n\n // LoadAssetContainerAsync needs a scene; create a temporary one\n const tempScene = this._sceneEngine.createScene();\n\n try\n {\n const container = await this._sceneEngine.loadModel(basePath, tempScene);\n\n this._containers.set(basePath, { container, refCount: 1 });\n this._log.debug(`Cached asset container: ${ basePath }`);\n\n return container;\n }\n finally\n {\n tempScene.dispose();\n }\n }\n\n /**\n * Find a mesh by name inside a container. Throws if not found.\n */\n private _findMeshInContainer(container : AssetContainer, meshName : string, fullPath : string) : AbstractMesh\n {\n const mesh = container.meshes.find((entry) => entry.name === meshName);\n if(!mesh)\n {\n const available = container.meshes.map((entry) => entry.name).join(', ');\n throw new Error(\n `Mesh '${ meshName }' not found in container for '${ fullPath }'. `\n + `Available meshes: ${ available }`\n );\n }\n\n return mesh;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Load an asset, caching the result. Supports fragment syntax (`path#meshName`).\n *\n * - Without fragment: loads and caches the full AssetContainer, returns it.\n * - With fragment: loads the container, finds the named mesh, caches it as a source mesh, returns the mesh.\n *\n * Increments refCount on the underlying container each time.\n */\n async load(path : string) : Promise<AssetContainer | AbstractMesh>\n {\n const { basePath, fragment } = this._parsePath(path);\n\n if(fragment)\n {\n // Check if we already cached this fragment mesh\n const cachedMesh = this._sourceMeshes.get(path);\n if(cachedMesh)\n {\n // Still increment refCount on the container\n const cached = this._containers.get(basePath);\n if(cached)\n {\n cached.refCount++;\n }\n\n return cachedMesh;\n }\n\n // Load the container (handles caching + refCount internally)\n const container = await this._loadContainer(basePath);\n\n // Find and cache the named mesh\n const mesh = this._findMeshInContainer(container, fragment, path);\n this._sourceMeshes.set(path, mesh);\n\n return mesh;\n }\n\n // No fragment — return the full container\n return this._loadContainer(basePath);\n }\n\n /**\n * Create an InstancedMesh from a cached source mesh. The path must have been loaded first.\n */\n instance(path : string) : InstancedMesh\n {\n const sourceMesh = this._sourceMeshes.get(path);\n if(!sourceMesh)\n {\n throw new Error(`Asset '${ path }' is not loaded. Call load() first.`);\n }\n\n return (sourceMesh as Mesh).createInstance(`${ sourceMesh.name }-instance`);\n }\n\n /**\n * Clone a cached source mesh into an independent copy. The path must have been loaded first.\n */\n clone(path : string) : Mesh\n {\n const sourceMesh = this._sourceMeshes.get(path);\n if(!sourceMesh)\n {\n throw new Error(`Asset '${ path }' is not loaded. Call load() first.`);\n }\n\n return (sourceMesh as Mesh).clone(`${ sourceMesh.name }-clone`);\n }\n\n /**\n * Preload a batch of asset paths. Emits `asset:progress` after each and `asset:complete` when finished.\n */\n async preload(paths : string[]) : Promise<void>\n {\n const total = paths.length;\n\n for(let i = 0; i < paths.length; i++)\n {\n // eslint-disable-next-line no-await-in-loop\n await this.load(paths[i]);\n\n this._eventBus.publish({\n type: 'asset:progress',\n payload: {\n path: paths[i],\n loaded: i + 1,\n total,\n },\n });\n }\n\n this._eventBus.publish({\n type: 'asset:complete',\n payload: { paths },\n });\n }\n\n /**\n * Decrement refCount for the container backing this path.\n * When refCount hits 0, dispose the container and clean up all fragment meshes from it.\n */\n dispose(path : string) : void\n {\n const { basePath } = this._parsePath(path);\n const cached = this._containers.get(basePath);\n if(!cached)\n {\n return;\n }\n\n cached.refCount--;\n\n if(cached.refCount <= 0)\n {\n this._log.debug(`Disposing asset container: ${ basePath } (refCount reached 0)`);\n\n // Clean up all fragment meshes that came from this container\n for(const [ fragmentPath, mesh ] of this._sourceMeshes)\n {\n if(fragmentPath.startsWith(`${ basePath }#`))\n {\n mesh.dispose();\n this._sourceMeshes.delete(fragmentPath);\n }\n }\n\n cached.container.dispose();\n this._containers.delete(basePath);\n }\n }\n\n /**\n * Dispose all cached containers and fragment meshes.\n */\n disposeAll() : void\n {\n for(const cached of this._containers.values())\n {\n cached.container.dispose();\n }\n\n for(const mesh of this._sourceMeshes.values())\n {\n mesh.dispose();\n }\n\n this._containers.clear();\n this._sourceMeshes.clear();\n }\n\n /**\n * Lifecycle teardown — disposes all assets.\n */\n async $teardown() : Promise<void>\n {\n this.disposeAll();\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Binding Interfaces\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { GameEventBus } from '../classes/eventBus.ts';\nimport type { DeviceType, DeviceValueReader, DeviceValueReaderDefinition, InputState } from './input.ts';\nimport type { Action } from './action.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Types of bindings supported by the system\n */\nexport type BindingType = 'trigger' | 'toggle' | 'value';\n\n/**\n * Array of all supported binding types for validation\n */\nexport const bindingTypes : BindingType[] = [\n 'trigger',\n 'toggle',\n 'value',\n];\n\n/**\n * Context for organizing bindings into groups that can be activated/deactivated together\n */\nexport interface Context\n{\n name : string;\n\n /**\n * Whether this context is exclusive (only one exclusive context can be active at a time)\n */\n exclusive : boolean;\n}\n\n/**\n * Input definition that combines device ID and reader configuration into a single object\n */\nexport type InputDefinition = DeviceValueReaderDefinition & {\n deviceID : string;\n};\n\n/**\n * Options for captureInput() — waits for the next intentional input and returns its descriptor.\n */\nexport interface CaptureInputOptions\n{\n /** Device types to listen for */\n deviceTypes : DeviceType[];\n\n /** Optional filter for source types (e.g. ['key', 'button', 'axis']) */\n sourceTypes ?: ('key' | 'button' | 'axis' | 'position' | 'wheel')[];\n\n /** Optional AbortSignal for cancellation or timeout */\n signal ?: AbortSignal;\n}\n\n/**\n * Base interface for binding definitions with object-based sources\n */\nexport interface BindingDefinitionBase\n{\n type : BindingType;\n\n /** Name of an already-registered action */\n action : string;\n\n input : InputDefinition;\n context ?: string;\n options ?: Record<string, unknown>;\n}\n\n/**\n * Trigger binding definition\n */\nexport interface TriggerBindingDefinition extends BindingDefinitionBase\n{\n type : 'trigger';\n\n options ?: {\n edgeMode ?: 'rising' | 'falling' | 'both';\n threshold ?: number;\n passthrough ?: boolean;\n };\n}\n\n/**\n * Toggle binding definition\n */\nexport interface ToggleBindingDefinition extends BindingDefinitionBase\n{\n type : 'toggle';\n state ?: boolean;\n\n options ?: {\n invert ?: boolean;\n initialState ?: boolean;\n threshold ?: number;\n onValue ?: boolean | number;\n offValue ?: boolean | number;\n };\n}\n\n/**\n * Value binding definition\n */\nexport interface ValueBindingDefinition extends BindingDefinitionBase\n{\n type : 'value';\n options ?: {\n scale ?: number;\n offset ?: number;\n min ?: number;\n max ?: number;\n invert ?: boolean;\n emitOnChange ?: boolean;\n deadzone ?: number;\n };\n}\n\n/**\n * Union type of all binding definitions\n */\nexport type BindingDefinition\n = | TriggerBindingDefinition\n | ToggleBindingDefinition\n | ValueBindingDefinition;\n\n/**\n * Base interface for all input bindings\n */\nexport interface Binding\n{\n readonly type : BindingType;\n readonly action : Action;\n readonly context ?: string;\n readonly deviceID : string;\n readonly deviceType : DeviceType;\n readonly reader : DeviceValueReader;\n\n /**\n * Process input state and potentially emit an action event\n *\n * @param state\n * @param eventBus\n */\n process(state : InputState, eventBus : GameEventBus) : void;\n\n /**\n * Reset edge-detection state for a context activation. When an input state is provided, the binding primes its\n * internal state from the current physical input so that already-held inputs don't produce phantom edges. Without\n * a state, resets to a clean baseline (false).\n *\n * @param state - Last known input state for this binding's device, if available\n */\n resetEdgeState(state ?: InputState) : void;\n\n /**\n * Convert the binding to a JSON-serializable object.\n */\n toJSON() : BindingDefinition;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Input Device Interfaces\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Valid input device types\n */\nexport const validDeviceTypes = [ 'keyboard', 'mouse', 'gamepad' ] as const;\nexport type DeviceType = typeof validDeviceTypes[number];\n\n/**\n * Base input device interface\n */\nexport interface InputDevice\n{\n id : string;\n name : string;\n type : DeviceType;\n connected : boolean;\n}\n\n/**\n * Keyboard device information\n */\nexport interface KeyboardDevice extends InputDevice\n{\n type : 'keyboard';\n}\n\n/**\n * Mouse device information\n */\nexport interface MouseDevice extends InputDevice\n{\n type : 'mouse';\n}\n\n/**\n * Gamepad device information\n */\nexport interface GamepadDevice extends InputDevice\n{\n type : 'gamepad';\n index : number;\n mapping : string;\n axes : number[];\n buttons : GamepadButton[];\n}\n\n/**\n * Base interface for all input device states\n */\nexport interface BaseInputState\n{\n event ?: Event;\n}\n\n/**\n * Button state interface (for both mouse and gamepad)\n */\nexport interface ButtonState\n{\n pressed : boolean;\n touched ?: boolean;\n value ?: number;\n}\n\n/**\n * Mouse position interface\n */\nexport interface Position\n{\n absolute : { x : number, y : number };\n relative : { x : number, y : number };\n}\n\n/**\n * Unified keyboard input state with object literals instead of Maps\n */\nexport interface KeyboardInputState extends BaseInputState\n{\n type : 'keyboard';\n keys : Record<string, boolean>;\n delta : Record<string, boolean>;\n event ?: KeyboardEvent;\n}\n\n/**\n * Unified mouse input state with object literals instead of Maps\n */\nexport interface MouseInputState extends BaseInputState\n{\n type : 'mouse';\n buttons : Record<string, ButtonState>;\n axes : Record<string, number>;\n position : Position;\n wheel ?: {\n deltaX : number;\n deltaY : number;\n deltaZ : number;\n deltaMode : number;\n };\n event ?: MouseEvent | WheelEvent;\n}\n\n/**\n * Unified gamepad input state with object literals instead of Maps\n */\nexport interface GamepadInputState extends BaseInputState\n{\n type : 'gamepad';\n buttons : Record<string, ButtonState>;\n axes : Record<string, number>;\n event ?: GamepadEvent;\n}\n\n/**\n * Unified input state interface that can represent any input device\n */\nexport type InputState = KeyboardInputState | MouseInputState | GamepadInputState;\n\n//----------------------------------------------------------------------------------------------------------------------\n// Type Guards\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Type guard to check if an input state is from a keyboard\n */\nexport function isKeyboardState(state : InputState) : state is KeyboardInputState\n{\n return state.type === 'keyboard';\n}\n\n/**\n * Type guard to check if an input state is from a mouse\n */\nexport function isMouseState(state : InputState) : state is MouseInputState\n{\n return state.type === 'mouse';\n}\n\n/**\n * Type guard to check if an input state is from a gamepad\n */\nexport function isGamepadState(state : InputState) : state is GamepadInputState\n{\n return state.type === 'gamepad';\n}\n\n/**\n * Base interface for serialized device value readers\n */\nexport interface DeviceValueReaderDefinitionBase\n{\n /** The device type: 'keyboard', 'mouse', or 'gamepad' */\n type : DeviceType;\n\n /** The input source category (e.g. 'key', 'button', 'axis', 'position', 'wheel') */\n sourceType : string;\n\n /** The specific input identifier (e.g. 'KeyA', 'button-0', 'axis-1', 'absolute:x') */\n sourceKey : string;\n}\n\n/**\n * Keyboard value reader definition\n */\nexport interface KeyboardValueReaderDefinition extends DeviceValueReaderDefinitionBase\n{\n type : 'keyboard';\n sourceType : 'key';\n sourceKey : string;\n options ?: {\n useDelta ?: boolean;\n };\n}\n\n/**\n * Mouse value reader definition\n */\nexport interface MouseValueReaderDefinition extends DeviceValueReaderDefinitionBase\n{\n type : 'mouse';\n sourceType : string;\n sourceKey : string;\n}\n\n/**\n * Gamepad value reader definition\n */\nexport interface GamepadValueReaderDefinition extends DeviceValueReaderDefinitionBase\n{\n type : 'gamepad';\n sourceType : string;\n sourceKey : string;\n options ?: {\n useAnalogValue ?: boolean;\n deadzone ?: number;\n invert ?: boolean;\n };\n}\n\n/**\n * Union type of all device value reader definitions\n */\nexport type DeviceValueReaderDefinition\n = | KeyboardValueReaderDefinition\n | MouseValueReaderDefinition\n | GamepadValueReaderDefinition;\n\n/**\n * Interface for all device value readers\n * Unified interface for all types of input sources that can extract values from device states\n */\nexport interface DeviceValueReader\n{\n /** The input source category (e.g. 'key', 'button', 'axis', 'position', 'wheel') */\n readonly sourceType : string;\n\n /** The specific input identifier (e.g. 'KeyA', 'button-0', 'axis-1', 'absolute:x') */\n readonly sourceKey : string;\n\n /**\n * Gets the value from an input state, or undefined if this reader doesn't apply to the given state type.\n *\n * @param state\n */\n getValue(state : InputState) : boolean | number | undefined;\n\n /**\n * Convert the reader to a JSON-serializable object.\n */\n toJSON() : DeviceValueReaderDefinition;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Trigger Binding\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { GameEventBus } from '../eventBus.ts';\n\n// Interfaces\nimport type { Action } from '../../interfaces/action.ts';\nimport type { Binding, TriggerBindingDefinition } from '../../interfaces/binding.ts';\nimport type { DeviceType, DeviceValueReader, InputState } from '../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * All supported edge modes for validation\n */\nexport const edgeModes = [ 'rising', 'falling', 'both' ] as const;\n\n/**\n * Defines when a trigger binding will fire based on input state changes\n */\nexport type EdgeMode = typeof edgeModes[number];\n\n/**\n * Options for configuring a trigger binding\n */\nexport interface TriggerBindingOptions\n{\n /**\n * Which edge(s) should cause the trigger to fire\n * - 'rising': Only trigger on false -> true transitions\n * - 'falling': Only trigger on true -> false transitions\n * - 'both': Trigger on any state change\n */\n edgeMode ?: EdgeMode;\n\n /**\n * Threshold for analog inputs (0.0 to 1.0)\n * When input value is >= threshold, it's considered \"active\" (true)\n * When input value is < threshold, it's considered \"inactive\" (false)\n * Defaults to 0.5 for analog inputs\n */\n threshold ?: number;\n\n /**\n * Optional context name this binding belongs to\n */\n context ?: string;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Implementation of a trigger binding, which emits an action when a button/key is pressed or released.\n * Can handle both digital and analog inputs with configurable threshold.\n * Can output either digital or analog values based on the action type.\n */\nexport class TriggerBinding implements Binding\n{\n public readonly type = 'trigger' as const;\n public readonly action : Action;\n public readonly context ?: string;\n public readonly deviceID : string;\n public readonly deviceType : DeviceType;\n public readonly reader : DeviceValueReader;\n\n // Trigger-specific options\n private readonly _edgeMode : EdgeMode;\n private readonly _threshold : number;\n\n // State tracking\n private _lastDigitalState = false;\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Getters\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Get the current options for this trigger binding.\n */\n get options() : TriggerBindingOptions\n {\n return {\n edgeMode: this._edgeMode,\n threshold: this._threshold,\n };\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new trigger binding\n *\n * @param action\n * @param deviceID\n * @param deviceType\n * @param reader\n * @param options\n */\n constructor(\n action : Action,\n deviceID : string,\n deviceType : DeviceType,\n reader : DeviceValueReader,\n options : TriggerBindingOptions = {}\n )\n {\n this.action = action;\n this.deviceID = deviceID;\n this.deviceType = deviceType;\n this.reader = reader;\n this.context = options.context;\n\n // Set options with defaults\n this._edgeMode = options.edgeMode ?? 'rising';\n this._threshold = options.threshold ?? 0.5;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Process input state and emit an action event on the configured edge transition(s).\n *\n * @param state\n * @param eventBus\n */\n public process(state : InputState, eventBus : GameEventBus) : void\n {\n // Extract the raw value using our source\n const rawValue = this.reader.getValue(state) ?? false;\n\n // Determine the digital state based on the threshold\n const digitalState = typeof rawValue === 'boolean'\n ? rawValue\n : rawValue >= this._threshold;\n\n // Determine if we should trigger based on the configured edge mode\n let shouldTrigger = false;\n\n switch (this._edgeMode)\n {\n case 'rising':\n shouldTrigger = digitalState && !this._lastDigitalState;\n break;\n case 'falling':\n shouldTrigger = !digitalState && this._lastDigitalState;\n break;\n case 'both':\n shouldTrigger = digitalState !== this._lastDigitalState;\n break;\n }\n\n // Update last known state for next time\n this._lastDigitalState = digitalState;\n\n // If triggered, emit the action event\n if(shouldTrigger)\n {\n let outputValue : boolean | number;\n if(this.action.type === 'analog')\n {\n // Use raw value and convert to a number if needed\n let finalValue = typeof rawValue === 'number' ? rawValue : (rawValue ? 1.0 : 0.0);\n\n // Apply scaling if action parameters exist\n if(this.action.minValue !== undefined || this.action.maxValue !== undefined)\n {\n const min = this.action.minValue ?? 0;\n const max = this.action.maxValue ?? 1;\n finalValue = min + (finalValue * (max - min));\n }\n outputValue = finalValue;\n }\n else\n {\n outputValue = true;\n }\n const actionEvent = {\n type: `action:${ this.action.name }`,\n payload: {\n value: outputValue,\n deviceId: this.deviceID,\n context: this.context,\n },\n };\n eventBus.publish(actionEvent);\n }\n }\n\n /**\n * Reset edge-detection state for a context activation. When a state is provided, primes from the current physical\n * input so that already-held inputs don't produce phantom rising edges in the newly activated context.\n */\n public resetEdgeState(state ?: InputState) : void\n {\n if(state)\n {\n const rawValue = this.reader.getValue(state) ?? false;\n this._lastDigitalState = typeof rawValue === 'boolean'\n ? rawValue\n : rawValue >= this._threshold;\n }\n else\n {\n this._lastDigitalState = false;\n }\n }\n\n /**\n * Returns a JSON-serializable representation of this trigger binding.\n */\n public toJSON() : TriggerBindingDefinition\n {\n return {\n type: this.type,\n action: this.action.name,\n input: {\n deviceID: this.deviceID,\n ...this.reader.toJSON(),\n },\n context: this.context,\n options: this.options,\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Toggle Binding\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { GameEventBus } from '../eventBus.ts';\n\n// Interfaces\nimport type { Action } from '../../interfaces/action.ts';\nimport type { Binding, ToggleBindingDefinition } from '../../interfaces/binding.ts';\nimport type { DeviceType, DeviceValueReader, InputState } from '../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Options for configuring a toggle binding\n */\nexport interface ToggleBindingOptions\n{\n /**\n * If true, toggle on falling edge (true -> false) instead of rising edge (false -> true)\n */\n invert ?: boolean;\n\n /**\n * Initial toggle state (defaults to false - off)\n */\n initialState ?: boolean;\n\n /**\n * Threshold for analog inputs (0.0 to 1.0)\n * When input value is >= threshold, it's considered \"active\" (true)\n * When input value is < threshold, it's considered \"inactive\" (false)\n * Defaults to 0.5 for analog inputs\n */\n threshold ?: number;\n\n /**\n * Value to emit when the toggle is in the \"on\" state for digital actions\n * If undefined, will emit boolean true\n * Ignored for analog actions\n */\n onValue ?: boolean | number;\n\n /**\n * Value to emit when the toggle is in the \"off\" state for digital actions\n * If undefined, will emit boolean false\n * Ignored for analog actions\n */\n offValue ?: boolean | number;\n\n /**\n * Optional context name this binding belongs to\n */\n context ?: string;\n}\n\n/**\n * Implementation of a toggle binding, which maintains and toggles state between true/false\n * when a button/key is pressed or released, remaining in that state until toggled again.\n * Can handle both digital and analog inputs with configurable threshold.\n * Can output either digital or analog values based on the action type.\n */\nexport class ToggleBinding implements Binding\n{\n public readonly type = 'toggle' as const;\n public readonly action : Action;\n public readonly context ?: string;\n public readonly deviceID : string;\n public readonly deviceType : DeviceType;\n public readonly reader : DeviceValueReader;\n\n // Toggle-specific options\n private readonly _invert : boolean;\n private readonly _threshold : number;\n private readonly _initialState : boolean;\n private readonly _onValue : boolean | number;\n private readonly _offValue : boolean | number;\n\n // State tracking\n private _lastDigitalState = false;\n private _toggleState : boolean;\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Getters\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Get the current toggle state (true = on, false = off)\n */\n public get state() : boolean\n {\n return this._toggleState;\n }\n\n /**\n * Set the toggle state directly (useful for programmatic control)\n */\n public set state(value : boolean)\n {\n this._toggleState = value;\n }\n\n /**\n * Get the value emitted when toggle is in the \"on\" state\n */\n public get onValue() : boolean | number\n {\n return this._onValue;\n }\n\n /**\n * Get the value emitted when toggle is in the \"off\" state\n */\n public get offValue() : boolean | number\n {\n return this._offValue;\n }\n\n /**\n * Get the current value based on toggle state.\n * For analog actions, returns min/max value. For digital, returns on/off value.\n */\n public get value() : boolean | number\n {\n if(this.action.type === 'analog')\n {\n return this._toggleState\n ? (this.action.maxValue ?? 1)\n : (this.action.minValue ?? 0);\n }\n else\n {\n return this._toggleState ? this._onValue : this._offValue;\n }\n }\n\n /**\n * Get the current options for this toggle binding.\n */\n public get options() : ToggleBindingOptions\n {\n return {\n invert: this._invert,\n threshold: this._threshold,\n initialState: this._initialState,\n onValue: this._onValue,\n offValue: this._offValue,\n };\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new toggle binding\n *\n * @param action\n * @param deviceID\n * @param deviceType\n * @param reader\n * @param options\n */\n constructor(\n action : Action,\n deviceID : string,\n deviceType : DeviceType,\n reader : DeviceValueReader,\n options : ToggleBindingOptions = {}\n )\n {\n this.action = action;\n this.deviceID = deviceID;\n this.deviceType = deviceType;\n this.reader = reader;\n this.context = options.context;\n\n // Set options with defaults\n this._invert = options.invert ?? false;\n this._threshold = options.threshold ?? 0.5;\n this._initialState = options.initialState ?? false;\n this._toggleState = this._initialState;\n\n // For digital actions, these control the output value\n // For analog actions, these are not used (we use action properties instead)\n this._onValue = options.onValue ?? true;\n this._offValue = options.offValue ?? false;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Process input state and emit an action event if the toggle flipped.\n *\n * @param state\n * @param eventBus\n */\n public process(state : InputState, eventBus : GameEventBus) : void\n {\n const rawValue = this.reader.getValue(state) ?? false;\n const digitalState = typeof rawValue === 'boolean'\n ? rawValue\n : rawValue >= this._threshold;\n\n const shouldToggle = this._invert\n ? !digitalState && this._lastDigitalState\n : digitalState && !this._lastDigitalState;\n\n this._lastDigitalState = digitalState;\n\n if(!shouldToggle) { return; }\n\n this._toggleState = !this._toggleState;\n\n eventBus.publish({\n type: `action:${ this.action.name }`,\n payload: {\n value: this.value,\n deviceId: this.deviceID,\n context: this.context,\n },\n });\n }\n\n /**\n * Reset edge-detection state for a context activation. When a state is provided, primes from the current physical\n * input so that already-held inputs don't produce phantom edges in the newly activated context.\n */\n public resetEdgeState(state ?: InputState) : void\n {\n if(state)\n {\n const rawValue = this.reader.getValue(state) ?? false;\n this._lastDigitalState = typeof rawValue === 'boolean'\n ? rawValue\n : rawValue >= this._threshold;\n }\n else\n {\n this._lastDigitalState = false;\n }\n }\n\n /**\n * Reset toggle to its initial state\n */\n public reset() : void\n {\n this._toggleState = this._initialState;\n }\n\n /**\n * Returns a JSON-serializable representation of this toggle binding.\n */\n public toJSON() : ToggleBindingDefinition\n {\n return {\n type: this.type,\n action: this.action.name,\n input: {\n deviceID: this.deviceID,\n ...this.reader.toJSON(),\n },\n state: this._toggleState,\n context: this.context,\n options: this.options,\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Value Binding\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { GameEventBus } from '../eventBus.ts';\n\n// Interfaces\nimport type { Action } from '../../interfaces/action.ts';\nimport type { Binding, ValueBindingDefinition } from '../../interfaces/binding.ts';\nimport type { DeviceType, DeviceValueReader, InputState } from '../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Options for configuring a value binding\n */\nexport interface ValueBindingOptions\n{\n /**\n * Factor to scale the input value by (1.0 = no scaling)\n */\n scale ?: number;\n\n /**\n * Value to add to the input value after scaling\n */\n offset ?: number;\n\n /**\n * Whether to invert the input value (multiply by -1)\n */\n invert ?: boolean;\n\n /**\n * Optional context name this binding belongs to\n */\n context ?: string;\n\n /**\n * If true, only emit values when they change\n * If false, always emit values on each process call\n */\n emitOnChange ?: boolean;\n\n /**\n * Deadzone threshold (0.0 to 1.0)\n * Input values below this threshold will be treated as 0\n */\n deadzone ?: number;\n\n /**\n * Value to emit when the input is digital and true\n * Only used when action is digital\n */\n onValue ?: boolean | number;\n\n /**\n * Value to emit when the input is digital and false\n * Only used when action is digital\n */\n offValue ?: boolean | number;\n\n /**\n * Minimum value to clamp output to\n */\n min ?: number;\n\n /**\n * Maximum value to clamp output to\n */\n max ?: number;\n}\n\n/**\n * Implementation of a value binding, which directly converts an analog input\n * to an analog output with optional scaling, offset, and clamping.\n * Can handle both digital and analog inputs/outputs based on action type.\n */\nexport class ValueBinding implements Binding\n{\n public readonly type = 'value' as const;\n public readonly action : Action;\n public readonly context ?: string;\n public readonly deviceID : string;\n public readonly deviceType : DeviceType;\n public readonly reader : DeviceValueReader;\n\n // Value-specific options\n private readonly _scale : number;\n private readonly _offset : number;\n private readonly _invert : boolean;\n private readonly _emitOnChange : boolean;\n private readonly _deadzone : number;\n private readonly _onValue : boolean | number;\n private readonly _offValue : boolean | number;\n private readonly _min : number;\n private readonly _max : number;\n\n // State tracking\n private _lastValue : number | boolean | undefined;\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Get the current options for this value binding.\n */\n get options() : ValueBindingOptions\n {\n return {\n scale: this._scale,\n offset: this._offset,\n invert: this._invert,\n emitOnChange: this._emitOnChange,\n deadzone: this._deadzone,\n onValue: this._onValue,\n offValue: this._offValue,\n min: this._min,\n max: this._max,\n };\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new value binding\n *\n * @param action\n * @param deviceID\n * @param reader\n * @param options\n */\n constructor(\n action : Action,\n deviceID : string,\n deviceType : DeviceType,\n reader : DeviceValueReader,\n options : ValueBindingOptions = {}\n )\n {\n this.action = action;\n this.deviceID = deviceID;\n this.deviceType = deviceType;\n this.reader = reader;\n this.context = options.context;\n\n // Common options that apply to both analog and digital\n this._scale = options.scale ?? 1.0;\n this._offset = options.offset ?? 0.0;\n this._invert = options.invert ?? false;\n this._emitOnChange = options.emitOnChange ?? true;\n this._deadzone = options.deadzone ?? 0.0;\n\n // For digital actions, these control the output value\n // For analog actions, these are not used\n this._onValue = options.onValue ?? true;\n this._offValue = options.offValue ?? false;\n\n // Store min/max values explicitly\n const defaultMin = this.action.type === 'analog'\n ? this.action.minValue ?? Number.NEGATIVE_INFINITY\n : Number.NEGATIVE_INFINITY;\n this._min = options.min ?? defaultMin;\n\n const defaultMax = this.action.type === 'analog'\n ? this.action.maxValue ?? Number.POSITIVE_INFINITY\n : Number.POSITIVE_INFINITY;\n this._max = options.max ?? defaultMax;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Process input state, apply scaling/deadzone/clamping, and emit an action event if the value changed.\n *\n * @param state\n * @param eventBus\n */\n public process(state : InputState, eventBus : GameEventBus) : void\n {\n const rawValue = this.reader.getValue(state);\n if(rawValue === undefined) { return; }\n\n const numericValue = typeof rawValue === 'boolean' ? (rawValue ? 1.0 : 0.0) : rawValue;\n\n let value = this._deadzone > 0 && Math.abs(numericValue) < this._deadzone ? 0 : numericValue;\n value = ((this._invert ? -value : value) * this._scale) + this._offset;\n\n // Apply min/max clamping\n value = Math.max(this._min, Math.min(this._max, value));\n\n if(this._emitOnChange && this._lastValue === value) { return; }\n this._lastValue = value;\n\n eventBus.publish({\n type: `action:${ this.action.name }`,\n payload: {\n value,\n deviceId: this.deviceID,\n context: this.context,\n },\n });\n }\n\n /**\n * No-op for value bindings — they have no edge-detection state to reset.\n */\n public resetEdgeState(_state ?: InputState) : void\n {\n // Value bindings don't track edge state\n }\n\n /**\n * Returns a JSON-serializable representation of this value binding.\n */\n public toJSON() : ValueBindingDefinition\n {\n return {\n type: this.type,\n action: this.action.name,\n input: {\n deviceID: this.deviceID,\n ...this.reader.toJSON(),\n },\n context: this.context,\n options: this.options,\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Keyboard Value Reader\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type {\n DeviceValueReader,\n InputState,\n KeyboardInputState,\n KeyboardValueReaderDefinition,\n} from '../../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Types of keyboard input sources\n */\nexport type KeyboardSourceType = 'key';\n\n/**\n * Options for configuring a keyboard value reader\n */\nexport interface KeyboardReaderOptions\n{\n /**\n * Whether to use delta state instead of current state\n * When true, the source will only return true on the initial key press\n */\n useDelta ?: boolean;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Reads values from keyboard input states\n */\nexport class KeyboardValueReader implements DeviceValueReader\n{\n /**\n * Always 'key' for keyboard readers.\n */\n public readonly sourceType : KeyboardSourceType = 'key';\n\n /**\n * The key code to monitor (e.g. \"KeyA\", \"Space\", \"ArrowUp\")\n */\n public readonly sourceKey : string;\n\n /**\n * When true, reads from delta (only true on the frame the key was first pressed/released)\n */\n private readonly useDelta : boolean;\n\n /**\n * Creates a new KeyboardValueReader\n *\n * @param keyCode - KeyboardEvent.code value (e.g. \"KeyA\", \"Space\", \"ArrowUp\")\n * @param options\n */\n constructor(keyCode : string, options : KeyboardReaderOptions = {})\n {\n this.sourceKey = keyCode;\n this.useDelta = options.useDelta || false;\n }\n\n /**\n * Gets the value of the key state. Returns undefined for non-keyboard input states.\n *\n * @param state\n */\n public getValue(state : InputState) : boolean | undefined\n {\n // Check if the state is from the correct device type\n if(state.type !== 'keyboard')\n {\n return undefined;\n }\n\n const keyboardState = state as KeyboardInputState;\n\n // Get value from either the current state or delta state based on configuration\n if(this.useDelta)\n {\n return keyboardState.delta[this.sourceKey];\n }\n else\n {\n return keyboardState.keys[this.sourceKey];\n }\n }\n\n /**\n * Creates a KeyboardValueReader from a string representation\n *\n * @param sourceKey - KeyboardEvent.code value (e.g. \"KeyA\", \"Space\")\n * @param options\n */\n public static fromString(sourceKey : string, options : KeyboardReaderOptions = {}) : KeyboardValueReader\n {\n return new KeyboardValueReader(sourceKey, options);\n }\n\n /**\n * Returns a JSON-serializable representation of this keyboard value reader.\n */\n public toJSON() : KeyboardValueReaderDefinition\n {\n return {\n\n type: 'keyboard',\n sourceType: this.sourceType,\n sourceKey: this.sourceKey,\n options: {\n useDelta: this.useDelta,\n },\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Mouse Value Reader\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { DeviceValueReader, InputState, MouseValueReaderDefinition } from '../../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Types of mouse input sources\n */\nexport type MouseSourceType = 'button' | 'position' | 'wheel';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Reads values from mouse input states\n */\nexport class MouseValueReader implements DeviceValueReader\n{\n /**\n * Which category of mouse input: button press, cursor position, or scroll wheel.\n */\n readonly sourceType : MouseSourceType;\n\n /**\n * Identifies the input within the source type (e.g. \"button-0\", \"absolute:x\", \"deltaY\")\n */\n readonly sourceKey : string;\n\n /**\n * Creates a new MouseValueReader\n *\n * @param sourceType\n * @param sourceKey - e.g. \"button-0\", \"absolute:x\", \"deltaY\"\n */\n constructor(sourceType : MouseSourceType, sourceKey : string)\n {\n this.sourceType = sourceType;\n this.sourceKey = sourceKey;\n }\n\n /**\n * Gets the value of the mouse input source. Returns undefined for non-mouse input states.\n *\n * @param state\n */\n public getValue(state : InputState) : boolean | number | undefined\n {\n // Check if the state is from the correct device type\n if(state.type !== 'mouse')\n {\n return undefined;\n }\n\n switch (this.sourceType)\n {\n case 'button':\n {\n const buttonState = state.buttons[this.sourceKey];\n return buttonState ? buttonState.pressed : undefined;\n }\n\n case 'position':\n {\n // Position keys are in format \"positionType:axis\" (e.g., \"absolute:x\")\n const [ posType, axis ] = this.sourceKey.split(':');\n\n if(!posType || !axis || (posType !== 'absolute' && posType !== 'relative')\n || (axis !== 'x' && axis !== 'y'))\n {\n return undefined;\n }\n\n return state.position[posType][axis];\n }\n\n case 'wheel':\n {\n const isValidWheelKey = this.sourceKey === 'deltaX' || this.sourceKey === 'deltaY'\n || this.sourceKey === 'deltaZ';\n if(state.wheel && isValidWheelKey)\n {\n return state.wheel[this.sourceKey];\n }\n return undefined;\n }\n\n default:\n return undefined;\n }\n }\n\n /**\n * Creates a MouseValueReader from a colon-delimited string (e.g. \"button:0\", \"position:absolute:x\")\n *\n * @param sourceTypeString\n */\n public static fromString(sourceTypeString : string) : MouseValueReader\n {\n const [ sourceType, ...keyParts ] = sourceTypeString.split(':');\n const sourceKey = keyParts.join(':');\n\n if(!sourceType || !sourceKey)\n {\n throw new Error(`Invalid mouse source format: ${ sourceTypeString }`);\n }\n\n return new MouseValueReader(\n sourceType as MouseSourceType,\n sourceKey\n );\n }\n\n /**\n * Returns a JSON-serializable representation of this mouse value reader.\n */\n public toJSON() : MouseValueReaderDefinition\n {\n // Always return consistent 'mouse' type with sourceType and sourceKey\n return {\n type: 'mouse',\n sourceType: this.sourceType,\n sourceKey: this.sourceKey,\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Gamepad Value Reader\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { DeviceValueReader, GamepadValueReaderDefinition, InputState } from '../../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Types of gamepad input sources\n */\nexport type GamepadSourceType = 'button' | 'axis';\n\n/**\n * Options for configuring a gamepad value reader\n */\nexport interface GamepadReaderOptions\n{\n /**\n * Whether to use the analog value instead of boolean pressed state for buttons\n */\n useAnalogValue ?: boolean;\n\n /**\n * Deadzone value (0.0 - 1.0) for analog axes\n * Input values below this threshold will be treated as 0\n */\n deadzone ?: number;\n\n /**\n * Whether to invert the axis values\n */\n invert ?: boolean;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Reads values from gamepad input states\n */\nexport class GamepadValueReader implements DeviceValueReader\n{\n /**\n * Whether this reader targets a button or analog axis.\n */\n readonly sourceType : GamepadSourceType;\n\n /**\n * Identifies the specific button or axis (e.g. \"button-0\", \"axis-1\")\n */\n readonly sourceKey : string;\n\n /**\n * When true, returns the pressure-sensitive float value instead of boolean pressed state for buttons.\n */\n private readonly useAnalogValue : boolean;\n\n /**\n * Axis values below this threshold are treated as 0 (default 10%)\n */\n private readonly deadzone : number;\n\n /**\n * Multiplies axis values by -1 when true\n */\n private readonly invert : boolean;\n\n /**\n * Creates a new GamepadValueReader\n *\n * @param sourceType\n * @param sourceKey - e.g. \"button-0\", \"axis-1\"\n * @param options\n */\n constructor(sourceType : GamepadSourceType, sourceKey : string, options : GamepadReaderOptions = {})\n {\n this.sourceType = sourceType;\n this.sourceKey = sourceKey;\n this.useAnalogValue = options.useAnalogValue || false;\n this.deadzone = options.deadzone || 0.1; // Default 10% deadzone\n this.invert = options.invert || false;\n }\n\n /**\n * Gets the value of the input source. Returns undefined for non-gamepad input states.\n *\n * @param state\n */\n public getValue(state : InputState) : boolean | number | undefined\n {\n // Check if the state is from the correct device type\n if(state.type !== 'gamepad')\n {\n return undefined;\n }\n\n switch (this.sourceType)\n {\n case 'button':\n {\n const buttonState = state.buttons[this.sourceKey];\n\n if(!buttonState)\n {\n return undefined;\n }\n\n return this.useAnalogValue ? buttonState.value : buttonState.pressed;\n }\n\n case 'axis':\n {\n let value = state.axes[this.sourceKey];\n\n if(value === undefined)\n {\n return undefined;\n }\n\n // Apply deadzone\n if(Math.abs(value) < this.deadzone)\n {\n value = 0;\n }\n\n // Apply inversion if needed\n return this.invert ? -value : value;\n }\n\n default:\n return undefined;\n }\n }\n\n /**\n * Creates a GamepadValueReader from a colon-delimited string (e.g. \"button:0\", \"axis:1\")\n *\n * @param sourceTypeString\n * @param options\n */\n public static fromString(sourceTypeString : string, options : GamepadReaderOptions = {}) : GamepadValueReader\n {\n const [ sourceType, sourceKey ] = sourceTypeString.split(':');\n\n if(!sourceType || !sourceKey)\n {\n throw new Error(`Invalid gamepad source format: ${ sourceTypeString }`);\n }\n\n return new GamepadValueReader(\n sourceType as GamepadSourceType,\n sourceKey,\n options\n );\n }\n\n /**\n * Returns a JSON-serializable representation of this gamepad value reader.\n */\n public toJSON() : GamepadValueReaderDefinition\n {\n return {\n type: 'gamepad',\n sourceType: this.sourceType,\n sourceKey: this.sourceKey,\n options: {\n useAnalogValue: this.useAnalogValue,\n deadzone: this.deadzone,\n invert: this.invert,\n },\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Binding Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { GameEventBus } from '../classes/eventBus.ts';\n\n// Interfaces\nimport {\n type Binding,\n type BindingDefinition,\n type CaptureInputOptions,\n type Context,\n type InputDefinition,\n bindingTypes,\n} from '../interfaces/binding.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport {\n type DeviceType,\n type DeviceValueReader,\n type DeviceValueReaderDefinition,\n type InputDevice,\n type InputState,\n isGamepadState,\n isKeyboardState,\n isMouseState,\n} from '../interfaces/input.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type { Action } from '../interfaces/action.ts';\n\n// Bindings\nimport { TriggerBinding } from '../classes/bindings/trigger.ts';\nimport { ToggleBinding } from '../classes/bindings/toggle.ts';\nimport { ValueBinding } from '../classes/bindings/value.ts';\n\n// Value Readers\nimport { KeyboardValueReader } from '../classes/input/readers/keyboard.ts';\nimport { MouseValueReader } from '../classes/input/readers/mouse.ts';\nimport { GamepadValueReader } from '../classes/input/readers/gamepad.ts';\n\n// Utils\nimport { LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Configuration object structure for binding manager\n */\nexport interface BindingConfiguration\n{\n /** All registered actions */\n actions : Action[];\n\n /** All registered bindings */\n bindings : BindingDefinition[];\n\n /** All registered contexts */\n contexts : {\n name : string;\n exclusive : boolean;\n active : boolean;\n }[];\n}\n\n/**\n * Minimum axis magnitude for a gamepad event to count as intentional activity.\n * This is separate from per-binding deadzones — it just prevents idle stick drift\n * from flipping the last-active device type.\n */\nconst GAMEPAD_ACTIVITY_DEADZONE = 0.05;\n\n/**\n * Minimum axis magnitude for a gamepad event to count as intentional during captureInput().\n * Much higher than the activity deadzone to avoid false positives from stick drift.\n */\nconst CAPTURE_AXIS_DEADZONE = 0.5;\n\n/**\n * Internal state tracked while a captureInput() call is in progress.\n */\ninterface CaptureState\n{\n resolve : (result : InputDefinition) => void;\n reject : (reason : unknown) => void;\n options : CaptureInputOptions;\n snapshot : string[];\n abortCleanup ?: () => void;\n}\n\n/* eslint-disable no-continue */\n\n/**\n * Manages input bindings and actions for the game\n */\nexport class BindingManager implements Disposable\n{\n /** Map of device IDs to their bindings */\n private _bindings = new Map<string, Binding[]>();\n\n /** Map of action names to their action definitions */\n private _actions = new Map<string, Action>();\n\n /** Map of all registered contexts by name for O(1) lookup */\n private _contexts = new Map<string, Context>();\n\n /**\n * Set of all active contexts (both exclusive and non-exclusive)\n */\n private _activeContexts = new Set<string>();\n\n /** The most recent device type that produced intentional input */\n private _lastActiveDeviceType : DeviceType | null = null;\n\n /** Last input state per device, used to prime edge state on context activation */\n private _lastInputState = new Map<string, InputState>();\n\n /** Active captureInput() state, or null when not capturing */\n private _captureState : CaptureState | null = null;\n\n /** Event bus for handling game events */\n private _eventBus : GameEventBus;\n\n /** Unsubscribe function for input events */\n private _inputUnsubscribe : (() => void) | null = null;\n\n /** Logger instance */\n private _log : LoggerInterface;\n\n /**\n * Creates an instance of BindingManager.\n * Automatically subscribes to 'input:changed' events on the event bus.\n *\n * @param eventBus\n * @param logger\n */\n constructor(eventBus : GameEventBus, logger ?: LoggingUtility)\n {\n this._eventBus = eventBus;\n\n // Bind to input events\n this._inputUnsubscribe = this._eventBus.subscribe(\n 'input:changed',\n (event) =>\n {\n const payload = event.payload as { device : InputDevice; state : InputState };\n if(payload)\n {\n this.$handleInput(payload.device, payload.state);\n }\n }\n );\n\n this._log = logger?.getLogger('BindingManager') || new SAGELogger('BindingManager');\n this._log.debug('BindingManager initialized');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Checks if a binding's context is currently active.\n * Bindings with no context are always considered active.\n *\n * @param binding\n */\n private _isBindingContextActive(binding : Binding) : boolean\n {\n // If no context is specified, binding is always active regardless of active contexts\n if(!binding.context)\n {\n return true;\n }\n\n // Otherwise check if the binding's specific context is active\n return this._activeContexts.has(binding.context);\n }\n\n /**\n * Get a context by name, creating it if it doesn't exist.\n *\n * @param contextName\n * @param exclusive - Only used when creating a new context\n */\n private _getOrCreateContext(contextName : string, exclusive = true) : Context\n {\n let context = this._contexts.get(contextName);\n\n if(!context)\n {\n context = {\n name: contextName,\n exclusive,\n };\n\n this._contexts.set(contextName, context);\n this._log.debug(`Auto-created context \"${ contextName }\" (exclusive: ${ exclusive })`);\n }\n\n return context;\n }\n\n /**\n * Deactivate all exclusive contexts except the specified one.\n * Non-exclusive contexts are left untouched.\n *\n * @param exceptContextName\n */\n private _deactivateExclusiveContexts(exceptContextName ?: string) : string[]\n {\n const deactivatedContexts : string[] = [];\n\n // Find all active exclusive contexts\n for(const contextName of this._activeContexts)\n {\n // Skip the context we want to keep\n if(contextName === exceptContextName)\n {\n continue;\n }\n\n const context = this._contexts.get(contextName);\n\n // If this is an exclusive context, deactivate it\n if(context?.exclusive)\n {\n this._activeContexts.delete(contextName);\n deactivatedContexts.push(contextName);\n }\n }\n\n return deactivatedContexts;\n }\n\n /**\n * Determines whether an input event represents intentional activity that should\n * update the last-active device type. Filters out noise like idle mousemove.\n *\n * @param state\n */\n private _shouldUpdateActiveDevice(state : InputState) : boolean\n {\n if(isMouseState(state))\n {\n // Only clicks and wheel count — mousemove alone is noise\n return Object.values(state.buttons).some((btn) => btn.pressed) || state.wheel !== undefined;\n }\n\n if(isGamepadState(state))\n {\n // Any button pressed or any axis past the activity deadzone\n return Object.values(state.buttons).some((btn) => btn.pressed)\n || Object.values(state.axes).some((val) => Math.abs(val) > GAMEPAD_ACTIVITY_DEADZONE);\n }\n\n // Keyboard events are always intentional\n return true;\n }\n\n /**\n * Create a binding from a binding definition.\n *\n * @param definition\n * @returns Null if the action is not registered or the binding type is unsupported\n */\n private _createBindingFromDefinition(definition : BindingDefinition) : Binding | null\n {\n // Get the full action object from the action name\n const action = this._actions.get(definition.action);\n\n // If the action doesn't exist, log a warning and return null\n if(!action)\n {\n this._log.warn(`Cannot create binding: Action \"${ definition.action }\" not found.`);\n return null;\n }\n\n // Extract deviceID and create reader from the input definition\n const { deviceID, ...readerDef } = definition.input;\n\n switch (definition.type)\n {\n case 'trigger': {\n // Create the binding with the device value reader based on definition's source\n return new TriggerBinding(\n action,\n deviceID,\n definition.input.type,\n this._createInputSourceFromDefinition(readerDef),\n {\n ...(definition.options || {}),\n context: definition.context,\n }\n );\n }\n\n case 'toggle': {\n // Create the binding with the device value reader based on definition's source\n return new ToggleBinding(\n action,\n deviceID,\n definition.input.type,\n this._createInputSourceFromDefinition(readerDef),\n {\n ...(definition.options || {}),\n context: definition.context,\n }\n );\n }\n\n case 'value': {\n // Create the binding with the device value reader based on definition's source\n return new ValueBinding(\n action,\n deviceID,\n definition.input.type,\n this._createInputSourceFromDefinition(readerDef),\n {\n ...(definition.options || {}),\n context: definition.context,\n }\n );\n }\n\n default:\n this._log.error(`Binding type not implemented: ${ (definition as BindingDefinition).type }`);\n return null;\n }\n }\n\n /**\n * Create a device value reader from a definition object.\n *\n * @param definition\n * @throws Error if the reader type is not supported\n */\n private _createInputSourceFromDefinition(definition : DeviceValueReaderDefinition) : DeviceValueReader\n {\n // Create reader based on device type\n switch (definition.type)\n {\n case 'keyboard':\n return new KeyboardValueReader(\n definition.sourceKey,\n definition.options\n );\n\n case 'mouse':\n {\n // Check if sourceType is valid\n const sourceType = definition.sourceType;\n if(!(sourceType === 'button' || sourceType === 'position' || sourceType === 'wheel'))\n {\n throw new Error(`Invalid mouse source type: ${ sourceType }`);\n }\n\n return new MouseValueReader(\n sourceType,\n definition.sourceKey\n );\n }\n\n case 'gamepad':\n {\n // Check if sourceType is valid\n const sourceType = definition.sourceType;\n if(!(sourceType === 'button' || sourceType === 'axis'))\n {\n throw new Error(`Invalid gamepad source type: ${ sourceType }`);\n }\n return new GamepadValueReader(\n sourceType,\n definition.sourceKey,\n definition.options\n );\n }\n\n default:\n throw new Error(`Unsupported input source type: ${ (definition as DeviceValueReaderDefinition).type }`);\n }\n }\n\n /**\n * Evaluates an input event during capture mode. If the event matches the capture criteria,\n * resolves the capture promise and cleans up. Otherwise does nothing and waits for the next event.\n *\n * @param device\n * @param state\n */\n private _processCaptureInput(device : InputDevice, state : InputState) : void\n {\n const capture = this._captureState;\n if(!capture)\n {\n return;\n }\n\n const { options } = capture;\n\n // Filter by device type\n if(!options.deviceTypes.includes(device.type))\n {\n return;\n }\n\n let result : InputDefinition | null = null;\n\n if(isKeyboardState(state))\n {\n // Only capture key-down, ignore key repeats\n if(state.event?.repeat === true)\n {\n return;\n }\n\n // Find the first key that transitioned to pressed\n for(const key of Object.keys(state.delta))\n {\n if(state.delta[key] === true)\n {\n result = { type: 'keyboard', sourceType: 'key', sourceKey: key, deviceID: device.id };\n break;\n }\n }\n }\n else if(isMouseState(state))\n {\n // Only capture button presses, ignore position and wheel\n for(const key of Object.keys(state.buttons))\n {\n if(state.buttons[key].pressed)\n {\n result = { type: 'mouse', sourceType: 'button', sourceKey: key, deviceID: device.id };\n break;\n }\n }\n }\n else if(isGamepadState(state))\n {\n // Check buttons first\n for(const key of Object.keys(state.buttons))\n {\n if(state.buttons[key].pressed)\n {\n result = { type: 'gamepad', sourceType: 'button', sourceKey: key, deviceID: device.id };\n break;\n }\n }\n\n // Then check axes if no button found\n if(!result)\n {\n for(const key of Object.keys(state.axes))\n {\n if(Math.abs(state.axes[key]) > CAPTURE_AXIS_DEADZONE)\n {\n result = { type: 'gamepad', sourceType: 'axis', sourceKey: key, deviceID: device.id };\n break;\n }\n }\n }\n }\n\n // No intentional input detected\n if(!result)\n {\n return;\n }\n\n // Apply sourceTypes filter — if it doesn't match, keep waiting\n if(options.sourceTypes\n && !options.sourceTypes.includes(result.sourceType as typeof options.sourceTypes[number])\n )\n {\n return;\n }\n\n // We have a match — resolve and clean up\n const { resolve } = capture;\n this._cleanupCapture();\n resolve(result);\n }\n\n /**\n * Tears down capture state, deactivates the temporary context, and restores the\n * snapshotted contexts. Does not resolve or reject — the caller handles that.\n */\n private _cleanupCapture() : void\n {\n const capture = this._captureState;\n if(!capture)\n {\n return;\n }\n\n // Clean up abort listener\n if(capture.abortCleanup)\n {\n capture.abortCleanup();\n }\n\n const { snapshot } = capture;\n this._captureState = null;\n\n // Remove the temporary context entirely and restore previous state\n this.deactivateContext('__sage_capture__');\n this._contexts.delete('__sage_capture__');\n\n for(const contextName of snapshot)\n {\n this.activateContext(contextName);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Internal API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Handle input from a device and process all relevant bindings.\n *\n * **Internal API**: This method is prefixed with `$` to indicate it is intended for internal\n * engine use only. While it is public for testing purposes, external code should not call\n * this method directly. Input handling is triggered automatically via the event bus.\n *\n * @param device - The input device\n * @param state - Current input state\n *\n * @internal\n */\n public $handleInput(device : InputDevice, state : InputState) : void\n {\n // Snapshot input state so context activations can prime edge state from physical reality\n this._lastInputState.set(device.id, state);\n\n // Track last-active device type before processing bindings.\n // This runs even if there are no bindings for this device.\n if(this._shouldUpdateActiveDevice(state) && device.type !== this._lastActiveDeviceType)\n {\n const previousDeviceType = this._lastActiveDeviceType;\n this._lastActiveDeviceType = device.type;\n this._eventBus.publish({\n type: 'input:device-type:changed',\n payload: { deviceType: device.type, previousDeviceType },\n });\n }\n\n // If capturing, route to the capture handler and suppress all normal binding processing\n if(this._captureState)\n {\n this._processCaptureInput(device, state);\n return;\n }\n\n const bindings = this._bindings.get(device.id);\n if(!bindings || bindings.length === 0)\n {\n return;\n }\n\n if(this._activeContexts.size === 0 && bindings.some((binding) => binding.context))\n {\n return;\n }\n\n for(const binding of bindings)\n {\n if(!this._isBindingContextActive(binding))\n {\n continue;\n }\n\n binding.process(state, this._eventBus);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Action Management API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Registers an action.\n *\n * @param action\n * @throws Error if action is already registered\n */\n public registerAction(action : Action) : void\n {\n this._log.debug(`Registering action \"${ action.name }\"`);\n\n if(this._actions.has(action.name))\n {\n const errorMsg = `Action \"${ action.name }\" already registered.`;\n this._log.error(errorMsg);\n throw new Error(errorMsg);\n }\n\n this._actions.set(action.name, action);\n this._log.debug(`Action \"${ action.name }\" registered successfully`);\n }\n\n /**\n * Gets an action by name.\n *\n * @param actionName\n */\n public getAction(actionName : string) : Action | null\n {\n this._log.trace(`Getting action \"${ actionName }\"`);\n\n const action = this._actions.get(actionName) ?? null;\n if(!action)\n {\n this._log.debug(`Action \"${ actionName }\" not found`);\n }\n\n return action;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Device Tracking API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Returns the device type that most recently produced intentional input,\n * or `null` if no input has been received yet.\n */\n public get lastActiveDeviceType() : DeviceType | null\n {\n return this._lastActiveDeviceType;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Input Capture API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Waits for the next intentional input event and returns a descriptor suitable for constructing\n * a binding. While capturing, all normal bindings are suppressed via a temporary exclusive context.\n * Previous context state is restored when the capture completes or is cancelled.\n *\n * @param options\n * @throws Error if a capture is already in progress\n */\n public captureInput(options : CaptureInputOptions) : Promise<InputDefinition>\n {\n if(this._captureState)\n {\n throw new Error('captureInput() already in progress');\n }\n\n // Snapshot current active contexts before suppression\n const snapshot = [ ...this._activeContexts ];\n\n // Push a temporary exclusive context to suppress all normal bindings\n this.registerContext('__sage_capture__', true);\n this.activateContext('__sage_capture__');\n\n return new Promise<InputDefinition>((resolve, reject) =>\n {\n const captureState : CaptureState = { resolve, reject, options, snapshot };\n this._captureState = captureState;\n\n // Wire up abort signal if provided\n if(options.signal)\n {\n if(options.signal.aborted)\n {\n this._cleanupCapture();\n reject(options.signal.reason ?? new Error('captureInput() aborted'));\n return;\n }\n\n const onAbort = () : void =>\n {\n const reason = options.signal?.reason ?? new Error('captureInput() aborted');\n this._cleanupCapture();\n reject(reason);\n };\n\n options.signal.addEventListener('abort', onAbort, { once: true });\n\n captureState.abortCleanup = () =>\n {\n options.signal?.removeEventListener('abort', onAbort);\n };\n }\n });\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Context Management API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Registers a context with specific options.\n * Updates exclusivity if the context already exists.\n *\n * @param contextName\n * @param exclusive\n */\n public registerContext(contextName : string, exclusive = true) : Context\n {\n const context = this._getOrCreateContext(contextName, exclusive);\n\n // Update context's exclusivity if it already exists\n if(context.exclusive !== exclusive)\n {\n context.exclusive = exclusive;\n this._log.info(`Updated context \"${ contextName }\" exclusivity: ${ exclusive }`);\n }\n\n return context;\n }\n\n /**\n * Activates a context, enabling all bindings associated with it.\n * If the context is exclusive, all other exclusive contexts will be deactivated.\n *\n * @param contextName\n */\n public activateContext(contextName : string) : void\n {\n // Get or create context\n const context = this._getOrCreateContext(contextName);\n\n // Get the context's exclusivity setting\n const isExclusive = context.exclusive;\n\n this._log.debug(`Activating context \"${ contextName }\" (exclusive: ${ isExclusive })`);\n\n // If already active, nothing to do unless we're making an exclusive activation\n const isAlreadyActive = this._activeContexts.has(contextName);\n\n if(isExclusive)\n {\n // Deactivate all other exclusive contexts\n const deactivated = this._deactivateExclusiveContexts(contextName);\n\n if(deactivated.length > 0)\n {\n this._log.info(`Deactivated exclusive contexts: ${ deactivated.join(', ') }`);\n }\n }\n\n // Add to active contexts if not already there\n if(!isAlreadyActive)\n {\n this._activeContexts.add(contextName);\n\n // Prime edge state on all bindings in this context from the last known physical input.\n // This prevents already-held inputs from producing phantom edges in the new context while\n // still allowing genuinely new inputs to fire.\n for(const [ deviceId, deviceBindings ] of this._bindings.entries())\n {\n const lastState = this._lastInputState.get(deviceId);\n for(const binding of deviceBindings)\n {\n if(binding.context === contextName)\n {\n binding.resetEdgeState(lastState);\n }\n }\n }\n\n this._log.info(`Context \"${ contextName }\" activated${ isExclusive ? ' as exclusive' : '' }`);\n }\n }\n\n /**\n * Deactivates a context, disabling all bindings associated with it.\n *\n * @param contextName\n */\n public deactivateContext(contextName : string) : void\n {\n this._log.debug(`Deactivating context \"${ contextName }\"`);\n\n if(this._activeContexts.has(contextName))\n {\n this._activeContexts.delete(contextName);\n this._log.info(`Context \"${ contextName }\" deactivated`);\n }\n else\n {\n this._log.debug(`Context \"${ contextName }\" was not active`);\n }\n }\n\n /**\n * Returns a list of all active contexts.\n */\n public getActiveContexts() : string[]\n {\n return [ ...this._activeContexts ];\n }\n\n /**\n * Returns whether a context is active.\n *\n * @param contextName\n */\n public isContextActive(contextName : string) : boolean\n {\n return this._activeContexts.has(contextName);\n }\n\n /**\n * Gets a context by name.\n *\n * @param contextName\n */\n public getContext(contextName : string) : Context | null\n {\n return this._contexts.get(contextName) || null;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Binding Management API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a binding for an input device.\n * Auto-registers the binding's context if not already known.\n *\n * @param binding\n * @throws Error if binding type is invalid\n */\n public $registerBinding(binding : Binding) : void\n {\n if(!bindingTypes.includes(binding.type))\n {\n throw new Error(`Invalid binding type: ${ binding.type }`);\n }\n\n // If binding has a context but context isn't registered yet, register it with default settings\n if(binding.context && !this._contexts.has(binding.context))\n {\n this.registerContext(binding.context);\n }\n\n // Initialize device bindings array if it doesn't exist\n if(!this._bindings.has(binding.deviceID))\n {\n this._bindings.set(binding.deviceID, []);\n }\n\n // Add the binding\n this._bindings.get(binding.deviceID)?.push(binding);\n this._log.debug(`Registered ${ binding.type } binding for \"${ binding.action.name }\" in context `\n + `\"${ binding.context || null }\"`);\n }\n\n /**\n * Register a binding using a binding definition object.\n * Creates the appropriate Binding subclass and registers it.\n *\n * @param definition\n */\n public registerBinding(definition : BindingDefinition) : void\n {\n const binding = this._createBindingFromDefinition(definition);\n\n if(binding)\n {\n this.$registerBinding(binding);\n }\n else\n {\n this._log.error(`Failed to create binding for action \"${ definition.action }\" with type `\n + `\"${ definition.type }\"`);\n }\n }\n\n /**\n * Unregister all bindings for an action within a context.\n *\n * @param actionName\n * @param context - Pass null (default) to target context-free bindings\n */\n public unregisterBindings(actionName : string, context : string | null = null) : void\n {\n this._log.debug(`Unregistering all bindings for action \"${ actionName }\" in context \"${ context }\"`);\n\n let bindingsRemoved = 0;\n\n // Iterate through all devices and their bindings\n for(const [ deviceId, deviceBindings ] of this._bindings.entries())\n {\n const newBindings = deviceBindings.filter((binding) =>\n {\n const bindingContext = binding.context || null;\n const shouldKeep = binding.action.name !== actionName || bindingContext !== context;\n if(!shouldKeep)\n {\n bindingsRemoved++;\n }\n return shouldKeep;\n });\n\n // Only update the map if we actually removed something\n if(newBindings.length !== deviceBindings.length)\n {\n this._bindings.set(deviceId, newBindings);\n }\n }\n\n this._log.info(`Removed ${ bindingsRemoved } bindings for action \"${ actionName }\" in context \"${ context }\"`);\n }\n\n /**\n * Gets all bindings for a specific action, optionally filtered by device type and/or context.\n *\n * @param actionName\n * @param deviceType - If provided, filters to only bindings for that device type\n * @param context - If provided, filters to only bindings in that context\n */\n public getBindingsForAction(actionName : string, deviceType ?: DeviceType, context ?: string | null) : Binding[]\n {\n const result : Binding[] = [];\n\n for(const deviceBindings of this._bindings.values())\n {\n for(const binding of deviceBindings)\n {\n if(binding.action.name !== actionName) { continue; }\n if(deviceType !== undefined && binding.deviceType !== deviceType) { continue; }\n\n const bindingContext = binding.context || null;\n if(context !== undefined && bindingContext !== context) { continue; }\n\n result.push(binding);\n }\n }\n\n return result;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Configuration Management API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Exports the current configuration to a serializable object.\n * Includes all actions, bindings, contexts, and their active states.\n */\n public exportConfiguration() : BindingConfiguration\n {\n this._log.debug('Exporting binding configuration');\n\n // Export all registered actions\n const actions = [ ...this._actions.values() ].map((action) =>\n {\n // Ensure we include all analog action properties when exporting\n if(action.type === 'analog')\n {\n return {\n name: action.name,\n type: action.type,\n label: action.label,\n minValue: action.minValue ?? 0,\n maxValue: action.maxValue ?? 1,\n };\n }\n return action;\n });\n\n // Export all registered bindings using their toJSON methods\n const bindings : BindingDefinition[] = [];\n\n // Iterate through all device bindings\n for(const deviceBindings of this._bindings.values())\n {\n for(const binding of deviceBindings)\n {\n // Let the binding serialize itself to the BindingDefinition format\n bindings.push(binding.toJSON());\n }\n }\n\n // Export all contexts with their active state\n const contexts = [ ...this._contexts.values() ].map((context) => ({\n name: context.name,\n exclusive: context.exclusive,\n active: this._activeContexts.has(context.name),\n }));\n\n this._log.info(`Configuration exported: ${ actions.length } actions, ${ bindings.length } bindings, `\n + `${ contexts.length } contexts`);\n\n return {\n actions,\n bindings,\n contexts,\n };\n }\n\n /**\n * Imports a configuration, replacing all current actions, bindings, and contexts.\n *\n * @param config\n */\n public importConfiguration(config : BindingConfiguration) : void\n {\n this._log.debug('Importing binding configuration');\n\n // Clear existing configuration\n this._bindings.clear();\n this._actions.clear();\n this._contexts.clear();\n this._activeContexts.clear();\n\n // Import actions\n for(const action of config.actions)\n {\n this.registerAction(action);\n }\n\n // Import contexts first so they exist before bindings reference them\n for(const contextData of config.contexts)\n {\n this.registerContext(contextData.name, contextData.exclusive);\n\n // Activate contexts that were active\n if(contextData.active)\n {\n this.activateContext(contextData.name);\n }\n }\n\n // Import bindings\n for(const bindingDef of config.bindings)\n {\n try\n {\n this.registerBinding(bindingDef);\n }\n catch (err : unknown)\n {\n if(err instanceof Error)\n {\n this._log.error(`Failed to import binding for action \"${ bindingDef.action }\": ${ err.message }`);\n }\n }\n }\n\n this._log.info(`Configuration imported: ${ config.actions.length } actions, `\n + `${ config.bindings.length } bindings, ${ config.contexts.length } contexts`);\n }\n\n /**\n * Tears down the binding manager and cleans up any resources\n */\n async $teardown() : Promise<void>\n {\n this._log.info('Tearing down BindingManager');\n\n // Abort any active capture before clearing state\n if(this._captureState)\n {\n const { reject } = this._captureState;\n this._cleanupCapture();\n reject(new Error('BindingManager torn down during captureInput()'));\n }\n\n // Unsubscribe from input events\n if(this._inputUnsubscribe)\n {\n this._inputUnsubscribe();\n this._inputUnsubscribe = null;\n }\n\n // Clear all bindings and cached input state\n this._bindings.clear();\n this._lastInputState.clear();\n\n // Clear all actions\n this._actions.clear();\n\n // Clear all contexts\n this._contexts.clear();\n this._activeContexts.clear();\n\n // Reset device tracking\n this._lastActiveDeviceType = null;\n\n this._log.info('BindingManager torn down successfully');\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Utilities to test for capability of the current environment\n//----------------------------------------------------------------------------------------------------------------------\n\nexport function isBrowser() : boolean\n{\n return typeof window !== 'undefined' && typeof window.document !== 'undefined';\n}\n\nexport function hasWebGPU() : boolean\n{\n return isBrowser()\n && !!window.navigator.gpu;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Game Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { AbstractEngine, Scene } from '@babylonjs/core';\n\n// Interfaces\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\n\n// Classes\nimport type { GameEventBus } from '../classes/eventBus.ts';\n\n// Managers\nimport { GameEntityManager } from './entity.ts';\nimport { LevelManager } from './level.ts';\nimport { UserInputManager } from './input.ts';\n\n// Utils\nimport { isBrowser } from '../utils/capabilities.ts';\nimport { LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Callback function for frame updates.\n * Called once per frame with the delta time in seconds.\n */\nexport type FrameCallback = (deltaTime : number) => void;\n\n/**\n * Internal representation of a registered frame callback.\n */\ninterface RegisteredFrameCallback\n{\n id : number;\n callback : FrameCallback;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class GameManager implements Disposable\n{\n private _engine : AbstractEngine;\n private _eventBus : GameEventBus;\n private _entityManager : GameEntityManager;\n private _inputManager : UserInputManager;\n private _levelManager : LevelManager;\n private _log : LoggerInterface;\n private _boundRenderLoop : () => void;\n private _boundResizeHandler : () => void;\n\n /** Registered frame callbacks, executed in registration order */\n private _frameCallbacks : RegisteredFrameCallback[] = [];\n\n /** Counter for generating unique callback IDs */\n private _nextCallbackId = 0;\n\n /** Whether the game is currently paused */\n private _paused = false;\n\n public started = false;\n\n //------------------------------------------------------------------------------------------------------------------\n\n constructor(\n engine : AbstractEngine,\n eventBus : GameEventBus,\n entityManager : GameEntityManager,\n inputManager : UserInputManager,\n levelManager : LevelManager,\n logger ?: LoggingUtility\n )\n {\n this._engine = engine;\n this._eventBus = eventBus;\n this._entityManager = entityManager;\n this._inputManager = inputManager;\n this._levelManager = levelManager;\n this._log = logger?.getLogger('GameManager') || new SAGELogger('GameManager');\n\n // Store bound function references for later cleanup\n this._boundRenderLoop = this._renderLoop.bind(this);\n this._boundResizeHandler = this._resizeHandler.bind(this);\n\n if(isBrowser())\n {\n // Resize the engine on window resize\n window.addEventListener('resize', this._boundResizeHandler);\n }\n }\n\n get currentScene() : Scene | null\n {\n return this._levelManager.currentLevel?.scene ?? null;\n }\n\n /**\n * Whether the game is currently paused.\n * When paused, entity updates and frame callbacks are skipped, but rendering continues.\n */\n get isPaused() : boolean\n {\n return this._paused;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a callback to be called once per frame.\n *\n * **Advanced Usage**: This is intended for systems that need frame-by-frame updates\n * outside of the entity/behavior system. For game logic, prefer using entity behaviors\n * with their `update()` method instead.\n *\n * Callbacks are executed in registration order, after entity updates but before rendering.\n * Each callback receives the delta time in seconds (typically ~0.016 for 60fps).\n *\n * @param callback\n * @returns A function to unregister the callback\n *\n * @example\n * ```typescript\n * const unsubscribe = gameManager.registerFrameCallback((deltaTime) => {\n * // deltaTime is in seconds (e.g., 0.016 for 60fps)\n * console.log(`Frame delta: ${deltaTime}s`);\n * });\n *\n * // Later, to stop receiving callbacks:\n * unsubscribe();\n * ```\n */\n public registerFrameCallback(callback : FrameCallback) : () => void\n {\n const id = this._nextCallbackId++;\n this._frameCallbacks.push({ id, callback });\n\n this._log.debug(`Registered frame callback (id: ${ id })`);\n\n // Return unsubscribe function\n return () =>\n {\n const index = this._frameCallbacks.findIndex((cb) => cb.id === id);\n if(index !== -1)\n {\n this._frameCallbacks.splice(index, 1);\n this._log.debug(`Unregistered frame callback (id: ${ id })`);\n }\n };\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n private _renderLoop() : void\n {\n const deltaTimeMs = this._engine.getDeltaTime();\n const deltaTime = deltaTimeMs / 1000; // Convert milliseconds to seconds\n\n // Always poll input (needed to unpause)\n this._inputManager.pollGamepads();\n\n // Skip game logic updates when paused\n if(!this._paused)\n {\n // Update entities and entity manager\n this._entityManager.$frameUpdate(deltaTime);\n\n // Run registered frame callbacks\n for(const { callback } of this._frameCallbacks)\n {\n try\n {\n callback(deltaTime);\n }\n catch (err)\n {\n this._log.error('Error in frame callback:', err);\n }\n }\n }\n\n // Always render (shows frozen frame when paused)\n if(this.currentScene)\n {\n this.currentScene.render();\n }\n }\n\n private _resizeHandler() : void\n {\n if(this.started)\n {\n this._engine.resize();\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n async start() : Promise<void>\n {\n // Start the render loop\n this._engine.runRenderLoop(this._boundRenderLoop);\n\n this.started = true;\n\n this._log.info('SkewedAspect Game Engine started successfully');\n }\n\n async stop() : Promise<void>\n {\n this.started = false;\n this._engine.stopRenderLoop();\n\n this._log.info('SkewedAspect Game Engine stopped successfully');\n }\n\n /**\n * Pause the game. Entity updates and frame callbacks will be skipped,\n * but rendering continues (frozen frame). Input is still polled so\n * the game can be unpaused.\n *\n * Emits a 'game:paused' event.\n *\n * @param reason - Optional reason for pausing (included in event payload)\n */\n pause(reason ?: string) : void\n {\n if(this._paused)\n {\n this._log.debug('Game is already paused');\n return;\n }\n\n this._paused = true;\n this._log.info('Game paused', reason ? `(reason: ${ reason })` : '');\n\n this._eventBus.publish({\n type: 'game:paused',\n payload: { reason },\n });\n }\n\n /**\n * Resume the game after being paused. Entity updates and frame callbacks\n * will resume.\n *\n * Emits a 'game:resumed' event.\n *\n * @param reason - Optional reason for resuming (included in event payload)\n */\n resume(reason ?: string) : void\n {\n if(!this._paused)\n {\n this._log.debug('Game is not paused');\n return;\n }\n\n this._paused = false;\n this._log.info('Game resumed', reason ? `(reason: ${ reason })` : '');\n\n this._eventBus.publish({\n type: 'game:resumed',\n payload: { reason },\n });\n }\n\n /**\n * Tears down any resources held by the GameManager\n * This handles unregistering event listeners and any other cleanup\n */\n async $teardown() : Promise<void>\n {\n this._log.info('Tearing down GameManager');\n\n // If we're still running, stop first\n if(this.started)\n {\n await this.stop();\n }\n\n // Remove event listeners\n if(isBrowser())\n {\n window.removeEventListener('resize', this._boundResizeHandler);\n }\n\n // Clear frame callbacks\n this._frameCallbacks = [];\n\n this._log.info('GameManager torn down successfully');\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","for(var r=256,n=[];r--;)n[r]=(r+256).toString(16).substring(1);export function hexoid(r){r=r||16;var t=\"\",o=0;return function(){if(!t||256===o){for(t=\"\",o=(1+r)/2|0;o--;)t+=n[256*Math.random()|0];t=t.substring(o=0,r-2)}return t+n[o++]}}","//----------------------------------------------------------------------------------------------------------------------\n// ID Generation Utility\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { hexoid } from 'hexoid';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Fast, unique ID generator for entities.\n * Uses hexoid for high-performance ID generation (~200M IDs/sec).\n * Generates 16-character hexadecimal strings.\n *\n * @example\n * ```typescript\n * const id = generateId(); // \"8cd88e1a9b13aac0\"\n * ```\n */\nexport const generateId = hexoid(16);\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Game Entity\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { TransformNode } from '@babylonjs/core';\n\nimport type { GameEntityBehaviorConstructor } from '../interfaces/entity.ts';\nimport type { Destroyable } from '../interfaces/lifecycle.ts';\nimport { type GameEvent, GameEventBus } from './eventBus.ts';\nimport type { GameEngine } from './gameEngine.ts';\nimport { generateId } from '../utils/id.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Utility type that removes readonly from all properties.\n * Used internally to allow controlled mutation of readonly properties.\n */\ntype Mutable<T> = { -readonly [P in keyof T] : T[P] };\n\n/**\n * Result of a request sent to an entity's behaviors.\n * `success: true` means a behavior handled the request and returned a value.\n * `success: false` means no behavior handled it, or a behavior threw an error.\n */\nexport type RequestResult<T> = { success : true; value : T } | { success : false; error : string };\n\n/**\n * Narrow interface for entity-to-manager messaging delegation.\n * Avoids a circular import between entity.ts and managers/entity.ts.\n */\ninterface EntityMessenger\n{\n send(target : string, type : string, payload ?: unknown, senderID ?: string) : Promise<void>;\n request<T>(target : string, type : string, payload ?: unknown, senderID ?: string) : Promise<RequestResult<T>>;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Tracks a reference-counted event subscription so multiple behaviors sharing the same event type\n * only produce a single underlying subscription.\n */\ninterface GameEventSubscription\n{\n count : number;\n unsubscribe : () => void;\n}\n\n/**\n * Options for attaching a behavior at a specific position.\n * Only one positioning option should be specified.\n */\nexport interface AttachBehaviorOptions\n{\n /** Insert before behavior of this class */\n before ?: GameEntityBehaviorConstructor;\n /** Insert after behavior of this class */\n after ?: GameEntityBehaviorConstructor;\n /** Insert at specific index (0-based, clamped to valid range) */\n at ?: number;\n}\n\n/**\n * Minimal entity surface exposed to behaviors, preventing them from accessing internals like\n * subscription management or behavior ordering.\n */\ninterface SimpleGameEntity<EntityState extends object = object>\n{\n id : string;\n type : string;\n name ?: string;\n node ?: TransformNode;\n state : EntityState;\n eventBus : GameEventBus;\n tags : ReadonlySet<string>;\n\n send(target : string, type : string, payload ?: unknown) : Promise<void>;\n request<T>(target : string, type : string, payload ?: unknown) : Promise<RequestResult<T>>;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Base class for game entity behaviors. Subclass this to define how an entity reacts to events\n * and updates each frame. Multiple behaviors can be attached to a single entity; event processing\n * order matches the attachment order.\n */\nexport abstract class GameEntityBehavior<RequiredState extends object = object>\n{\n abstract name : string;\n abstract eventSubscriptions : string[];\n\n protected entity : SimpleGameEntity<RequiredState> | null = null;\n\n //------------------------------------------------------------------------------------------------------------------\n // Internal API\n //------------------------------------------------------------------------------------------------------------------\n\n $emit(event : GameEvent) : void\n {\n if(!this.entity)\n {\n throw new Error('Entity is not set for this behavior.');\n }\n\n event.senderID = this.entity.id;\n // Cast to allow publishing events with arbitrary payloads\n (this.entity.eventBus as unknown as GameEventBus<Record<string, unknown>>).publish(event);\n }\n\n /**\n * Emits an entity:state-changed event to notify UI or other observers of state changes.\n * Call this after making significant state changes that external systems should know about.\n *\n * @param state - Optional state to include in the event. If omitted, uses current entity state.\n * @param changes - Optional record of specific changes made (alternative to full state).\n */\n $emitStateChanged(state ?: object, changes ?: Record<string, unknown>) : void\n {\n if(!this.entity)\n {\n throw new Error('Entity is not set for this behavior.');\n }\n\n this.$emit({\n type: 'entity:state-changed',\n payload: {\n entityId: this.entity.id,\n entityType: this.entity.type,\n state: state ?? this.entity.state,\n changes,\n },\n });\n }\n\n /**\n * Sets the entity for this behavior.\n * @param entity\n */\n $setEntity(entity : SimpleGameEntity<RequiredState> | null) : void\n {\n this.entity = entity;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Implementation API\n //------------------------------------------------------------------------------------------------------------------\n\n abstract processEvent(event : GameEvent, state : RequiredState) : Promise<boolean> | boolean;\n\n /**\n * Handles a direct request sent to this entity. Return a value to respond,\n * or `undefined` to pass the request to the next behavior.\n *\n * @param event - The request event (type, payload, senderID, targetID)\n * @param state - Current entity state\n */\n processRequest?(event : GameEvent, state : RequiredState) : Promise<unknown> | unknown | undefined;\n\n update?(dt : number, state : RequiredState) : void;\n\n destroy?() : Promise<void>;\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle Hooks\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Called when the entity is attached to a scene node.\n * Override to perform initialization that requires the node (physics, spatial audio, etc.)\n *\n * The scene is accessible via `node.getScene()`.\n *\n * @param node - The TransformNode (typically a Mesh) the entity is attached to.\n * @param gameEngine - The game engine instance for accessing managers and engines.\n */\n onNodeAttached?(node : TransformNode, gameEngine : GameEngine) : void;\n\n /**\n * Called when the entity is detached from its scene node.\n * Override to clean up node-dependent resources.\n */\n onNodeDetached?() : void;\n\n /**\n * Called when the entity is recycled from an object pool.\n * Override to reset behavior-specific state that isn't covered by entity state reset.\n * Distinct from onNodeDetached — this signals a full lifecycle reset.\n *\n * @param state - The freshly reset entity state.\n */\n onReset?(state : RequiredState) : void;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Base class for game entities. Owns a set of behaviors, manages event subscriptions\n * (reference-counted per event type), and delegates lifecycle calls to its behaviors.\n */\nexport class GameEntity<EntityState extends object = object> implements SimpleGameEntity<EntityState>, Destroyable\n{\n /** The unique identifier of the entity (16-char hex string). */\n public readonly id : string;\n\n public readonly type : string;\n\n /** Optional human-readable name for the entity. */\n public readonly name ?: string;\n\n /**\n * The scene node this entity is attached to (if any).\n * When attached, the entity controls this node's behavior.\n * Use EntityManager.attachToNode() / detachFromNode() to modify.\n */\n public readonly node ?: TransformNode;\n\n /** Child entities in the hierarchy. */\n public readonly children : GameEntity[] = [];\n\n /** Parent entity in the hierarchy, or null if this is a root entity. */\n public readonly parent : GameEntity | null = null;\n\n public state : EntityState;\n\n /** Array of behaviors attached to the entity. Order determines event processing priority. */\n public behaviors : GameEntityBehavior[] = [];\n\n public eventBus : GameEventBus;\n\n private _gameEngine : GameEngine | null = null;\n private _entityManager : EntityMessenger | null = null;\n private _tags : Set<string> = new Set<string>();\n private subscriptions : Map<string, GameEventSubscription> = new Map<string, GameEventSubscription>();\n\n /**\n * Creates an instance of GameEntity.\n * @param type\n * @param eventBus\n * @param initialState\n * @param behaviors - Attached in order; order determines event processing priority.\n * @param name\n */\n constructor(\n type : string,\n eventBus : GameEventBus,\n initialState : EntityState,\n behaviors : GameEntityBehaviorConstructor[],\n name ?: string\n )\n {\n this.id = generateId();\n this.type = type;\n this.name = name;\n this.state = initialState;\n this.eventBus = eventBus;\n\n // Attach behaviors\n for(const behaviorCtor of behaviors)\n {\n this.attachBehavior(new behaviorCtor());\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Tags\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Read-only access to the entity's tags.\n */\n get tags() : ReadonlySet<string>\n {\n return this._tags;\n }\n\n /**\n * Checks if the entity has a specific tag.\n * @param tag\n */\n hasTag(tag : string) : boolean\n {\n return this._tags.has(tag);\n }\n\n /**\n * Internal method for EntityManager to add a tag.\n * @param tag\n */\n $addTag(tag : string) : void\n {\n this._tags.add(tag);\n }\n\n /**\n * Internal method for EntityManager to remove a tag.\n * @param tag\n */\n $removeTag(tag : string) : boolean\n {\n return this._tags.delete(tag);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Node Attachment\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Find a direct child node by name.\n * @param name\n */\n getChildNode(name : string) : TransformNode | undefined\n {\n if(!this.node)\n {\n return undefined;\n }\n\n return this.node.getChildren(\n (child) => child.name === name,\n false\n )[0] as TransformNode | undefined;\n }\n\n /**\n * Find a descendant node by path (e.g., \"armature/hand/finger\").\n * @param path - Slash-separated path of node names.\n */\n findNode(path : string) : TransformNode | undefined\n {\n if(!this.node)\n {\n return undefined;\n }\n\n let current : TransformNode | undefined = this.node;\n\n for(const part of path.split('/'))\n {\n current = current?.getChildren(\n (child) => child.name === part,\n false\n )[0] as TransformNode | undefined;\n }\n\n return current;\n }\n\n /**\n * Internal method for EntityManager to attach entity to a node.\n * Do not call directly - use EntityManager.attachToNode() instead.\n * @param node\n * @param gameEngine\n * @internal\n */\n $attachToNode(node : TransformNode, gameEngine : GameEngine) : void\n {\n (this as Mutable<GameEntity>).node = node;\n this._gameEngine = gameEngine;\n\n // Notify behaviors of node attachment\n for(const behavior of this.behaviors)\n {\n behavior.onNodeAttached?.(node, gameEngine);\n }\n\n // Parent any children's nodes under this node\n for(const child of this.children)\n {\n if(child.node)\n {\n (child.node as { parent : unknown }).parent = node;\n }\n }\n }\n\n /**\n * Internal method for EntityManager to detach entity from its node.\n * Do not call directly - use EntityManager.detachFromNode() instead.\n * @internal\n */\n $detachFromNode() : void\n {\n // Remove from all outline layers before detaching\n this.unhighlight();\n\n // Detach children's nodes before detaching self\n for(const child of this.children)\n {\n if(child.node)\n {\n (child.node as { parent : unknown }).parent = null;\n }\n }\n\n // Notify behaviors before detaching\n for(const behavior of this.behaviors)\n {\n behavior.onNodeDetached?.();\n }\n\n (this as Mutable<GameEntity>).node = undefined;\n this._gameEngine = null;\n }\n\n /**\n * Add this entity to a named outline layer for visual highlighting.\n * Requires the entity to be attached to a node in a level with outlines configured.\n *\n * @param layerName - The name of the outline layer (must exist on the current level)\n */\n highlight(layerName : string) : void\n {\n const outlines = this._gameEngine?.managers?.levelManager?.currentLevel?.outlines;\n if(outlines)\n {\n outlines.highlightEntity(layerName, this);\n }\n }\n\n /**\n * Remove this entity from an outline layer, or from all layers if no name given.\n *\n * @param layerName - Specific layer to remove from, or omit to remove from all\n */\n unhighlight(layerName ?: string) : void\n {\n const outlines = this._gameEngine?.managers?.levelManager?.currentLevel?.outlines;\n if(outlines)\n {\n if(layerName)\n {\n outlines.unhighlightEntity(layerName, this);\n }\n else\n {\n outlines.unhighlightEntityAll(this);\n }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Child Hierarchy\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Adds a child entity to this entity's hierarchy.\n * Sets the child's parent to this entity. If both entities have nodes, parents the child's node.\n * @param child\n */\n $addChild(child : GameEntity) : void\n {\n this.children.push(child);\n (child as Mutable<GameEntity>).parent = this;\n\n // If both have nodes, parent the child's node under ours\n if(this.node && child.node)\n {\n (child.node as { parent : unknown }).parent = this.node;\n }\n }\n\n /**\n * Removes a child entity from this entity's hierarchy.\n * Clears the child's parent reference. If the child has a node, detaches it.\n * @param child\n */\n $removeChild(child : GameEntity) : void\n {\n const index = this.children.indexOf(child);\n if(index !== -1)\n {\n this.children.splice(index, 1);\n (child as Mutable<GameEntity>).parent = null;\n\n // Detach child node from parent node\n if(child.node)\n {\n (child.node as { parent : unknown }).parent = null;\n }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Messaging\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Sets the entity manager reference for cross-entity messaging.\n * Called by the EntityManager after entity creation.\n * @param manager\n * @internal\n */\n $setEntityManager(manager : EntityMessenger) : void\n {\n this._entityManager = manager;\n }\n\n /**\n * Sends a fire-and-forget event directly to a specific entity's behaviors, bypassing the event bus.\n * If the target matches this entity's own ID, dispatches locally.\n *\n * @param target - Entity ID or name\n * @param type - Event type string\n * @param payload - Optional event payload\n */\n async send(target : string, type : string, payload ?: unknown) : Promise<void>\n {\n if(target === this.id)\n {\n return this.$send(type, payload, this.id);\n }\n\n if(!this._entityManager)\n {\n throw new Error('Entity is not registered with an EntityManager.');\n }\n\n return this._entityManager.send(target, type, payload, this.id);\n }\n\n /**\n * Sends a request to a specific entity's behaviors and returns the first response.\n * If the target matches this entity's own ID, dispatches locally.\n *\n * @param target - Entity ID or name\n * @param type - Request type string\n * @param payload - Optional request payload\n */\n async request<T>(target : string, type : string, payload ?: unknown) : Promise<RequestResult<T>>\n {\n if(target === this.id)\n {\n return this.$request<T>(type, payload, this.id);\n }\n\n if(!this._entityManager)\n {\n throw new Error('Entity is not registered with an EntityManager.');\n }\n\n return this._entityManager.request<T>(target, type, payload, this.id);\n }\n\n /**\n * Sends a fire-and-forget event directly to this entity's own behaviors.\n *\n * @param type - Event type string\n * @param payload - Optional event payload\n * @internal\n */\n async $send(type : string, payload ?: unknown, senderID ?: string) : Promise<void>\n {\n await this.$processEvent({ type, targetID: this.id, senderID, payload: payload as never });\n }\n\n /**\n * Sends a request to this entity's own behaviors and returns the first response.\n *\n * @param type - Request type string\n * @param payload - Optional request payload\n * @internal\n */\n async $request<T>(type : string, payload ?: unknown, senderID ?: string) : Promise<RequestResult<T>>\n {\n const event : GameEvent = { type, targetID: this.id, senderID, payload: payload as never };\n let lastError : string | undefined;\n\n for(const behavior of this.behaviors)\n {\n try\n {\n // eslint-disable-next-line no-await-in-loop\n const result = await behavior.processRequest?.(event, this.state);\n if(result !== undefined)\n {\n return { success: true, value: result as T };\n }\n }\n catch (err : unknown)\n {\n // Record the error but keep trying subsequent behaviors\n lastError = err instanceof Error ? err.message : String(err);\n }\n }\n\n return {\n success: false,\n error: lastError ?? `No handler for request \"${ type }\" on entity \"${ this.name ?? this.id }\"`,\n };\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Internal Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Resets the entity for object pool recycling.\n * Restores state and tags to defaults, detaches from node, notifies behaviors, and resets children.\n * @param defaultState - Default state from the entity definition\n * @param defaultTags - Default tags from the entity definition\n */\n $reset(defaultState : object, defaultTags : string[]) : void\n {\n // Reset state to a copy of defaults\n this.state = { ...defaultState } as EntityState;\n\n // Clear all tags, then re-add defaults\n this._tags.clear();\n for(const tag of defaultTags)\n {\n this._tags.add(tag);\n }\n\n // Detach from node if attached\n if(this.node)\n {\n this.$detachFromNode();\n }\n\n // Notify behaviors\n for(const behavior of this.behaviors)\n {\n behavior.onReset?.(this.state);\n }\n\n // Recursively reset children\n for(const child of this.children)\n {\n child.$reset({}, []);\n }\n\n // Clear parent reference (the manager handles the parent side)\n (this as Mutable<GameEntity>).parent = null;\n }\n\n /**\n * Processes a game event by passing it to the entity's behaviors.\n * @param event\n */\n async $processEvent(event : GameEvent) : Promise<void>\n {\n for(const behavior of this.behaviors)\n {\n // eslint-disable-next-line no-await-in-loop\n const result = await behavior.processEvent(event, this.state);\n if(result)\n {\n // If the behavior returns true, stop other behaviors from processing the event.\n break;\n }\n }\n }\n\n /**\n * Updates the entity by calling the update method of its behaviors.\n * @param dt - Seconds elapsed since the last update.\n */\n $update(dt : number) : void\n {\n for(const behavior of this.behaviors)\n {\n behavior.update?.(dt, this.state);\n }\n\n // Cascade update to children\n for(const child of this.children)\n {\n child.$update(dt);\n }\n }\n\n /**\n * Destroys the entity by calling the destroy method of its behaviors and unsubscribing from all events.\n */\n async $destroy() : Promise<void>\n {\n // Destroy children first (iterate a copy to avoid mutation during iteration)\n for(const child of [ ...this.children ])\n {\n // eslint-disable-next-line no-await-in-loop\n await child.$destroy();\n }\n\n this.children.length = 0;\n\n for(const behavior of this.behaviors)\n {\n // eslint-disable-next-line no-await-in-loop\n await behavior.destroy?.();\n }\n\n // Unsubscribe from all event subscriptions\n for(const subscription of this.subscriptions.values())\n {\n subscription.unsubscribe();\n }\n\n this.behaviors = [];\n this.subscriptions.clear();\n }\n\n /**\n * Finds the index of a behavior by its constructor.\n * @param behaviorClass\n */\n private findBehaviorIndex(behaviorClass : GameEntityBehaviorConstructor) : number\n {\n return this.behaviors.findIndex((bh) => bh.constructor === behaviorClass);\n }\n\n /**\n * Checks if a behavior of the given class is attached.\n * @param behaviorClass\n */\n hasBehavior(behaviorClass : GameEntityBehaviorConstructor) : boolean\n {\n return this.findBehaviorIndex(behaviorClass) !== -1;\n }\n\n /**\n * Gets a behavior by its constructor.\n * @param behaviorClass\n */\n getBehavior<T extends GameEntityBehavior>(behaviorClass : new () => T) : T | undefined\n {\n return this.behaviors.find((bh) => bh.constructor === behaviorClass) as T | undefined;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Attaches a behavior to the entity.\n * Without options, appends to the end (most common case).\n * With options, inserts at the specified position.\n *\n * @param behavior\n * @param options\n * @throws If the behavior is already attached.\n * @throws If 'before' or 'after' target is not found.\n * @throws If more than one positioning option is specified.\n */\n attachBehavior(behavior : GameEntityBehavior, options ?: AttachBehaviorOptions) : void\n {\n // Check if a behavior of this class is already attached\n if(this.hasBehavior(behavior.constructor as GameEntityBehaviorConstructor))\n {\n throw new Error(`Behavior ${ behavior.constructor.name } is already attached to this entity.`);\n }\n\n // Subscribe to the behavior's event subscriptions\n for(const event of behavior.eventSubscriptions)\n {\n // Check if the event is already subscribed\n const existingSubscription = this.subscriptions.get(event);\n if(existingSubscription)\n {\n existingSubscription.count++;\n }\n else\n {\n // Create a new subscription\n // Cast eventBus to accept any payload map for entity event handling\n const bus = this.eventBus as unknown as GameEventBus<Record<string, unknown>>;\n const unsubscribe = bus.subscribe(event, this.$processEvent.bind(this));\n this.subscriptions.set(event, { count: 1, unsubscribe });\n }\n }\n\n // Determine insertion position\n if(!options)\n {\n // Common case: simple append\n this.behaviors.push(behavior);\n }\n else\n {\n // Validate: only one positioning option allowed\n const positioningOptions = [ options.before, options.after, options.at ]\n .filter((opt) => opt !== undefined);\n\n if(positioningOptions.length > 1)\n {\n throw new Error('attachBehavior: specify only one of before, after, or at');\n }\n\n if(options.before)\n {\n const index = this.findBehaviorIndex(options.before);\n if(index === -1)\n {\n throw new Error(`attachBehavior: no behavior of type ${ options.before.name } found`);\n }\n this.behaviors.splice(index, 0, behavior);\n }\n else if(options.after)\n {\n const index = this.findBehaviorIndex(options.after);\n if(index === -1)\n {\n throw new Error(`attachBehavior: no behavior of type ${ options.after.name } found`);\n }\n this.behaviors.splice(index + 1, 0, behavior);\n }\n else if(options.at !== undefined)\n {\n // Clamp to valid range rather than throwing\n const clampedIndex = Math.max(0, Math.min(options.at, this.behaviors.length));\n this.behaviors.splice(clampedIndex, 0, behavior);\n }\n else\n {\n // No positioning option specified, append\n this.behaviors.push(behavior);\n }\n }\n\n behavior.$setEntity(this);\n\n // If entity already has a node, notify the new behavior\n if(this.node && this._gameEngine)\n {\n behavior.onNodeAttached?.(this.node, this._gameEngine);\n }\n }\n\n /**\n * Detaches a behavior from the entity by its constructor.\n * @param behaviorClass\n */\n detachBehavior(behaviorClass : GameEntityBehaviorConstructor) : boolean\n {\n const index = this.findBehaviorIndex(behaviorClass);\n if(index === -1)\n {\n return false;\n }\n\n const behavior = this.behaviors[index];\n\n // Notify behavior of node detachment if entity has a node\n if(this.node)\n {\n behavior.onNodeDetached?.();\n }\n\n // Remove the behavior's event subscriptions\n for(const event of behavior.eventSubscriptions)\n {\n const subscription = this.subscriptions.get(event);\n if(subscription)\n {\n subscription.count--;\n if(subscription.count <= 0)\n {\n subscription.unsubscribe();\n this.subscriptions.delete(event);\n }\n }\n }\n\n // Remove the behavior\n this.behaviors.splice(index, 1);\n behavior.$setEntity(null);\n\n return true;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n","//----------------------------------------------------------------------------------------------------------------------\n// Entity Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { TransformNode } from '@babylonjs/core';\n\nimport { GameEntity, type RequestResult } from '../classes/entity.ts';\nimport { GameEventBus } from '../classes/eventBus.ts';\nimport type { GameEngine } from '../classes/gameEngine.ts';\n\n// Interfaces\nimport type { Action } from '../interfaces/action.ts';\nimport type { GameEntityDefinition } from '../interfaces/entity.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type { BindingManager } from '../managers/binding.ts';\n\n// Utils\nimport { LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Options for creating an entity instance.\n */\nexport interface CreateEntityOptions<State extends object = object>\n{\n /** Initial state to merge with defaultState. */\n initialState ?: Partial<State>;\n\n /** Override name from entity definition. */\n name ?: string;\n\n /** Additional tags to add (merged with definition tags). */\n tags ?: string[];\n\n /** Scene node to attach the entity to. */\n node ?: TransformNode;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class GameEntityManager implements Disposable\n{\n /** The event bus for the entity manager. */\n private eventBus : GameEventBus;\n\n /** A map of entities managed by the entity manager. */\n private entities : Map<string, GameEntity> = new Map<string, GameEntity>();\n\n /** A map of entity definitions registered with the entity manager. */\n private entityDefinitions : Map<string, GameEntityDefinition> = new Map<string, GameEntityDefinition>();\n\n /** Late-bound GameEngine reference */\n private _gameEngine : GameEngine | null = null;\n\n /** Reference to the binding manager for registering actions */\n private bindingManager : BindingManager;\n\n /** Logger instance */\n private _log : LoggerInterface;\n\n //------------------------------------------------------------------------------------------------------------------\n // Indices for fast lookups\n //------------------------------------------------------------------------------------------------------------------\n\n /** Index: name -> entity (names are not unique, first match returned by getByName) */\n private _entitiesByName : Map<string, Set<GameEntity>> = new Map<string, Set<GameEntity>>();\n\n /** Index: type -> entities */\n private _entitiesByType : Map<string, Set<GameEntity>> = new Map<string, Set<GameEntity>>();\n\n /** Index: tag -> entities */\n private _entitiesByTag : Map<string, Set<GameEntity>> = new Map<string, Set<GameEntity>>();\n\n /** Index: node -> entity (one entity per node) */\n private _entitiesByNode : Map<TransformNode, GameEntity> = new Map<TransformNode, GameEntity>();\n\n /** Object pool storage: type -> pooled entities */\n private _pools : Map<string, GameEntity[]> = new Map<string, GameEntity[]>();\n\n /**\n * Creates an instance of EntityManager.\n * @param eventBus\n * @param logger\n * @param bindingManager\n */\n constructor(eventBus : GameEventBus, logger : LoggingUtility | undefined, bindingManager : BindingManager)\n {\n this.eventBus = eventBus;\n this.bindingManager = bindingManager;\n this._log = logger?.getLogger('EntityManager') || new SAGELogger('EntityManager');\n this._log.info('EntityManager initialized');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Internal API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Late-bind the GameEngine after construction.\n * Called by createGameEngine() after the GameEngine instance is created.\n */\n $setGameEngine(gameEngine : GameEngine) : void\n {\n this._gameEngine = gameEngine;\n }\n\n private get _engine() : GameEngine\n {\n if(!this._gameEngine)\n {\n throw new Error('EntityManager: gameEngine not set. Call $setGameEngine() first.');\n }\n return this._gameEngine;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods - Index Management\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Gets or creates a Set in a Map, returning the Set.\n */\n private _getOrCreateSet<K>(map : Map<K, Set<GameEntity>>, key : K) : Set<GameEntity>\n {\n let set = map.get(key);\n if(!set)\n {\n set = new Set<GameEntity>();\n map.set(key, set);\n }\n return set;\n }\n\n /**\n * Adds an entity to all relevant indices.\n */\n private _indexEntity(entity : GameEntity) : void\n {\n // Index by type\n this._getOrCreateSet(this._entitiesByType, entity.type).add(entity);\n\n // Index by name (if present)\n if(entity.name)\n {\n this._getOrCreateSet(this._entitiesByName, entity.name).add(entity);\n }\n\n // Index by tags\n for(const tag of entity.tags)\n {\n this._getOrCreateSet(this._entitiesByTag, tag).add(entity);\n }\n\n // Index by node (if attached)\n if(entity.node)\n {\n this._entitiesByNode.set(entity.node, entity);\n }\n }\n\n /**\n * Removes an entity from all indices.\n */\n private _unindexEntity(entity : GameEntity) : void\n {\n // Remove from type index\n const typeSet = this._entitiesByType.get(entity.type);\n if(typeSet)\n {\n typeSet.delete(entity);\n if(typeSet.size === 0)\n {\n this._entitiesByType.delete(entity.type);\n }\n }\n\n // Remove from name index\n if(entity.name)\n {\n const nameSet = this._entitiesByName.get(entity.name);\n if(nameSet)\n {\n nameSet.delete(entity);\n if(nameSet.size === 0)\n {\n this._entitiesByName.delete(entity.name);\n }\n }\n }\n\n // Remove from all tag indices\n for(const tag of entity.tags)\n {\n const tagSet = this._entitiesByTag.get(tag);\n if(tagSet)\n {\n tagSet.delete(entity);\n if(tagSet.size === 0)\n {\n this._entitiesByTag.delete(tag);\n }\n }\n }\n\n // Remove from node index\n if(entity.node)\n {\n this._entitiesByNode.delete(entity.node);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods - Inheritance\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Resolves an entity definition that extends a base definition by merging according to inheritance rules.\n * The base definition must already be registered.\n */\n private _resolveInheritance(childDef : GameEntityDefinition) : GameEntityDefinition\n {\n const baseType = childDef.extends;\n if(!baseType)\n {\n return childDef;\n }\n\n const baseDef = this.entityDefinitions.get(baseType);\n if(!baseDef)\n {\n const errorMsg = `Cannot extend \"${ baseType }\": base entity definition is not registered.`;\n this._log.error(errorMsg);\n throw new Error(errorMsg);\n }\n\n // Merge tags: concatenate and deduplicate\n let mergedTags : string[] | undefined;\n if(baseDef.tags || childDef.tags)\n {\n mergedTags = [ ...new Set([ ...(baseDef.tags ?? []), ...(childDef.tags ?? []) ]) ];\n }\n\n // Build resolved definition — child overrides base for scalar/replace fields\n const resolved : GameEntityDefinition = {\n // Scalar fields: child overrides if provided, else inherit\n type: childDef.type,\n name: childDef.name ?? baseDef.name,\n mesh: childDef.mesh ?? baseDef.mesh,\n\n // Shallow-merged state\n defaultState: { ...baseDef.defaultState, ...childDef.defaultState },\n\n // Replace-entirely fields: child overrides if provided, else inherit\n behaviors: childDef.behaviors ?? baseDef.behaviors,\n actions: childDef.actions ?? baseDef.actions,\n\n // Concatenated tags\n tags: mergedTags,\n\n // Pooling: child overrides if provided, else inherit\n poolable: childDef.poolable ?? baseDef.poolable,\n poolSize: childDef.poolSize ?? baseDef.poolSize,\n\n // Children: child overrides if provided, else inherit\n children: childDef.children ?? baseDef.children,\n\n // Lifecycle hooks: child overrides if provided, else inherit\n onBeforeCreate: childDef.onBeforeCreate ?? baseDef.onBeforeCreate,\n onCreate: childDef.onCreate ?? baseDef.onCreate,\n onBeforeDestroy: childDef.onBeforeDestroy ?? baseDef.onBeforeDestroy,\n onDestroy: childDef.onDestroy ?? baseDef.onDestroy,\n };\n\n return resolved;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods - Actions\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Checks if two actions are compatible (same type and non-conflicting options).\n * @param existingAction - The action already registered\n * @param newAction - The action being registered\n */\n private _areActionsCompatible(existingAction : Action, newAction : Action) : boolean\n {\n // If types don't match, they're incompatible\n if(existingAction.type !== newAction.type)\n {\n return false;\n }\n\n // For analog actions, check min and max values\n if(existingAction.type === 'analog' && newAction.type === 'analog')\n {\n // If minValue or maxValue is specified and differs, they're incompatible\n if((newAction.minValue !== undefined && existingAction.minValue !== newAction.minValue)\n || (newAction.maxValue !== undefined && existingAction.maxValue !== newAction.maxValue))\n {\n return false;\n }\n }\n\n // Digital actions are always compatible if they have the same name\n return true;\n }\n\n /**\n * Registers actions defined in the entity definition with the binding manager.\n * Skips actions already registered with compatible options; warns on conflicts.\n * @param entityDef\n */\n private _registerEntityActions(entityDef : GameEntityDefinition) : void\n {\n if(!entityDef.actions)\n {\n return;\n }\n\n for(const action of entityDef.actions)\n {\n try\n {\n // Check if the action is already registered\n const existingAction = this.bindingManager.getAction(action.name);\n\n if(!existingAction)\n {\n // Action doesn't exist yet, register it\n this._log.debug(`Registering action \"${ action.name }\" from entity type \"${ entityDef.type }\"`);\n this.bindingManager.registerAction(action);\n }\n else if(!this._areActionsCompatible(existingAction, action))\n {\n this._log.warn(\n `Action \"${ action.name }\" already registered with different options. `\n + `Entity \"${ entityDef.type }\" requires: ${ JSON.stringify(action) }, `\n + `but found: ${ JSON.stringify(existingAction) }`\n );\n }\n else\n {\n this._log.trace(\n `Action \"${ action.name }\" already registered with compatible options, skipping registration`\n );\n }\n }\n catch (err)\n {\n this._log.debug(`Failed to register action \"${ action.name }\": `\n + `${ err instanceof Error ? err.message : String(err) }`);\n }\n }\n }\n\n $frameUpdate(dt : number) : void\n {\n this._log.trace(`Updating ${ this.entities.size } entities with dt=${ dt }`);\n for(const entity of this.entities.values())\n {\n // Only update root entities; children are cascaded via parent.$update()\n if(!entity.parent)\n {\n entity.$update(dt);\n }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Registers a new entity definition.\n * Also registers any actions declared in the definition with the binding manager.\n * @param entityDef\n */\n registerEntityDefinition(entityDef : GameEntityDefinition) : void\n {\n this._log.debug(`Registering entity definition: ${ entityDef.type }`);\n\n // Resolve inheritance if extends is specified\n const resolved = entityDef.extends\n ? this._resolveInheritance(entityDef)\n : entityDef;\n\n // Register any actions this entity requires\n this._registerEntityActions(resolved);\n\n // Store the resolved entity definition\n this.entityDefinitions.set(resolved.type, resolved);\n\n // Auto-prewarm pool if poolSize is specified\n if(resolved.poolSize && resolved.poolSize > 0)\n {\n // Prewarm is async but registration is sync — fire and forget is fine here\n // because prewarm just creates entities and pools them\n void this.prewarm(resolved.type, resolved.poolSize);\n }\n }\n\n /**\n * Retrieve an entity definition by type.\n * @param type\n */\n getDefinition(type : string) : GameEntityDefinition | undefined\n {\n return this.entityDefinitions.get(type);\n }\n\n /**\n * Creates a new entity of the given type.\n * Runs onBeforeCreate/onCreate lifecycle hooks, merges state, applies tags, and indexes the entity.\n * @param type - Must match a previously registered entity definition\n * @param options\n */\n async createEntity<\n State extends object = object,\n >\n (\n type : string,\n options : CreateEntityOptions<State> = {}\n ) : Promise<GameEntity<State>>\n {\n this._log.debug(`Creating entity of type: ${ type }`);\n const entityDef = this.entityDefinitions.get(type);\n if(!entityDef)\n {\n const errorMsg = `Entity type ${ type } is not registered.`;\n this._log.error(errorMsg);\n throw new Error(errorMsg);\n }\n\n // Check pool for recyclable entity\n if(entityDef.poolable)\n {\n const pool = this._pools.get(type);\n if(pool && pool.length > 0)\n {\n this._log.trace(`Recycling entity from pool for type: ${ type }`);\n const entity = pool.pop() as GameEntity<State>;\n\n // Reset with definition defaults\n entity.$reset(\n entityDef.defaultState,\n entityDef.tags ?? []\n );\n\n // Re-wire the entity manager back-reference\n entity.$setEntityManager(this);\n\n // Apply any initialState overrides\n if(options.initialState)\n {\n entity.state = { ...entity.state, ...options.initialState } as State;\n }\n\n // Apply additional tags from options\n if(options.tags)\n {\n for(const tag of options.tags)\n {\n entity.$addTag(tag);\n }\n }\n\n // Attach to node if provided\n if(options.node)\n {\n entity.$attachToNode(options.node, this._engine);\n }\n\n // Re-index\n this.entities.set(entity.id, entity);\n this._indexEntity(entity);\n\n this._log.debug(`Recycled entity ${ entity.id } from pool`);\n return entity;\n }\n }\n\n this._log.trace(`Using entity definition with ${ entityDef.behaviors?.length || 0 } behaviors`);\n let mergedState = { ...entityDef.defaultState, ...options.initialState } as State;\n\n // Call onBeforeCreate hook if it exists\n if(entityDef.onBeforeCreate)\n {\n this._log.trace(`Calling onBeforeCreate hook for entity type: ${ type }`);\n const result = await entityDef.onBeforeCreate(mergedState);\n // If the hook returns a new state, use it\n if(result !== undefined)\n {\n mergedState = result as State;\n }\n }\n\n // Determine entity name (options override definition)\n const entityName = options.name ?? entityDef.name;\n\n // Create the entity instance with possibly modified state\n const entity = new GameEntity<State>(\n entityDef.type,\n this.eventBus,\n mergedState,\n entityDef.behaviors,\n entityName\n );\n\n // Wire up the entity manager back-reference for cross-entity messaging\n entity.$setEntityManager(this);\n\n // Attach to node if provided\n if(options.node)\n {\n entity.$attachToNode(options.node, this._engine);\n }\n\n // Apply tags from definition\n if(entityDef.tags)\n {\n for(const tag of entityDef.tags)\n {\n entity.$addTag(tag);\n }\n }\n\n // Apply additional tags from options\n if(options.tags)\n {\n for(const tag of options.tags)\n {\n entity.$addTag(tag);\n }\n }\n\n // Add entity to the manager and index it\n this.entities.set(entity.id, entity);\n this._indexEntity(entity);\n this._log.debug(`Entity created with ID: ${ entity.id }${ entityName ? ` (name: ${ entityName })` : '' }`);\n\n // Call onCreate hook if it exists\n if(entityDef.onCreate)\n {\n this._log.trace(`Calling onCreate hook for entity type: ${ type }`);\n const result = await entityDef.onCreate(entity.state);\n // If the hook returns a new state, update the entity's state\n if(result !== undefined)\n {\n entity.state = result as State;\n }\n }\n\n // Create child entities if defined\n if(entityDef.children)\n {\n for(const childDef of entityDef.children)\n {\n const childInitialState : Record<string, unknown> = { ...childDef.initialState };\n\n // Store position/rotation in initial state for later application\n if(childDef.position)\n {\n childInitialState._position = childDef.position;\n }\n if(childDef.rotation)\n {\n childInitialState._rotation = childDef.rotation;\n }\n\n // eslint-disable-next-line no-await-in-loop\n const childEntity = await this.createEntity(childDef.type, {\n name: childDef.name,\n initialState: childInitialState,\n });\n\n entity.$addChild(childEntity);\n }\n }\n\n return entity;\n }\n\n /**\n * Destroys the entity with the given ID.\n * Runs onBeforeDestroy/onDestroy lifecycle hooks, unindexes, and removes the entity.\n * @param entityID\n */\n async destroyEntity(entityID : string) : Promise<void>\n {\n this._log.debug(`Destroying entity: ${ entityID }`);\n const entity = this.entities.get(entityID);\n if(entity)\n {\n // Get entity definition\n const entityDef = this.entityDefinitions.get(entity.type);\n\n // Check if this entity should be pooled instead of destroyed\n if(entityDef?.poolable)\n {\n this._log.trace(`Pooling entity ${ entityID } instead of destroying`);\n\n // Pool children first (iterate copy to avoid mutation during iteration)\n for(const child of [ ...entity.children ])\n {\n // eslint-disable-next-line no-await-in-loop\n await this.destroyEntity(child.id);\n }\n\n // If the entity has a parent, remove it from the parent's children\n if(entity.parent)\n {\n entity.parent.$removeChild(entity);\n }\n\n // Remove from all indices\n this._unindexEntity(entity);\n\n // Reset the entity\n entity.$reset(entityDef.defaultState, entityDef.tags ?? []);\n\n // Push to pool\n let pool = this._pools.get(entity.type);\n if(!pool)\n {\n pool = [];\n this._pools.set(entity.type, pool);\n }\n pool.push(entity);\n\n // Remove from active entities\n this.entities.delete(entityID);\n\n this._log.debug(`Entity ${ entityID } pooled`);\n return;\n }\n\n // Non-poolable: destroy normally\n\n // Destroy children first (iterate copy to avoid mutation during iteration)\n for(const child of [ ...entity.children ])\n {\n // eslint-disable-next-line no-await-in-loop\n await this.destroyEntity(child.id);\n }\n\n // If the entity has a parent, remove it from the parent's children\n if(entity.parent)\n {\n entity.parent.$removeChild(entity);\n }\n\n // Call onBeforeDestroy hook if it exists\n if(entityDef?.onBeforeDestroy)\n {\n this._log.trace(`Calling onBeforeDestroy hook for entity: ${ entityID }`);\n const result = await entityDef.onBeforeDestroy(entity.state);\n // If the hook returns a new state, update the entity's state\n if(result !== undefined)\n {\n entity.state = result;\n }\n }\n\n // Remove from indices BEFORE destroying\n this._unindexEntity(entity);\n\n // Destroy entity (call $destroy internally)\n await entity.$destroy();\n\n // Call onDestroy hook if it exists\n if(entityDef?.onDestroy)\n {\n this._log.trace(`Calling onDestroy hook for entity: ${ entityID }`);\n await entityDef.onDestroy(entity.state);\n }\n\n // Remove entity from manager\n this.entities.delete(entityID);\n\n this._log.debug(`Entity ${ entityID } destroyed`);\n }\n else\n {\n this._log.warn(`Attempted to destroy non-existent entity: ${ entityID }`);\n }\n }\n\n /**\n * Gets the entity with the given ID.\n * @param entityID\n */\n getEntity(entityID : string) : GameEntity | null\n {\n this._log.trace(`Getting entity: ${ entityID }`);\n return this.entities.get(entityID) ?? null;\n }\n\n /**\n * Adds an existing entity to the entity manager and indexes it.\n * @param entity\n */\n addEntity(entity : GameEntity) : void\n {\n this._log.debug(`Adding existing entity: ${ entity.id } (type: ${ entity.type })`);\n entity.$setEntityManager(this);\n this.entities.set(entity.id, entity);\n this._indexEntity(entity);\n }\n\n /**\n * Removes the entity with the given ID, without destroying it.\n * @param entityID\n */\n removeEntity(entityID : string) : void\n {\n this._log.debug(`Removing entity: ${ entityID }`);\n const entity = this.entities.get(entityID);\n if(entity)\n {\n this._unindexEntity(entity);\n this.entities.delete(entityID);\n this._log.debug(`Entity ${ entityID } removed`);\n }\n else\n {\n this._log.warn(`Attempted to remove non-existent entity: ${ entityID }`);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Parent-Child Hierarchy\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Adds a child entity to a parent entity.\n * @param parentId - ID of the parent entity\n * @param childId - ID of the child entity\n * @returns False if either entity is not found\n */\n addChild(parentId : string, childId : string) : boolean\n {\n const parent = this.entities.get(parentId);\n const child = this.entities.get(childId);\n\n if(!parent)\n {\n this._log.warn(`addChild: parent entity \"${ parentId }\" not found`);\n return false;\n }\n\n if(!child)\n {\n this._log.warn(`addChild: child entity \"${ childId }\" not found`);\n return false;\n }\n\n parent.$addChild(child);\n this._log.trace(`Added child ${ childId } to parent ${ parentId }`);\n return true;\n }\n\n /**\n * Removes a child entity from a parent entity.\n * @param parentId - ID of the parent entity\n * @param childId - ID of the child entity\n * @returns False if either entity is not found\n */\n removeChild(parentId : string, childId : string) : boolean\n {\n const parent = this.entities.get(parentId);\n const child = this.entities.get(childId);\n\n if(!parent)\n {\n this._log.warn(`removeChild: parent entity \"${ parentId }\" not found`);\n return false;\n }\n\n if(!child)\n {\n this._log.warn(`removeChild: child entity \"${ childId }\" not found`);\n return false;\n }\n\n parent.$removeChild(child);\n this._log.trace(`Removed child ${ childId } from parent ${ parentId }`);\n return true;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Query Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Gets the first entity with the given name.\n * Names are not unique; use getEntitiesByName() to retrieve all matches.\n * @param name\n */\n getByName(name : string) : GameEntity | undefined\n {\n const nameSet = this._entitiesByName.get(name);\n if(nameSet && nameSet.size > 0)\n {\n return nameSet.values().next().value;\n }\n return undefined;\n }\n\n /**\n * Gets all entities with the given name.\n * @param name\n */\n getEntitiesByName(name : string) : GameEntity[]\n {\n const nameSet = this._entitiesByName.get(name);\n return nameSet ? Array.from(nameSet) : [];\n }\n\n /**\n * Gets all entities of the given type.\n * @param type\n */\n getByType(type : string) : GameEntity[]\n {\n const typeSet = this._entitiesByType.get(type);\n return typeSet ? Array.from(typeSet) : [];\n }\n\n /**\n * Gets all entities with the given tag.\n * @param tag\n */\n getByTag(tag : string) : GameEntity[]\n {\n const tagSet = this._entitiesByTag.get(tag);\n return tagSet ? Array.from(tagSet) : [];\n }\n\n /**\n * Gets all entities matching multiple tags.\n * @param tags\n * @param mode - 'all' requires every tag (intersection), 'any' requires at least one (union)\n */\n getByTags(tags : string[], mode : 'all' | 'any' = 'all') : GameEntity[]\n {\n if(tags.length === 0)\n {\n return [];\n }\n\n if(mode === 'any')\n {\n // Union - entities with ANY of the tags\n const result = new Set<GameEntity>();\n for(const tag of tags)\n {\n const tagSet = this._entitiesByTag.get(tag);\n if(tagSet)\n {\n for(const entity of tagSet)\n {\n result.add(entity);\n }\n }\n }\n return Array.from(result);\n }\n else\n {\n // Intersection - entities with ALL tags\n // Start with smallest set for efficiency\n const sets = tags\n .map((tag) => this._entitiesByTag.get(tag))\n .filter((set) : set is Set<GameEntity> => set !== undefined);\n\n // If any tag has no entities, result is empty\n if(sets.length !== tags.length)\n {\n return [];\n }\n\n // Sort by size (smallest first) for efficient intersection\n sets.sort((setA, setB) => setA.size - setB.size);\n const [ smallest, ...rest ] = sets;\n\n return Array.from(smallest).filter((entity) => rest.every((set) => set.has(entity)));\n }\n }\n\n /**\n * Returns an iterator over all entities.\n * More memory-efficient than returning an array for large entity counts.\n */\n getAllEntities() : IterableIterator<GameEntity>\n {\n return this.entities.values();\n }\n\n /**\n * Gets the total number of entities.\n */\n get entityCount() : number\n {\n return this.entities.size;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Messaging\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Sends a fire-and-forget event directly to a specific entity's behaviors, bypassing the event bus.\n * Resolves the target by entity ID first (O(1)), then falls back to name lookup (O(1)).\n *\n * @param target - Entity ID or name\n * @param type - Event type string\n * @param payload - Optional event payload\n */\n async send(target : string, type : string, payload ?: unknown, senderID ?: string) : Promise<void>\n {\n const entity = this.entities.get(target) ?? this.getByName(target);\n if(!entity)\n {\n this._log.warn(`send: entity \"${ target }\" not found`);\n return;\n }\n\n await entity.$send(type, payload, senderID);\n }\n\n /**\n * Sends a request to a specific entity's behaviors and returns the first response.\n * Resolves the target by entity ID first (O(1)), then falls back to name lookup (O(1)).\n *\n * @param target - Entity ID or name\n * @param type - Request type string\n * @param payload - Optional request payload\n * @param senderID - Optional sender entity ID (stamped on the event)\n */\n async request<T>(\n target : string,\n type : string,\n payload ?: unknown,\n senderID ?: string\n ) : Promise<RequestResult<T>>\n {\n const entity = this.entities.get(target) ?? this.getByName(target);\n if(!entity)\n {\n return { success: false, error: `Entity \"${ target }\" not found` };\n }\n\n return entity.$request<T>(type, payload, senderID);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Tag Manipulation\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Adds a tag to an entity and updates the tag index.\n * @param entity - Entity instance or entity ID\n * @param tag\n * @returns False if already present or entity not found\n */\n addTag(entity : GameEntity | string, tag : string) : boolean\n {\n const targetEntity = typeof entity === 'string' ? this.entities.get(entity) : entity;\n if(!targetEntity)\n {\n this._log.warn(`addTag: entity not found`);\n return false;\n }\n\n if(targetEntity.hasTag(tag))\n {\n return false; // Already has tag\n }\n\n // Add to entity and update index\n targetEntity.$addTag(tag);\n this._getOrCreateSet(this._entitiesByTag, tag).add(targetEntity);\n\n this._log.trace(`Added tag \"${ tag }\" to entity ${ targetEntity.id }`);\n return true;\n }\n\n /**\n * Removes a tag from an entity and updates the tag index.\n * @param entity - Entity instance or entity ID\n * @param tag\n * @returns False if not present or entity not found\n */\n removeTag(entity : GameEntity | string, tag : string) : boolean\n {\n const targetEntity = typeof entity === 'string' ? this.entities.get(entity) : entity;\n if(!targetEntity)\n {\n this._log.warn(`removeTag: entity not found`);\n return false;\n }\n\n if(!targetEntity.$removeTag(tag))\n {\n return false; // Didn't have tag\n }\n\n // Update index\n const tagSet = this._entitiesByTag.get(tag);\n if(tagSet)\n {\n tagSet.delete(targetEntity);\n if(tagSet.size === 0)\n {\n this._entitiesByTag.delete(tag);\n }\n }\n\n this._log.trace(`Removed tag \"${ tag }\" from entity ${ targetEntity.id }`);\n return true;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Node Attachment\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Gets an entity by its attached scene node.\n * @param node\n */\n getByNode(node : TransformNode) : GameEntity | undefined\n {\n return this._entitiesByNode.get(node);\n }\n\n /**\n * Attaches an entity to a scene node.\n * The entity will reference this node and can be looked up via getByNode().\n *\n * @param entity - Entity instance or entity ID\n * @param node\n * @returns False if entity not found or node already has an entity\n */\n attachToNode(entity : GameEntity | string, node : TransformNode) : boolean\n {\n const targetEntity = typeof entity === 'string' ? this.entities.get(entity) : entity;\n if(!targetEntity)\n {\n this._log.warn(`attachToNode: entity not found`);\n return false;\n }\n\n // Check if node already has an entity\n if(this._entitiesByNode.has(node))\n {\n this._log.warn(`attachToNode: node already has an attached entity`);\n return false;\n }\n\n // Detach from current node if any\n if(targetEntity.node)\n {\n this._entitiesByNode.delete(targetEntity.node);\n }\n\n // Attach to new node\n targetEntity.$attachToNode(node, this._engine);\n this._entitiesByNode.set(node, targetEntity);\n\n this._log.trace(`Attached entity ${ targetEntity.id } to node \"${ node.name }\"`);\n return true;\n }\n\n /**\n * Detaches an entity from its scene node.\n *\n * @param entity - Entity instance or entity ID\n * @returns False if entity not found or had no node\n */\n detachFromNode(entity : GameEntity | string) : boolean\n {\n const targetEntity = typeof entity === 'string' ? this.entities.get(entity) : entity;\n if(!targetEntity)\n {\n this._log.warn(`detachFromNode: entity not found`);\n return false;\n }\n\n if(!targetEntity.node)\n {\n return false; // Nothing to detach\n }\n\n // Remove from index\n this._entitiesByNode.delete(targetEntity.node);\n\n // Detach entity\n const nodeName = targetEntity.node.name;\n targetEntity.$detachFromNode();\n\n this._log.trace(`Detached entity ${ targetEntity.id } from node \"${ nodeName }\"`);\n return true;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Object Pooling\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Pre-creates entities and pools them for fast recycling.\n * @param type - Entity type to prewarm\n * @param count - Number of entities to create and pool\n */\n async prewarm(type : string, count : number) : Promise<void>\n {\n this._log.debug(`Prewarming pool for \"${ type }\" with ${ count } entities`);\n\n for(let i = 0; i < count; i++)\n {\n // eslint-disable-next-line no-await-in-loop\n const entity = await this.createEntity(type);\n // eslint-disable-next-line no-await-in-loop\n await this.destroyEntity(entity.id);\n }\n }\n\n /**\n * Drains the pool for a given type, actually destroying all pooled entities.\n * @param type - Entity type whose pool to drain\n */\n async drainPool(type : string) : Promise<void>\n {\n const pool = this._pools.get(type);\n if(!pool)\n {\n return;\n }\n\n this._log.debug(`Draining pool for \"${ type }\" (${ pool.length } entities)`);\n\n for(const entity of pool)\n {\n // eslint-disable-next-line no-await-in-loop\n await entity.$destroy();\n }\n\n pool.length = 0;\n this._pools.delete(type);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Tears down all entities and releases resources.\n * Destroys entities individually (running lifecycle hooks), then clears definitions and indices.\n */\n async $teardown() : Promise<void>\n {\n this._log.info(`Tearing down EntityManager with ${ this.entities.size } entities`);\n\n // Create a copy of the entity IDs to avoid modification during iteration\n const entityIds = [ ...this.entities.keys() ];\n\n // Destroy all entities\n for(const entityId of entityIds)\n {\n try\n {\n // eslint-disable-next-line no-await-in-loop\n await this.destroyEntity(entityId);\n }\n catch (err)\n {\n this._log.error(`Error destroying entity ${ entityId }: ${ err }`);\n }\n }\n\n // Drain all pools\n for(const type of [ ...this._pools.keys() ])\n {\n try\n {\n // eslint-disable-next-line no-await-in-loop\n await this.drainPool(type);\n }\n catch (err)\n {\n this._log.error(`Error draining pool for \"${ type }\": ${ err }`);\n }\n }\n\n // Clear entity definitions\n this.entityDefinitions.clear();\n\n // Clear indices\n this._entitiesByName.clear();\n this._entitiesByType.clear();\n this._entitiesByTag.clear();\n this._entitiesByNode.clear();\n\n this._log.info('EntityManager torn down successfully');\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n","//----------------------------------------------------------------------------------------------------------------------\n// Post-Processing Pipeline Backend\n//\n// Applies post-processing effects via BabylonJS DefaultRenderingPipeline and SSAO2RenderingPipeline.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport {\n Color4,\n DefaultRenderingPipeline,\n ImageProcessingConfiguration,\n SSAO2RenderingPipeline,\n type Scene,\n} from '@babylonjs/core';\n\nimport type { PostProcessingConfig } from '../interfaces/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Map from user-facing tonemap operator names to BabylonJS constants.\n * BabylonJS only has STANDARD (0) and ACES (1) — the other names all map to STANDARD.\n */\nconst TONEMAP_OPERATORS : Record<string, number> = {\n hable: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n reinhard: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n hejidawson: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n photographic: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n aces: ImageProcessingConfiguration.TONEMAPPING_ACES,\n};\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Apply post-processing effects to a scene based on a declarative config.\n * Creates BabylonJS rendering pipelines and configures them according to the provided settings.\n *\n * This is a level-config handler, not a node property handler.\n */\nexport function applyPipelinePostProcessing(scene : Scene, config : PostProcessingConfig) : void\n{\n // Can't set up post-processing without a camera\n if(!scene.activeCamera)\n {\n return;\n }\n\n // Warn about Frame Graph-only features\n if(config.volumetric)\n {\n console.warn('[SAGE] Volumetric lighting requires renderer: \"frameGraph\". Ignored in pipeline mode.');\n }\n\n // Create the default rendering pipeline\n const pipeline = new DefaultRenderingPipeline('sage-default', true, scene, scene.cameras);\n\n // Bloom\n if(config.bloom)\n {\n pipeline.bloomEnabled = true;\n\n if(config.bloom.weight !== undefined)\n {\n pipeline.bloomWeight = config.bloom.weight;\n }\n if(config.bloom.threshold !== undefined)\n {\n pipeline.bloomThreshold = config.bloom.threshold;\n }\n if(config.bloom.scale !== undefined)\n {\n pipeline.bloomScale = config.bloom.scale;\n }\n if(config.bloom.kernel !== undefined)\n {\n pipeline.bloomKernel = config.bloom.kernel;\n }\n }\n\n // Tone mapping\n if(config.tonemap)\n {\n pipeline.imageProcessingEnabled = true;\n pipeline.imageProcessing.toneMappingEnabled = true;\n\n if(config.tonemap.operator)\n {\n pipeline.imageProcessing.toneMappingType\n = TONEMAP_OPERATORS[config.tonemap.operator] ?? ImageProcessingConfiguration.TONEMAPPING_STANDARD;\n }\n }\n\n // Film grain\n if(config.grain)\n {\n pipeline.grainEnabled = true;\n\n if(config.grain.intensity !== undefined)\n {\n pipeline.grain.intensity = config.grain.intensity;\n }\n if(config.grain.animated !== undefined)\n {\n pipeline.grain.animated = config.grain.animated;\n }\n }\n\n // Vignette\n if(config.vignette)\n {\n pipeline.imageProcessingEnabled = true;\n pipeline.imageProcessing.vignetteEnabled = true;\n\n if(config.vignette.weight !== undefined)\n {\n pipeline.imageProcessing.vignetteWeight = config.vignette.weight;\n }\n if(config.vignette.stretch !== undefined)\n {\n pipeline.imageProcessing.vignetteStretch = config.vignette.stretch;\n }\n if(config.vignette.color)\n {\n pipeline.imageProcessing.vignetteColor = new Color4(\n config.vignette.color.r,\n config.vignette.color.g,\n config.vignette.color.b,\n 1\n );\n }\n }\n\n // Sharpen\n if(config.sharpen)\n {\n pipeline.sharpenEnabled = true;\n\n if(config.sharpen.edge !== undefined)\n {\n pipeline.sharpen.edgeAmount = config.sharpen.edge;\n }\n if(config.sharpen.color !== undefined)\n {\n pipeline.sharpen.colorAmount = config.sharpen.color;\n }\n }\n\n // Chromatic aberration\n if(config.chromaticAberration)\n {\n pipeline.chromaticAberrationEnabled = true;\n\n if(config.chromaticAberration.amount !== undefined)\n {\n pipeline.chromaticAberration.aberrationAmount = config.chromaticAberration.amount;\n }\n }\n\n // SSAO — uses its own separate pipeline\n if(config.ssao)\n {\n const ssao = new SSAO2RenderingPipeline('sage-ssao', scene, { ssaoRatio: 0.5, blurRatio: 1.0 });\n\n if(config.ssao.radius !== undefined)\n {\n ssao.radius = config.ssao.radius;\n }\n if(config.ssao.samples !== undefined)\n {\n ssao.samples = config.ssao.samples;\n }\n if(config.ssao.totalStrength !== undefined)\n {\n ssao.totalStrength = config.ssao.totalStrength;\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Frame Graph Post-Processing Backend\n//\n// Builds a directed task chain using BabylonJS Frame Graph system.\n// Each post-processing effect is a separate task chained via texture handles.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport {\n Constants,\n FrameGraph,\n FrameGraphBloomTask,\n FrameGraphChromaticAberrationTask,\n FrameGraphClearTextureTask,\n FrameGraphCopyToBackbufferColorTask,\n FrameGraphGeometryRendererTask,\n FrameGraphGrainTask,\n FrameGraphImageProcessingTask,\n FrameGraphObjectRendererTask,\n FrameGraphSSAO2RenderingPipelineTask,\n FrameGraphSharpenTask,\n type FrameGraphTextureHandle,\n ImageProcessingConfiguration,\n type Scene,\n} from '@babylonjs/core';\n\nimport type { PostProcessingConfig } from '../interfaces/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Map from user-facing tonemap operator names to BabylonJS constants.\n * BabylonJS only has STANDARD (0) and ACES (1) — the other names all map to STANDARD.\n */\nconst TONEMAP_OPERATORS : Record<string, number> = {\n hable: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n reinhard: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n hejidawson: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n photographic: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n aces: ImageProcessingConfiguration.TONEMAPPING_ACES,\n};\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport async function applyFrameGraphPostProcessing(scene : Scene, config : PostProcessingConfig) : Promise<void>\n{\n const camera = scene.activeCamera;\n if(!camera)\n {\n return;\n }\n\n const engine = scene.getEngine();\n const fg = new FrameGraph(scene);\n\n // Create the main render target texture\n const colorTexture = fg.textureManager.createRenderTargetTexture('sage-fg-color', {\n size: { width: 100, height: 100 },\n sizeIsPercentage: true,\n options: { formats: [ Constants.TEXTUREFORMAT_RGBA ], samples: 4 },\n });\n\n // 1. Clear\n const clearTask = new FrameGraphClearTextureTask('sage-clear', fg);\n clearTask.targetTexture = colorTexture;\n fg.addTask(clearTask);\n\n // 2. Render objects\n const renderTask = new FrameGraphObjectRendererTask('sage-render', fg, scene);\n renderTask.targetTexture = clearTask.outputTexture;\n renderTask.depthTexture = clearTask.outputDepthTexture;\n renderTask.camera = camera;\n renderTask.objectList = { meshes: scene.meshes, particleSystems: scene.particleSystems };\n renderTask.isMainObjectRenderer = true;\n fg.addTask(renderTask);\n\n // Track the current output texture through the chain\n let currentTexture : FrameGraphTextureHandle = renderTask.outputTexture;\n\n // 3. SSAO (needs geometry renderer for depth/normals)\n if(config.ssao)\n {\n const geoTask = new FrameGraphGeometryRendererTask('sage-geometry', fg, scene);\n fg.addTask(geoTask);\n\n const ssaoTask = new FrameGraphSSAO2RenderingPipelineTask('sage-ssao', fg, 0.5, 1.0);\n ssaoTask.sourceTexture = currentTexture;\n ssaoTask.depthTexture = geoTask.geometryViewDepthTexture;\n ssaoTask.normalTexture = geoTask.geometryViewNormalTexture;\n ssaoTask.camera = camera;\n fg.addTask(ssaoTask);\n\n if(config.ssao.radius !== undefined) { ssaoTask.ssao.radius = config.ssao.radius; }\n if(config.ssao.samples !== undefined) { ssaoTask.ssao.samples = config.ssao.samples; }\n if(config.ssao.totalStrength !== undefined) { ssaoTask.ssao.totalStrength = config.ssao.totalStrength; }\n\n currentTexture = ssaoTask.outputTexture;\n }\n\n // 4. Bloom\n if(config.bloom)\n {\n const bloomTask = new FrameGraphBloomTask(\n 'sage-bloom',\n fg,\n config.bloom.weight ?? 0.5,\n config.bloom.kernel ?? 128,\n config.bloom.threshold ?? 0.1,\n true,\n config.bloom.scale ?? 0.5\n );\n bloomTask.sourceTexture = currentTexture;\n fg.addTask(bloomTask);\n\n currentTexture = bloomTask.outputTexture;\n }\n\n // 5. Grain\n if(config.grain)\n {\n const grainTask = new FrameGraphGrainTask('sage-grain', fg);\n grainTask.sourceTexture = currentTexture;\n fg.addTask(grainTask);\n\n if(config.grain.intensity !== undefined) { grainTask.postProcess.intensity = config.grain.intensity; }\n if(config.grain.animated !== undefined) { grainTask.postProcess.animated = config.grain.animated; }\n\n currentTexture = grainTask.outputTexture;\n }\n\n // 6. Sharpen\n if(config.sharpen)\n {\n const sharpenTask = new FrameGraphSharpenTask('sage-sharpen', fg);\n sharpenTask.sourceTexture = currentTexture;\n fg.addTask(sharpenTask);\n\n if(config.sharpen.edge !== undefined) { sharpenTask.postProcess.edgeAmount = config.sharpen.edge; }\n if(config.sharpen.color !== undefined) { sharpenTask.postProcess.colorAmount = config.sharpen.color; }\n\n currentTexture = sharpenTask.outputTexture;\n }\n\n // 7. Chromatic aberration\n if(config.chromaticAberration)\n {\n const chromTask = new FrameGraphChromaticAberrationTask('sage-chromab', fg);\n chromTask.sourceTexture = currentTexture;\n fg.addTask(chromTask);\n\n if(config.chromaticAberration.amount !== undefined)\n {\n chromTask.postProcess.aberrationAmount = config.chromaticAberration.amount;\n }\n\n currentTexture = chromTask.outputTexture;\n }\n\n // 8. Image processing (tonemap + vignette)\n if(config.tonemap || config.vignette)\n {\n const imgTask = new FrameGraphImageProcessingTask('sage-imgproc', fg);\n imgTask.sourceTexture = currentTexture;\n fg.addTask(imgTask);\n\n if(config.tonemap)\n {\n imgTask.postProcess.toneMappingEnabled = true;\n\n if(config.tonemap.operator)\n {\n imgTask.postProcess.toneMappingType\n = TONEMAP_OPERATORS[config.tonemap.operator]\n ?? ImageProcessingConfiguration.TONEMAPPING_STANDARD;\n }\n }\n\n if(config.vignette)\n {\n imgTask.postProcess.vignetteEnabled = true;\n\n if(config.vignette.weight !== undefined) { imgTask.postProcess.vignetteWeight = config.vignette.weight; }\n if(config.vignette.stretch !== undefined)\n {\n imgTask.postProcess.vignetteStretch = config.vignette.stretch;\n }\n if(config.vignette.color)\n {\n const vc = config.vignette.color;\n imgTask.postProcess.vignetteColor.set(vc.r, vc.g, vc.b, 1);\n }\n }\n\n currentTexture = imgTask.outputTexture;\n }\n\n // 9. Volumetric lighting — not yet wired up for automatic light detection\n if(config.volumetric)\n {\n console.warn(\n '[SAGE] Volumetric lighting config is recognized but automatic light detection is not yet supported. '\n + 'Use scene.frameGraph to add FrameGraphVolumetricLightingTask manually.'\n );\n }\n\n // 10. Output to screen\n const copyTask = new FrameGraphCopyToBackbufferColorTask('sage-output', fg);\n copyTask.sourceTexture = currentTexture;\n fg.addTask(copyTask);\n\n // Wire up the scene — cameraToUseForPointers is needed because activeCamera becomes null between tasks\n scene.cameraToUseForPointers = camera;\n scene.frameGraph = fg;\n\n // Rebuild on resize\n engine.onResizeObservable.add(async () =>\n {\n await fg.buildAsync();\n });\n\n // Initial build\n await fg.buildAsync();\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Post-Processing Dispatcher\n//\n// Routes to either the DefaultRenderingPipeline backend or the Frame Graph backend\n// based on config.renderer.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { Scene } from '@babylonjs/core';\n\nimport type { PostProcessingConfig } from '../interfaces/level.ts';\n\nimport { applyPipelinePostProcessing } from './postProcessingPipeline.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport async function applyPostProcessing(scene : Scene, config : PostProcessingConfig) : Promise<void>\n{\n if(config.renderer === 'frameGraph')\n {\n const { applyFrameGraphPostProcessing } = await import('./postProcessingFrameGraph.ts');\n await applyFrameGraphPostProcessing(scene, config);\n }\n else\n {\n applyPipelinePostProcessing(scene, config);\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Abstract Level Class\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { Scene } from '@babylonjs/core';\nimport { ClusteredLightContainer } from '@babylonjs/core/Lights/Clustered/clusteredLightContainer';\n\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type {\n LevelConfig,\n LevelContext,\n LevelInstance,\n PropertyHandler,\n} from '../interfaces/level.ts';\n\nimport { SAGELogger } from '../utils/logger.ts';\nimport type { GameEngine } from './gameEngine.ts';\nimport { OutlineManager } from '../managers/outline.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Abstract base class for game levels.\n *\n * Levels are responsible for creating and configuring their own scenes via the\n * `buildScene()` method. All runtime dependencies (eventBus, sceneEngine, entityManager)\n * are injected via the constructor and are immediately available.\n *\n * Users should not instantiate Level subclasses directly. Instead, use LevelManager\n * which acts as a factory and injects the required dependencies:\n *\n * ```typescript\n * // Register a custom level class\n * levelManager.registerLevelClass('my-level', MyCustomLevel);\n *\n * // Load the level - manager creates instance with deps injected\n * const level = await levelManager.loadLevel({ name: 'Level1', class: 'my-level' });\n * ```\n */\nexport abstract class Level implements LevelInstance\n{\n public readonly name : string;\n protected readonly _log : LoggerInterface;\n protected readonly _context : LevelContext;\n protected _scene : Scene | null = null;\n public clusteredLights : ClusteredLightContainer | null = null;\n public outlines : OutlineManager | null = null;\n\n //------------------------------------------------------------------------------------------------------------------\n // Constructor\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Creates a new Level instance.\n *\n * @param config\n * @param context\n */\n constructor(config : LevelConfig, context : LevelContext)\n {\n this.name = config.name;\n this._context = context;\n this._log = context.logger?.getLogger(`Level:${ config.name }`)\n ?? new SAGELogger(`Level:${ config.name }`, 'info');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Accessors\n //------------------------------------------------------------------------------------------------------------------\n\n get scene() : Scene | null\n {\n return this._scene;\n }\n\n get isLoaded() : boolean\n {\n return this._scene !== null;\n }\n\n get gameEngine() : GameEngine\n {\n return this._context.gameEngine;\n }\n\n get propertyHandlers() : Map<string, PropertyHandler>\n {\n return this._context.propertyHandlers;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Protected Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Emit a progress event during loading\n *\n * @param progress - Percentage (0-100)\n * @param message\n */\n protected $emitProgress(progress : number, message : string) : void\n {\n this.gameEngine.eventBus.publish({\n type: 'level:progress',\n payload: {\n levelName: this.name,\n progress,\n message,\n },\n });\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Load and create the scene for this level.\n * Progress events will be emitted during loading.\n */\n async load() : Promise<Scene>\n {\n if(this._scene)\n {\n this._log.warn(`Level ${ this.name } is already loaded`);\n return this._scene;\n }\n\n this._log.info(`Loading level: ${ this.name }`);\n this._log.time(`level-${ this.name }-load`);\n\n try\n {\n this.$emitProgress(0, 'Starting level load...');\n\n // Let the concrete level build its content\n this._scene = await this.buildScene();\n\n this.$emitProgress(100, 'Level loaded successfully');\n this.gameEngine.eventBus.publish({\n type: 'level:complete',\n payload: {\n levelName: this.name,\n message: 'Level loaded successfully',\n level: this,\n },\n });\n\n this._log.timeEnd(`level-${ this.name }-load`);\n this._log.info(`Level ${ this.name } loaded successfully`);\n\n return this._scene;\n }\n catch (error)\n {\n this._log.error(`Failed to load level ${ this.name }:`, error);\n this.gameEngine.eventBus.publish({\n type: 'level:error',\n payload: {\n levelName: this.name,\n message: 'Failed to load level',\n error,\n },\n });\n throw error;\n }\n }\n\n /**\n * Abstract method for building the scene content.\n * Concrete levels must implement this to create their specific content.\n *\n * All dependencies are available via `this.gameEngine`.\n *\n */\n protected abstract buildScene() : Promise<Scene>;\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Called when this level becomes the active level during a transition.\n * Override in subclasses to perform activation logic (e.g., start music, show UI).\n * Default implementation is a no-op.\n */\n onActivate?() : Promise<void> | void;\n\n /**\n * Called when this level is being replaced during a transition.\n * Override in subclasses to perform cleanup logic (e.g., fade out, save state).\n * Default implementation is a no-op.\n */\n onDeactivate?() : Promise<void> | void;\n\n /**\n * Dispose of this level's resources and scene.\n *\n * The `$` prefix indicates this is an internal lifecycle method, consistent with\n * other engine lifecycle methods like `$teardown()`.\n */\n async $dispose() : Promise<void>\n {\n if(this._scene)\n {\n this._log.info(`Disposing level: ${ this.name }`);\n\n // Detach all entities from this scene's nodes BEFORE disposing the scene.\n // Behaviors need the scene alive to clean up physics bodies, cameras, etc.\n const entityManager = this._context.gameEngine?.managers?.entityManager;\n if(entityManager?.getAllEntities)\n {\n for(const entity of entityManager.getAllEntities())\n {\n if(entity.node && entity.node.getScene() === this._scene)\n {\n entityManager.detachFromNode(entity);\n }\n }\n }\n\n if(this.outlines)\n {\n this.outlines.dispose();\n this.outlines = null;\n }\n\n if(this.clusteredLights)\n {\n this.clusteredLights.dispose();\n this.clusteredLights = null;\n }\n\n this._scene.dispose();\n this._scene = null;\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Outline Manager\n//\n// Manages named SelectionOutlineLayer instances for entity highlighting.\n// Each layer tracks a Set<GameEntity> and rebuilds its selection on any change.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { Color3, type Scene, SelectionOutlineLayer } from '@babylonjs/core';\n\nimport type { GameEntity } from '../classes/entity.ts';\nimport type { OutlineLayerConfig } from '../interfaces/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface OutlineLayerEntry\n{\n layer : SelectionOutlineLayer;\n entities : Set<GameEntity>;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class OutlineManager\n{\n private _scene : Scene;\n private _layers = new Map<string, OutlineLayerEntry>();\n\n constructor(scene : Scene, configs ?: Record<string, OutlineLayerConfig>)\n {\n this._scene = scene;\n\n if(configs)\n {\n for(const [ name, config ] of Object.entries(configs))\n {\n this.create(name, config);\n }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n get(name : string) : SelectionOutlineLayer | undefined\n {\n return this._layers.get(name)?.layer;\n }\n\n create(name : string, config ?: OutlineLayerConfig) : SelectionOutlineLayer\n {\n const existing = this._layers.get(name);\n if(existing)\n {\n return existing.layer;\n }\n\n const layer = new SelectionOutlineLayer(`sage-outline-${ name }`, this._scene);\n\n if(config?.color)\n {\n layer.outlineColor = new Color3(config.color.r, config.color.g, config.color.b);\n }\n\n if(config?.thickness !== undefined) { layer.outlineThickness = config.thickness; }\n if(config?.occlusionStrength !== undefined) { layer.occlusionStrength = config.occlusionStrength; }\n\n this._layers.set(name, { layer, entities: new Set() });\n return layer;\n }\n\n highlightEntity(layerName : string, entity : GameEntity) : void\n {\n const entry = this._layers.get(layerName);\n if(!entry)\n {\n return;\n }\n\n entry.entities.add(entity);\n this._rebuildSelection(entry);\n }\n\n unhighlightEntity(layerName : string, entity : GameEntity) : void\n {\n const entry = this._layers.get(layerName);\n if(!entry)\n {\n return;\n }\n\n entry.entities.delete(entity);\n this._rebuildSelection(entry);\n }\n\n unhighlightEntityAll(entity : GameEntity) : void\n {\n for(const entry of this._layers.values())\n {\n if(entry.entities.delete(entity))\n {\n this._rebuildSelection(entry);\n }\n }\n }\n\n clear(layerName : string) : void\n {\n const entry = this._layers.get(layerName);\n if(!entry)\n {\n return;\n }\n\n entry.entities.clear();\n entry.layer.clearSelection();\n }\n\n dispose() : void\n {\n for(const entry of this._layers.values())\n {\n entry.entities.clear();\n entry.layer.dispose();\n }\n\n this._layers.clear();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private\n //------------------------------------------------------------------------------------------------------------------\n\n private _rebuildSelection(entry : OutlineLayerEntry) : void\n {\n entry.layer.clearSelection();\n\n for(const entity of entry.entities)\n {\n if(entity.node)\n {\n const meshes = entity.node.getChildMeshes();\n if(meshes.length > 0)\n {\n entry.layer.addSelection(meshes);\n }\n }\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// GameLevel Class - Default Level Implementation\n//----------------------------------------------------------------------------------------------------------------------\n\nimport {\n type AbstractMesh,\n ArcRotateCamera,\n type Camera,\n Color3,\n CubeTexture,\n DirectionalLight,\n FreeCamera,\n HDRCubeTexture,\n HemisphericLight,\n MeshBuilder,\n PBRMaterial,\n PointLight,\n RectAreaLight,\n Scene,\n SoundState,\n SpotLight,\n StandardMaterial,\n type StaticSound,\n Texture,\n TransformNode,\n UniversalCamera,\n Vector3,\n} from '@babylonjs/core';\nimport { GeospatialCamera } from '@babylonjs/core/Cameras/geospatialCamera';\nimport { ClusteredLightContainer } from '@babylonjs/core/Lights/Clustered/clusteredLightContainer';\n\nimport type {\n CameraDefinition,\n ColorConfig,\n LevelConfig,\n LevelContext,\n LightDefinition,\n SpawnDefinition,\n Vec3Config,\n} from '../interfaces/level.ts';\nimport { applyPostProcessing } from '../handlers/postProcessing.ts';\n\nimport type { GameEntity } from './entity.ts';\nimport { Level } from './level.ts';\nimport { OutlineManager } from '../managers/outline.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Metadata collected from a spawn point node\n */\ninterface SpawnPointData\n{\n name : string;\n position : Vector3;\n rotation : Vector3;\n scaling : Vector3;\n node : TransformNode;\n}\n\n/**\n * Metadata collected from an entity node\n */\ninterface EntityNodeData\n{\n type : string;\n node : TransformNode;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Default Level implementation that loads from YAML configuration.\n * Supports property handlers for processing scene node metadata.\n */\nexport class GameLevel extends Level\n{\n /** The level configuration */\n protected _config : LevelConfig;\n\n /** Collected spawn points from the scene */\n protected _spawnPoints : SpawnPointData[] = [];\n\n /** Collected entity nodes from the scene */\n protected _entityNodes : EntityNodeData[] = [];\n\n /** Entities spawned by this level */\n protected _spawnedEntities : GameEntity[] = [];\n\n /** Level-scoped sounds created from config */\n protected _levelSounds = new Map<string, StaticSound>();\n\n /** Sounds that were playing before deactivation (for resume on activate) */\n private _playingSoundsBeforeDeactivate = new Set<string>();\n\n //------------------------------------------------------------------------------------------------------------------\n // Constructor\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a GameLevel from a configuration object\n *\n * @param config\n * @param context\n */\n constructor(config : LevelConfig, context : LevelContext)\n {\n super(config, context);\n this._config = config;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Accessors\n //------------------------------------------------------------------------------------------------------------------\n\n get config() : LevelConfig\n {\n return this._config;\n }\n\n get spawnedEntities() : readonly GameEntity[]\n {\n return this._spawnedEntities;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Protected Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Build the scene by loading assets, processing node metadata, and spawning entities.\n */\n protected async buildScene() : Promise<Scene>\n {\n const { sceneEngine } = this.gameEngine.engines;\n const { assetManager } = this.gameEngine.managers;\n\n // Preload assets if configured\n if(this._config.preload && this._config.preload.length > 0)\n {\n this.$emitProgress(2, 'Preloading assets...');\n await assetManager.preload(this._config.preload);\n }\n\n // Create an empty scene and assign it early so property handlers can access it\n const scene = sceneEngine.createScene();\n this._scene = scene;\n\n // Enable physics if configured (must be done before loading scene with colliders)\n if(this._config.physics)\n {\n this.$emitProgress(5, 'Initializing physics...');\n await this._enablePhysics(scene);\n }\n\n // Load the scene file if specified\n if(this._config.scene)\n {\n this.$emitProgress(10, 'Loading scene file...');\n await this._loadSceneFile(scene);\n }\n\n // Set up environment (skybox + IBL) — before cameras so the background is ready\n if(this._config.environment)\n {\n this.$emitProgress(15, 'Setting up environment...');\n this._processEnvironment(scene);\n }\n\n // Process cameras (must happen before post-processing, which requires activeCamera)\n this._processCameras(scene);\n\n // Process lights (override Blender-imported lights, create new ones)\n this._processLights(scene);\n\n // Set up clustered lighting if configured (must be after _processLights)\n this._setupClustering(scene);\n\n // Apply post-processing effects if configured\n if(this._config.postProcessing)\n {\n const renderer = this._config.postProcessing.renderer ?? 'pipeline';\n this._log.info(`Applying post-processing with ${ renderer } renderer`);\n await applyPostProcessing(scene, this._config.postProcessing);\n }\n\n // Create outline manager for entity highlighting\n this.outlines = new OutlineManager(scene, this._config.outlines);\n\n // Process all nodes for property handlers\n this.$emitProgress(50, 'Processing scene properties...');\n await this._processNodeProperties(scene);\n\n // Process spawn points\n this.$emitProgress(70, 'Spawning entities...');\n await this._processSpawnPoints();\n\n // Process entity nodes\n this.$emitProgress(85, 'Configuring entities...');\n await this._processEntityNodes();\n\n // Load level-scoped sounds (last — everything else is ready)\n if(this._config.sounds)\n {\n this.$emitProgress(95, 'Loading sounds...');\n await this._processLevelSounds();\n }\n\n return scene;\n }\n\n /**\n * Import all meshes from the configured scene file (GLB/GLTF/Babylon) into the scene.\n */\n private async _loadSceneFile(scene : Scene) : Promise<void>\n {\n const scenePath = this._getScenePath();\n if(!scenePath)\n {\n return;\n }\n\n this._log.debug(`Loading scene file: ${ scenePath }`);\n\n // Set right-handed coordinate system before loading so loaders respect it\n if(this._isRightHanded())\n {\n scene.useRightHandedSystem = true;\n this._log.debug('Scene set to right-handed coordinate system');\n }\n\n try\n {\n await this.gameEngine.engines.sceneEngine.importMeshes([], scenePath, scene);\n this._log.debug('Scene file loaded successfully');\n }\n catch (error)\n {\n this._log.error(`Failed to load scene file: ${ scenePath }`, error);\n throw error;\n }\n }\n\n /**\n * Extract the scene file path from the config, which can be a string or SceneConfig object.\n */\n private _getScenePath() : string | undefined\n {\n if(!this._config.scene)\n {\n return undefined;\n }\n\n return typeof this._config.scene === 'string'\n ? this._config.scene\n : this._config.scene.path;\n }\n\n /**\n * Check if the scene config specifies a right-handed coordinate system.\n */\n private _isRightHanded() : boolean\n {\n if(!this._config.scene || typeof this._config.scene === 'string')\n {\n return false;\n }\n\n return this._config.scene.rightHanded === true;\n }\n\n /**\n * Set up the scene environment: IBL (image-based lighting) for PBR reflections and/or a visible skybox.\n */\n private _processEnvironment(scene : Scene) : void\n {\n const env = this._config.environment;\n if(!env) { return; }\n\n // Set up IBL for PBR reflections\n if(env.ibl)\n {\n scene.environmentTexture = this._createEnvironmentTexture(env.ibl, env.iblResolution ?? 256, scene);\n this._log.debug(`IBL set from: ${ env.ibl }`);\n }\n\n // Set up visible skybox\n if(env.skybox)\n {\n const ext = this._getFileExtension(env.skybox);\n\n if(ext === '.hdr' || ext === '.env')\n {\n // HDR/env skybox — use as IBL too if no separate IBL was specified\n if(!env.ibl)\n {\n const res = env.iblResolution ?? 256;\n scene.environmentTexture = this._createEnvironmentTexture(env.skybox, res, scene);\n }\n\n if(scene.environmentTexture)\n {\n scene.createDefaultSkybox(scene.environmentTexture, true, env.skyboxSize ?? 1000, 0, false);\n }\n }\n else\n {\n // JPG/PNG — create a textured sphere\n const size = env.skyboxSize ?? 1000;\n const skybox = MeshBuilder.CreateSphere('skybox', { diameter: size, segments: 32 }, scene);\n\n const mat = new StandardMaterial('skybox-mat', scene);\n mat.backFaceCulling = false;\n mat.disableLighting = true;\n mat.emissiveTexture = new Texture(env.skybox, scene);\n skybox.material = mat;\n skybox.infiniteDistance = true;\n }\n\n this._log.debug(`Skybox set from: ${ env.skybox }`);\n }\n\n // Apply rotation to environment texture\n if(env.rotation !== undefined && scene.environmentTexture)\n {\n (scene.environmentTexture as HDRCubeTexture).rotationY = env.rotation;\n }\n }\n\n /**\n * Create an environment texture from an HDR or env file.\n */\n private _createEnvironmentTexture(path : string, resolution : number, scene : Scene) : CubeTexture | HDRCubeTexture\n {\n const ext = this._getFileExtension(path);\n\n if(ext === '.env')\n {\n return CubeTexture.CreateFromPrefilteredData(path, scene);\n }\n\n return new HDRCubeTexture(path, scene, resolution);\n }\n\n private _getFileExtension(path : string) : string\n {\n const dotIndex = path.lastIndexOf('.');\n return dotIndex >= 0 ? path.slice(dotIndex).toLowerCase() : '';\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Process cameras: apply YAML overrides to imported cameras, create new ones, and set the active camera.\n * If no YAML cameras config exists, the first imported camera (if any) is activated.\n */\n private _processCameras(scene : Scene) : void\n {\n const cameraConfigs = this._config.cameras;\n const canvas = this.gameEngine.canvas as HTMLCanvasElement | null;\n\n if(!cameraConfigs)\n {\n // No YAML config — if the scene has imported cameras, activate the first one\n if(scene.cameras.length > 0)\n {\n scene.activeCamera = scene.cameras[0];\n\n if(canvas)\n {\n scene.activeCamera.attachControl(canvas, true);\n }\n\n this._log.debug(`Activated imported camera: \"${ scene.activeCamera.name }\"`);\n }\n\n return;\n }\n\n let activeCamera : Camera | null = null;\n\n for(const [ name, config ] of Object.entries(cameraConfigs))\n {\n const existing = scene.getCameraByName(name);\n\n if(existing)\n {\n this._applyCameraConfig(existing, config);\n this._log.debug(`Applied overrides to camera: \"${ name }\"`);\n\n if(!activeCamera || config.active)\n {\n activeCamera = existing;\n }\n }\n else if(config.type)\n {\n const camera = this._createCamera(name, config, scene);\n this._log.debug(`Created ${ config.type } camera: \"${ name }\"`);\n\n if(!activeCamera || config.active)\n {\n activeCamera = camera;\n }\n }\n else\n {\n this._log.warn(\n `Camera \"${ name }\" not found in scene and no type specified — skipping`\n );\n }\n }\n\n if(activeCamera)\n {\n scene.activeCamera = activeCamera;\n\n const activeDef = cameraConfigs[activeCamera.name];\n if(canvas && activeDef?.attachControl !== false)\n {\n activeCamera.attachControl(canvas, true);\n }\n }\n }\n\n /**\n * Create a new camera from a YAML definition.\n */\n private _createCamera(name : string, config : CameraDefinition, scene : Scene) : Camera\n {\n const pos = config.position ? this._toVector3(config.position) : Vector3.Zero();\n\n let camera : Camera;\n\n switch (config.type)\n {\n case 'arcRotate':\n {\n const target = config.target ? this._toVector3(config.target) : Vector3.Zero();\n\n camera = new ArcRotateCamera(\n name,\n config.alpha ?? Math.PI / 2,\n config.beta ?? Math.PI / 3,\n config.radius ?? 10,\n target,\n scene\n );\n break;\n }\n\n case 'universal':\n {\n camera = new UniversalCamera(name, pos, scene);\n break;\n }\n\n case 'geospatial':\n {\n if(!config.planetRadius)\n {\n this._log.warn(`Geospatial camera \"${ name }\" requires planetRadius -- skipping`);\n return new FreeCamera(name, Vector3.Zero(), scene);\n }\n\n const cam = new GeospatialCamera(name, scene, {\n planetRadius: config.planetRadius,\n });\n\n if(config.center) { cam.center = this._toVector3(config.center); }\n if(config.yaw !== undefined) { cam.yaw = config.yaw; }\n if(config.pitch !== undefined) { cam.pitch = config.pitch; }\n if(config.radius !== undefined) { cam.radius = config.radius; }\n if(config.checkCollisions !== undefined) { cam.checkCollisions = config.checkCollisions; }\n\n if(config.radiusMin !== undefined) { cam.limits.radiusMin = config.radiusMin; }\n if(config.radiusMax !== undefined) { cam.limits.radiusMax = config.radiusMax; }\n if(config.pitchMin !== undefined) { cam.limits.pitchMin = config.pitchMin; }\n if(config.pitchMax !== undefined) { cam.limits.pitchMax = config.pitchMax; }\n if(config.yawMin !== undefined) { cam.limits.yawMin = config.yawMin; }\n if(config.yawMax !== undefined) { cam.limits.yawMax = config.yawMax; }\n\n camera = cam;\n break;\n }\n\n case 'free':\n default:\n {\n camera = new FreeCamera(name, pos, scene);\n break;\n }\n }\n\n this._applyCameraConfig(camera, config);\n return camera;\n }\n\n /**\n * Apply shared camera properties from a config to an existing camera.\n */\n private _applyCameraConfig(camera : Camera, config : CameraDefinition) : void\n {\n if(config.fov !== undefined) { camera.fov = config.fov; }\n if(config.position) { camera.position = this._toVector3(config.position); }\n if(config.minZ !== undefined) { camera.minZ = config.minZ; }\n if(config.maxZ !== undefined) { camera.maxZ = config.maxZ; }\n\n if(camera instanceof FreeCamera)\n {\n if(config.speed !== undefined) { camera.speed = config.speed; }\n if(config.rotation) { camera.rotation = this._toVector3(config.rotation); }\n }\n\n if(camera instanceof ArcRotateCamera)\n {\n if(config.target) { camera.setTarget(this._toVector3(config.target)); }\n if(config.lowerRadiusLimit !== undefined) { camera.lowerRadiusLimit = config.lowerRadiusLimit; }\n if(config.upperRadiusLimit !== undefined) { camera.upperRadiusLimit = config.upperRadiusLimit; }\n if(config.lowerBetaLimit !== undefined) { camera.lowerBetaLimit = config.lowerBetaLimit; }\n if(config.upperBetaLimit !== undefined) { camera.upperBetaLimit = config.upperBetaLimit; }\n if(config.wheelPrecision !== undefined) { camera.wheelPrecision = config.wheelPrecision; }\n }\n\n if(camera instanceof GeospatialCamera)\n {\n if(config.yaw !== undefined) { camera.yaw = config.yaw; }\n if(config.pitch !== undefined) { camera.pitch = config.pitch; }\n if(config.radius !== undefined) { camera.radius = config.radius; }\n if(config.center) { camera.center = this._toVector3(config.center); }\n if(config.checkCollisions !== undefined) { camera.checkCollisions = config.checkCollisions; }\n if(config.radiusMin !== undefined) { camera.limits.radiusMin = config.radiusMin; }\n if(config.radiusMax !== undefined) { camera.limits.radiusMax = config.radiusMax; }\n if(config.pitchMin !== undefined) { camera.limits.pitchMin = config.pitchMin; }\n if(config.pitchMax !== undefined) { camera.limits.pitchMax = config.pitchMax; }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Process lights: apply YAML overrides to imported lights and create new ones.\n * If no YAML lights config exists, imported lights are left unchanged.\n */\n private _processLights(scene : Scene) : void\n {\n const lightConfigs = this._config.lights;\n if(!lightConfigs) { return; }\n\n for(const [ name, config ] of Object.entries(lightConfigs))\n {\n const existing = scene.getLightByName(name);\n\n if(existing)\n {\n this._applyLightConfig(existing, config);\n this._log.debug(`Applied overrides to light: \"${ name }\"`);\n }\n else if(config.type)\n {\n this._createLight(name, config, scene);\n this._log.debug(`Created ${ config.type } light: \"${ name }\"`);\n }\n else\n {\n this._log.warn(\n `Light \"${ name }\" not found in scene and no type specified — skipping`\n );\n }\n }\n }\n\n /**\n * Create a new light from a YAML definition.\n */\n private _createLight(name : string, config : LightDefinition, scene : Scene) : void\n {\n const dir = config.direction ? this._toVector3(config.direction) : new Vector3(0, -1, 0);\n const pos = config.position ? this._toVector3(config.position) : Vector3.Zero();\n\n switch (config.type)\n {\n case 'hemispheric':\n {\n const light = new HemisphericLight(name, dir, scene);\n\n if(config.groundColor)\n {\n light.groundColor = this._toColor3(config.groundColor);\n }\n\n this._applyLightConfig(light, config);\n break;\n }\n\n case 'directional':\n {\n const light = new DirectionalLight(name, dir, scene);\n this._applyLightConfig(light, config);\n break;\n }\n\n case 'point':\n {\n const light = new PointLight(name, pos, scene);\n this._applyLightConfig(light, config);\n break;\n }\n\n case 'spot':\n {\n const light = new SpotLight(\n name,\n pos,\n dir,\n config.angle ?? Math.PI / 3,\n config.exponent ?? 2,\n scene\n );\n this._applyLightConfig(light, config);\n break;\n }\n\n case 'rectarea':\n {\n const light = new RectAreaLight(\n name,\n pos,\n config.width ?? 1,\n config.height ?? 1,\n scene\n );\n this._applyLightConfig(light, config);\n\n // RectAreaLight emits along local -Z and has no direction property. If a direction\n // is specified, parent the light to a rotated pivot node that orients -Z toward it.\n if(config.direction)\n {\n const pivot = new TransformNode(`${ name }_pivot`, scene);\n pivot.position = light.position.clone();\n light.position = Vector3.Zero();\n light.parent = pivot;\n\n // lookAt aligns +Z toward the target; we want -Z toward dir, so look opposite\n pivot.lookAt(pivot.position.subtract(dir));\n }\n\n break;\n }\n }\n }\n\n /**\n * Apply shared light properties from a config to an existing light.\n */\n private _applyLightConfig(\n light : { intensity : number; diffuse : Color3; specular : Color3 },\n config : LightDefinition\n ) : void\n {\n if(config.intensity !== undefined) { light.intensity = config.intensity; }\n if(config.diffuse) { light.diffuse = this._toColor3(config.diffuse); }\n if(config.specular) { light.specular = this._toColor3(config.specular); }\n }\n\n /**\n * Set up clustered lighting: create a container, move eligible lights into it,\n * and fix PBR material falloff for compatibility.\n */\n private _setupClustering(scene : Scene) : void\n {\n const clusterConfig = this._config.clustering;\n if(!clusterConfig?.enabled)\n {\n return;\n }\n\n // Collect eligible lights from the scene\n const eligibleLights = scene.lights.filter(\n (light) => ClusteredLightContainer.IsLightSupported(light)\n );\n\n if(eligibleLights.length === 0)\n {\n this._log.debug('Clustering enabled but no eligible lights found');\n return;\n }\n\n // Remove eligible lights from the scene before adding to container\n for(const light of eligibleLights)\n {\n scene.removeLight(light);\n }\n\n // Create the container with collected lights\n const container = new ClusteredLightContainer('sage-clustered', eligibleLights, scene);\n\n // Apply tuning parameters\n if(clusterConfig.horizontalTiles !== undefined) { container.horizontalTiles = clusterConfig.horizontalTiles; }\n if(clusterConfig.verticalTiles !== undefined) { container.verticalTiles = clusterConfig.verticalTiles; }\n if(clusterConfig.depthSlices !== undefined) { container.depthSlices = clusterConfig.depthSlices; }\n if(clusterConfig.maxRange !== undefined) { container.maxRange = clusterConfig.maxRange; }\n\n this.clusteredLights = container;\n\n this._log.debug(\n `Clustered lighting enabled: ${ eligibleLights.length } lights, `\n + `${ container.horizontalTiles }x${ container.verticalTiles } tiles, `\n + `${ container.depthSlices } depth slices`\n );\n\n // Fix PBR material falloff — physical falloff is incompatible with clustering\n for(const material of scene.materials)\n {\n if(material instanceof PBRMaterial)\n {\n material.useGLTFLightFalloff = true;\n }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n private _toVector3(vec : Vec3Config) : Vector3\n {\n return new Vector3(vec.x, vec.y, vec.z);\n }\n\n private _toColor3(color : ColorConfig) : Color3\n {\n return new Color3(color.r, color.g, color.b);\n }\n\n /**\n * Configure and enable the Havok physics plugin with the level's gravity settings.\n */\n private async _enablePhysics(scene : Scene) : Promise<void>\n {\n const physicsConfig = this._config.physics;\n\n // Determine gravity\n let gravity = new Vector3(0, -9.81, 0); // Default gravity\n\n if(typeof physicsConfig === 'object' && physicsConfig.gravity)\n {\n gravity = new Vector3(\n physicsConfig.gravity.x,\n physicsConfig.gravity.y,\n physicsConfig.gravity.z\n );\n }\n\n // When large world rendering is active, enable Havok multi-region physics\n const floatingOriginRadius = this.gameEngine.largeWorldRendering\n ? 100000\n : undefined;\n\n this._log.debug(`Enabling physics with gravity: ${ gravity.toString() }`);\n await this.gameEngine.engines.sceneEngine.enablePhysics(scene, gravity, floatingOriginRadius);\n }\n\n /**\n * Walk all transform nodes and meshes, normalizing glTF metadata and dispatching to property handlers.\n */\n private async _processNodeProperties(scene : Scene) : Promise<void>\n {\n const nodes = scene.transformNodes.concat(scene.meshes as TransformNode[]);\n\n for(const node of nodes)\n {\n // Normalize metadata - glTF extras are in node.metadata.gltf.extras\n const metadata = this._getNormalizedMetadata(node);\n\n if(metadata && Object.keys(metadata).length > 0)\n {\n // Store normalized metadata back on node for property handlers\n node.metadata = { ...node.metadata, ...metadata };\n\n // eslint-disable-next-line no-await-in-loop\n await this._processNodeMetadata(node);\n }\n }\n\n this._log.debug(\n `Processed ${ nodes.length } nodes, found ${ this._spawnPoints.length } spawn points `\n + `and ${ this._entityNodes.length } entity nodes`\n );\n }\n\n /**\n * Get normalized metadata from a node, handling glTF extras.\n *\n * Babylon.js stores glTF custom properties in node.metadata.gltf.extras,\n * so we need to extract them for easier access.\n */\n private _getNormalizedMetadata(node : TransformNode) : Record<string, unknown> | null\n {\n if(!node.metadata)\n {\n return null;\n }\n\n // Check for glTF extras (from imported GLB/GLTF files)\n const gltfExtras = node.metadata?.gltf?.extras as Record<string, unknown> | undefined;\n\n if(gltfExtras)\n {\n // Merge glTF extras with any existing top-level metadata\n return { ...gltfExtras };\n }\n\n // Return existing metadata if no glTF extras\n return node.metadata as Record<string, unknown>;\n }\n\n /**\n * Check a node's metadata for spawn/entity markers and run any registered property handlers.\n */\n private async _processNodeMetadata(node : TransformNode) : Promise<void>\n {\n // Check for spawn property\n if('spawn' in node.metadata)\n {\n this._spawnPoints.push({\n name: node.metadata.spawn as string,\n position: node.position.clone(),\n rotation: node.rotation.clone(),\n scaling: node.scaling.clone(),\n node,\n });\n }\n\n // Check for entity property\n if('entity' in node.metadata)\n {\n this._entityNodes.push({\n type: node.metadata.entity as string,\n node,\n });\n }\n\n // Run registered property handlers\n for(const [ property, handler ] of this.propertyHandlers)\n {\n if(property in node.metadata)\n {\n try\n {\n // eslint-disable-next-line no-await-in-loop\n await handler(node, node.metadata[property], this, this.gameEngine);\n }\n catch (error)\n {\n this._log.error(\n `Error in property handler '${ property }' for node '${ node.name }':`,\n error\n );\n }\n }\n }\n }\n\n /**\n * Match collected spawn points against the level config's spawn definitions and instantiate entities.\n */\n private async _processSpawnPoints() : Promise<void>\n {\n for(const spawnPoint of this._spawnPoints)\n {\n const spawnDef = this._config.spawns?.[spawnPoint.name];\n\n if(!spawnDef)\n {\n this._log.warn(`No spawn definition found for '${ spawnPoint.name }', skipping`);\n }\n else\n {\n // eslint-disable-next-line no-await-in-loop\n await this._processSpawnPoint(spawnPoint, spawnDef);\n }\n }\n }\n\n /**\n * Spawn an entity at the given spawn point and dispose of the placeholder node.\n */\n private async _processSpawnPoint(spawnPoint : SpawnPointData, spawnDef : SpawnDefinition) : Promise<void>\n {\n try\n {\n const entity = await this._spawnEntity(spawnDef, spawnPoint);\n this._spawnedEntities.push(entity);\n\n // Remove the placeholder node from the scene\n spawnPoint.node.dispose();\n\n this._log.debug(`Spawned entity '${ spawnDef.entity }' at spawn point '${ spawnPoint.name }'`);\n }\n catch (error)\n {\n this._log.error(`Failed to spawn entity at spawn point '${ spawnPoint.name }':`, error);\n }\n }\n\n /**\n * Create an entity from a spawn definition, inheriting position/rotation/scaling from the spawn point.\n */\n private async _spawnEntity(spawnDef : SpawnDefinition, spawnPoint : SpawnPointData) : Promise<GameEntity>\n {\n const initialState = {\n ...spawnDef.config,\n position: this._vectorToObject(spawnPoint.position),\n rotation: this._vectorToObject(spawnPoint.rotation),\n scaling: this._vectorToObject(spawnPoint.scaling),\n };\n\n const entity = await this.gameEngine.managers.entityManager.createEntity(spawnDef.entity, {\n name: spawnDef.name,\n tags: spawnDef.tags,\n initialState,\n });\n\n // Create mesh if the entity definition specifies one\n await this._createEntityMesh(entity, spawnPoint);\n\n return entity;\n }\n\n /**\n * Converts a Vector3 to a plain `{ x, y, z }` object for serialization into entity state.\n */\n private _vectorToObject(vector : Vector3) : { x : number; y : number; z : number }\n {\n return { x: vector.x, y: vector.y, z: vector.z };\n }\n\n /**\n * Build or import a mesh from the entity definition and attach it to the entity.\n * Supports primitive shapes (box/sphere/capsule/cylinder) and GLB/GLTF file paths.\n */\n private async _createEntityMesh(entity : GameEntity, spawnPoint : SpawnPointData) : Promise<void>\n {\n if(!this._scene)\n {\n return;\n }\n\n // Get the entity definition to check for mesh config\n const definition = this.gameEngine.managers.entityManager.getDefinition(entity.type);\n if(!definition?.mesh)\n {\n return;\n }\n\n const meshConfig = definition.mesh;\n const scene = this._scene;\n\n // Create a transform node for the entity\n const node = new TransformNode(`entity-${ entity.id }`, scene);\n node.position.copyFrom(spawnPoint.position);\n node.rotation.copyFrom(spawnPoint.rotation);\n\n // Create or import meshes based on source type\n let meshes : AbstractMesh[];\n\n switch (meshConfig.source)\n {\n case 'box':\n meshes = [ MeshBuilder.CreateBox(`${ entity.type }-mesh`, {\n size: meshConfig.params?.size ?? 1,\n width: meshConfig.params?.width,\n height: meshConfig.params?.height,\n depth: meshConfig.params?.depth,\n }, scene) ];\n break;\n\n case 'sphere':\n meshes = [ MeshBuilder.CreateSphere(`${ entity.type }-mesh`, {\n diameter: meshConfig.params?.diameter ?? 1,\n segments: meshConfig.params?.segments ?? 16,\n }, scene) ];\n break;\n\n case 'capsule':\n meshes = [ MeshBuilder.CreateCapsule(`${ entity.type }-mesh`, {\n height: meshConfig.params?.height ?? 1.8,\n radius: meshConfig.params?.radius ?? 0.4,\n }, scene) ];\n break;\n\n case 'cylinder':\n meshes = [ MeshBuilder.CreateCylinder(`${ entity.type }-mesh`, {\n height: meshConfig.params?.height ?? 1,\n diameter: meshConfig.params?.diameter ?? 1,\n }, scene) ];\n break;\n\n default:\n {\n // GLB/GLTF file path — import all meshes from the file\n const result = await this.gameEngine.engines.sceneEngine.importMeshes([], meshConfig.source, scene);\n\n if(result.meshes.length === 0)\n {\n this._log.warn(`No meshes loaded from: ${ meshConfig.source }`);\n node.dispose();\n return;\n }\n\n meshes = result.meshes;\n\n // Re-parent root-level imported transform nodes (e.g. __root__) under the entity node.\n // This preserves the GLB's internal hierarchy and coordinate conversion.\n for(const tn of result.transformNodes as TransformNode[])\n {\n if(!tn.parent)\n {\n tn.parent = node;\n }\n }\n\n break;\n }\n }\n\n // Parent any orphan meshes under the entity node.\n // Primitives are always orphans; GLB meshes are already parented in their hierarchy.\n for(const mesh of meshes)\n {\n if(!mesh.parent)\n {\n mesh.parent = node;\n }\n }\n\n // Apply scale\n if(meshConfig.scale)\n {\n if(typeof meshConfig.scale === 'number')\n {\n node.scaling.setAll(meshConfig.scale);\n }\n else\n {\n node.scaling.set(meshConfig.scale.x, meshConfig.scale.y, meshConfig.scale.z);\n }\n }\n\n // Apply material override (typically used for primitives; GLB files usually bring their own materials)\n const matConfig = meshConfig.material;\n if(matConfig?.color)\n {\n const color = matConfig.color;\n\n if(matConfig.type === 'pbr')\n {\n const material = new PBRMaterial(`${ entity.type }-material`, scene);\n material.albedoColor = this._toColor3(color);\n\n if(matConfig.emissive)\n {\n material.emissiveColor = this._toColor3(matConfig.emissive);\n }\n\n material.metallic = matConfig.metallic ?? 0;\n material.roughness = matConfig.roughness ?? 1;\n\n for(const mesh of meshes)\n {\n mesh.material = material;\n }\n }\n else\n {\n const material = new StandardMaterial(`${ entity.type }-material`, scene);\n material.diffuseColor = this._toColor3(color);\n\n if(matConfig.emissive)\n {\n material.emissiveColor = this._toColor3(matConfig.emissive);\n }\n\n for(const mesh of meshes)\n {\n mesh.material = material;\n }\n }\n }\n\n // Attach the node to the entity\n this.gameEngine.managers.entityManager.attachToNode(entity, node);\n }\n\n /**\n * Create game entities for all nodes marked with the 'entity' metadata property.\n */\n private async _processEntityNodes() : Promise<void>\n {\n for(const entityNode of this._entityNodes)\n {\n // eslint-disable-next-line no-await-in-loop\n await this._processEntityNode(entityNode);\n }\n }\n\n /**\n * Create a game entity for an entity-tagged node, attaching the existing scene node to it.\n */\n private async _processEntityNode(entityNode : EntityNodeData) : Promise<void>\n {\n const entityDef = this._config.entities?.[entityNode.type];\n\n try\n {\n const initialState = {\n ...entityDef?.config,\n position: this._vectorToObject(entityNode.node.position),\n };\n\n const entity = await this.gameEngine.managers.entityManager.createEntity(entityNode.type, {\n name: entityDef?.name ?? entityNode.node.name,\n tags: entityDef?.tags,\n initialState,\n node: entityNode.node,\n });\n this._spawnedEntities.push(entity);\n\n this._log.debug(`Created entity '${ entityNode.type }' for node '${ entityNode.node.name }'`);\n }\n catch (error)\n {\n this._log.error(`Failed to create entity for node '${ entityNode.node.name }':`, error);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Level Sounds\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create level-scoped sounds from YAML config. Sounds are tracked for lifecycle management\n * (pause on deactivate, resume on activate, dispose on unload).\n */\n private async _processLevelSounds() : Promise<void>\n {\n const soundConfigs = this._config.sounds;\n if(!soundConfigs) { return; }\n\n const { audioManager } = this.gameEngine.managers;\n if(!audioManager)\n {\n this._log.warn('No AudioManager configured. Level sounds will not be created.');\n return;\n }\n\n for(const [ name, config ] of Object.entries(soundConfigs))\n {\n try\n {\n // eslint-disable-next-line no-await-in-loop\n const sound = await audioManager.createSound(name, config.url, config.channel, {\n loop: config.loop ?? false,\n volume: config.volume ?? 1,\n });\n\n this._levelSounds.set(name, sound);\n\n if(config.autoplay)\n {\n sound.play();\n }\n\n this._log.debug(`Created level sound: \"${ name }\"`);\n }\n catch (error)\n {\n this._log.error(`Failed to create level sound \"${ name }\":`, error);\n }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Pause all playing level sounds when the level is deactivated.\n * Subclasses that override this must call `super.onDeactivate()`.\n */\n async onDeactivate() : Promise<void>\n {\n for(const [ name, sound ] of this._levelSounds)\n {\n if(sound.state === SoundState.Started)\n {\n this._playingSoundsBeforeDeactivate.add(name);\n sound.pause();\n }\n }\n }\n\n /**\n * Resume previously playing level sounds when the level is reactivated.\n * Subclasses that override this must call `super.onActivate()`.\n */\n async onActivate() : Promise<void>\n {\n for(const name of this._playingSoundsBeforeDeactivate)\n {\n const sound = this._levelSounds.get(name);\n if(sound)\n {\n sound.play();\n }\n }\n\n this._playingSoundsBeforeDeactivate.clear();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Dispose of this level's resources\n */\n override async $dispose() : Promise<void>\n {\n // Destroy all spawned entities\n const { entityManager } = this.gameEngine.managers;\n const destroyPromises = this._spawnedEntities.map(async (entity) =>\n {\n try\n {\n await entityManager.destroyEntity(entity.id);\n }\n catch (error)\n {\n this._log.error(`Failed to destroy entity ${ entity.id }:`, error);\n }\n });\n\n await Promise.all(destroyPromises);\n\n this._spawnedEntities = [];\n this._spawnPoints = [];\n this._entityNodes = [];\n\n // Dispose level sounds\n for(const sound of this._levelSounds.values())\n {\n sound.dispose();\n }\n this._levelSounds.clear();\n this._playingSoundsBeforeDeactivate.clear();\n\n await super.$dispose();\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Level Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type {\n LevelConfig,\n LevelConstructor,\n LevelContext,\n PropertyHandler,\n} from '../interfaces/level.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type {\n LevelCompletePayload,\n LevelErrorPayload,\n LevelProgressPayload,\n} from '../events/payloads.ts';\n\nimport { Level } from '../classes/level.ts';\nimport { GameLevel } from '../classes/gameLevel.ts';\nimport { GameEventBus } from '../classes/eventBus.ts';\nimport type { GameEngine } from '../classes/gameEngine.ts';\n\n// Utils\nimport { type LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Options for level transitions.\n */\nexport interface TransitionOptions\n{\n /** If true, the old level is kept loaded (not disposed) after transitioning away */\n keepAlive ?: boolean;\n\n /** If true, the new level is loaded but not activated — currentLevel remains unchanged */\n preloadOnly ?: boolean;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Manages game levels, acting as a factory for level instantiation.\n *\n * The LevelManager is responsible for:\n * - Registering level configurations and custom level classes\n * - Creating level instances with properly injected dependencies\n * - Managing the lifecycle of loaded levels (loading, activation, disposal)\n *\n * Levels are not instantiated directly. Instead, use the manager's factory methods:\n *\n * ```typescript\n * // Register a config\n * levelManager.registerLevelConfig({ name: 'Level1', scene: 'level1.glb' });\n *\n * // Register a custom level class\n * levelManager.registerLevelClass('custom', MyCustomLevel);\n *\n * // Load creates the instance with deps injected\n * await levelManager.loadLevel('Level1');\n * ```\n */\nexport class LevelManager implements Disposable\n{\n private _eventBus : GameEventBus;\n private _gameEngine : GameEngine | null = null;\n private _log : LoggerInterface;\n private _logger ?: LoggingUtility;\n\n /** Map of registered level configurations by name */\n private _levelConfigs = new Map<string, LevelConfig>();\n\n /** Map of registered custom level classes by name */\n private _levelClasses = new Map<string, LevelConstructor>();\n\n /** Map of loaded level instances by name */\n private _loadedLevels = new Map<string, Level>();\n\n /** Map of registered property handlers for scene node metadata */\n private _propertyHandlers = new Map<string, PropertyHandler>();\n\n /** The currently active level */\n private _currentLevel : Level | null = null;\n\n /** Unsubscribe functions for event subscriptions */\n private _eventUnsubscribers : (() => void)[] = [];\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Gets the currently active level\n */\n public get currentLevel() : Level | null\n {\n return this._currentLevel;\n }\n\n /**\n * Gets the registered property handlers\n */\n public get propertyHandlers() : Map<string, PropertyHandler>\n {\n return this._propertyHandlers;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n constructor(\n eventBus : GameEventBus,\n logger ?: LoggingUtility\n )\n {\n this._eventBus = eventBus;\n this._logger = logger;\n this._log = logger?.getLogger('LevelManager') || new SAGELogger('LevelManager');\n\n // Subscribe to level events and store unsubscribe handles\n this._eventUnsubscribers.push(\n this._eventBus.subscribe('level:progress', (event) =>\n {\n this._handleProgress(event.payload);\n })\n );\n\n this._eventUnsubscribers.push(\n this._eventBus.subscribe('level:complete', (event) =>\n {\n this._handleComplete(event.payload);\n })\n );\n\n this._eventUnsubscribers.push(\n this._eventBus.subscribe('level:error', (event) =>\n {\n this._handleError(event.payload);\n })\n );\n\n this._log.info('LevelManager initialized');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Internal API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Late-bind the GameEngine after construction.\n * Called by createGameEngine() after the GameEngine instance is created.\n */\n $setGameEngine(gameEngine : GameEngine) : void\n {\n this._gameEngine = gameEngine;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods\n //------------------------------------------------------------------------------------------------------------------\n\n private _handleProgress(payload : LevelProgressPayload) : void\n {\n this._log.debug(`Level ${ payload.levelName } progress: ${ payload.progress }%`);\n }\n\n private _handleComplete(payload : LevelCompletePayload) : void\n {\n this._log.info(`Level ${ payload.levelName } loaded successfully`);\n }\n\n private _handleError(payload : LevelErrorPayload) : void\n {\n this._log.error(`Level ${ payload.levelName } error: ${ payload.message }`);\n }\n\n /**\n * Creates the LevelContext with all dependencies\n */\n private _createContext() : LevelContext\n {\n if(!this._gameEngine)\n {\n throw new Error('LevelManager: gameEngine not set. Call $setGameEngine() first.');\n }\n\n return {\n gameEngine: this._gameEngine,\n propertyHandlers: this._propertyHandlers,\n logger: this._logger,\n };\n }\n\n /**\n * Creates a Level instance from a config using the appropriate class\n */\n private _createLevelInstance(config : LevelConfig) : Level\n {\n const context = this._createContext();\n\n if(config.class)\n {\n // Look up the registered class\n const LevelClass = this._levelClasses.get(config.class);\n if(!LevelClass)\n {\n throw new Error(`Level class '${ config.class }' is not registered.`);\n }\n\n this._log.debug(`Creating level '${ config.name }' using class '${ config.class }'`);\n return new LevelClass(config, context) as Level;\n }\n\n // Use GameLevel as default\n this._log.debug(`Creating level '${ config.name }' using GameLevel`);\n return new GameLevel(config, context);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Configuration Registration API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Registers a level configuration.\n * The level will be instantiated when loadLevel() is called.\n *\n * @param config\n */\n public registerLevelConfig(config : LevelConfig) : void\n {\n if(this._levelConfigs.has(config.name))\n {\n this._log.warn(`Level config '${ config.name }' is already registered. Overwriting.`);\n }\n\n this._levelConfigs.set(config.name, config);\n this._log.info(`Registered level config: ${ config.name }`);\n }\n\n /**\n * Registers a custom Level class that can be referenced in configs.\n * When a config specifies `class: \"my-level\"`, this class will be instantiated.\n *\n * @param name - The key used in LevelConfig.class to reference this constructor\n * @param levelClass\n */\n public registerLevelClass(name : string, levelClass : LevelConstructor) : void\n {\n if(this._levelClasses.has(name))\n {\n this._log.warn(`Level class '${ name }' is already registered. Overwriting.`);\n }\n\n this._levelClasses.set(name, levelClass);\n this._log.info(`Registered level class: ${ name }`);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Property Handler API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a property handler for processing custom properties on scene nodes.\n * Handlers are called for each node that has the specified property in its metadata.\n *\n * @param property - The metadata property name to match (e.g., 'sound', 'collider')\n * @param handler\n */\n public registerPropertyHandler(property : string, handler : PropertyHandler) : void\n {\n if(this._propertyHandlers.has(property))\n {\n this._log.warn(`Property handler '${ property }' is already registered. Overwriting.`);\n }\n\n this._propertyHandlers.set(property, handler);\n this._log.debug(`Registered property handler: ${ property }`);\n }\n\n /**\n * Unregister a property handler.\n *\n * @param property\n */\n public unregisterPropertyHandler(property : string) : void\n {\n this._propertyHandlers.delete(property);\n }\n\n /**\n * Clear all registered property handlers.\n * Primarily useful for testing to ensure a clean state between tests.\n */\n public clearAllPropertyHandlers() : void\n {\n this._propertyHandlers.clear();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Level Loading API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Gets a loaded level by name.\n * Returns null if the level is not currently loaded.\n *\n * @param name\n */\n public getLevel(name : string) : Level | null\n {\n return this._loadedLevels.get(name) || null;\n }\n\n /**\n * Gets a registered level config by name.\n *\n * @param name\n */\n public getLevelConfig(name : string) : LevelConfig | null\n {\n return this._levelConfigs.get(name) || null;\n }\n\n /**\n * Loads a level by name. The level config must be registered first.\n *\n * @param name\n */\n public async loadLevel(name : string) : Promise<Level>;\n\n /**\n * Loads a level from a config. The config will be registered automatically.\n *\n * @param config\n */\n public async loadLevel(config : LevelConfig) : Promise<Level>;\n\n /**\n * Loads a level. If a name is passed, the config must be registered.\n * If a config is passed, it will be registered automatically.\n * Returns the existing instance if already loaded.\n *\n * @param configOrName\n */\n public async loadLevel(configOrName : string | LevelConfig) : Promise<Level>\n {\n // Resolve the config\n let config : LevelConfig;\n if(typeof configOrName === 'string')\n {\n const registered = this._levelConfigs.get(configOrName);\n if(!registered)\n {\n throw new Error(`Level config '${ configOrName }' is not registered.`);\n }\n config = registered;\n }\n else\n {\n config = configOrName;\n // Auto-register the config\n this.registerLevelConfig(config);\n }\n\n // Check if already loaded\n const existing = this._loadedLevels.get(config.name);\n if(existing)\n {\n this._log.warn(`Level '${ config.name }' is already loaded`);\n return existing;\n }\n\n // Create and load the level\n this._log.debug(`Loading level: ${ config.name }`);\n const level = this._createLevelInstance(config);\n await level.load();\n\n // Store the loaded level\n this._loadedLevels.set(config.name, level);\n\n return level;\n }\n\n /**\n * Activates a level by name, loading it if necessary.\n *\n * @param name\n */\n public async activateLevel(name : string) : Promise<Level>;\n\n /**\n * Activates a level from a config, loading it if necessary.\n *\n * @param config\n */\n public async activateLevel(config : LevelConfig) : Promise<Level>;\n\n /**\n * Activates a level, loading it if necessary.\n * Sets it as the currentLevel used by the render loop.\n *\n * @param configOrName\n */\n public async activateLevel(configOrName : string | LevelConfig) : Promise<Level>\n {\n const level = typeof configOrName === 'string'\n ? await this.loadLevel(configOrName)\n : await this.loadLevel(configOrName);\n this._currentLevel = level;\n this._log.info(`Activated level: ${ level.name }`);\n return level;\n }\n\n /**\n * Unloads a level and disposes its resources.\n * Clears currentLevel if this was the active level.\n *\n * @param name\n */\n public async unloadLevel(name : string) : Promise<void>\n {\n const level = this._loadedLevels.get(name);\n if(level)\n {\n // If this is the current level, clear it\n if(this._currentLevel === level)\n {\n this._currentLevel = null;\n }\n\n // Dispose the level\n await level.$dispose();\n\n // Remove from loaded levels\n this._loadedLevels.delete(name);\n\n this._log.info(`Unloaded level: ${ name }`);\n }\n }\n\n /**\n * Unloads the current level and disposes its resources\n */\n public async unloadCurrentLevel() : Promise<void>\n {\n if(this._currentLevel)\n {\n await this.unloadLevel(this._currentLevel.name);\n }\n else\n {\n this._log.warn('No current level to unload');\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Transitions\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Orchestrates a level-to-level transition with lifecycle hooks and events.\n *\n * Flow: deactivate old -> load new -> dispose old (unless keepAlive) -> activate new.\n * If `preloadOnly` is true, the new level is loaded but not activated.\n *\n * @param levelName - The name of the level to transition to (must be registered)\n * @param options\n */\n public async transition(levelName : string, options ?: TransitionOptions) : Promise<void>\n {\n const oldLevel = this._currentLevel;\n\n try\n {\n // 1. Emit start\n this._eventBus.publish({\n type: 'level:transition:start',\n payload: {\n from: oldLevel?.name,\n to: levelName,\n },\n });\n\n // 2. Deactivate the old level\n if(oldLevel?.onDeactivate)\n {\n await oldLevel.onDeactivate();\n }\n\n // 3. Emit loading progress\n this._eventBus.publish({\n type: 'level:transition:progress',\n payload: {\n stage: 'loading',\n levelName,\n },\n });\n\n // 4. Load the new level\n const newLevel = await this.loadLevel(levelName);\n\n // 5. If preloadOnly, stop here\n if(options?.preloadOnly)\n {\n return;\n }\n\n // 6. Dispose old level unless keepAlive\n if(oldLevel && !options?.keepAlive)\n {\n await this.unloadLevel(oldLevel.name);\n }\n\n // 7. Set new level as current\n this._currentLevel = newLevel;\n\n // 8. Activate the new level\n if(newLevel.onActivate)\n {\n await newLevel.onActivate();\n }\n\n // 9. Emit complete\n this._eventBus.publish({\n type: 'level:transition:complete',\n payload: { levelName },\n });\n }\n catch (error)\n {\n // 10. Emit error event and re-throw\n this._eventBus.publish({\n type: 'level:transition:error',\n payload: {\n from: oldLevel?.name,\n to: levelName,\n error,\n },\n });\n\n throw error;\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Tears down the level manager and cleans up any resources\n */\n async $teardown() : Promise<void>\n {\n // Unsubscribe from all events\n for(const unsubscribe of this._eventUnsubscribers)\n {\n unsubscribe();\n }\n this._eventUnsubscribers = [];\n\n // Dispose of all loaded levels\n for(const level of this._loadedLevels.values())\n {\n // eslint-disable-next-line no-await-in-loop\n await level.$dispose();\n }\n\n this._loadedLevels.clear();\n this._levelConfigs.clear();\n this._currentLevel = null;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Audio Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { AudioBus, IStaticSoundOptions, StaticSound } from '@babylonjs/core';\n\n// Engines\nimport type { AudioEngine } from '../engines/audio.ts';\n\n// Interfaces\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\n\n// Utils\nimport type { LoggingUtility } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Public channel info exposed to consumers (no BabylonJS internals).\n */\nexport interface ChannelInfo\n{\n volume : number;\n muted : boolean;\n}\n\n/**\n * Internal channel state including the BabylonJS bus reference.\n */\ninterface ChannelState extends ChannelInfo\n{\n bus : AudioBus;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class AudioManager implements Disposable\n{\n private _audioEngine : AudioEngine;\n private _channels = new Map<string, ChannelState>();\n private _masterMuted = false;\n private _masterVolumeBeforeMute = 1;\n private _log : LoggerInterface;\n\n constructor(audioEngine : AudioEngine, logger : LoggingUtility)\n {\n this._audioEngine = audioEngine;\n this._log = logger.getLogger('AudioManager');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n async initialize(channels : string[]) : Promise<void>\n {\n this._log.info(`Initializing audio channels: ${ channels.join(', ') }`);\n\n for(const name of channels)\n {\n // eslint-disable-next-line no-await-in-loop\n const bus = await this._audioEngine.createBus(name);\n this._channels.set(name, { bus, volume: 1, muted: false });\n }\n\n this._log.info('Audio channels initialized.');\n }\n\n async $teardown() : Promise<void>\n {\n this._log.info('Tearing down audio manager...');\n\n for(const state of this._channels.values())\n {\n state.bus.dispose();\n }\n\n this._channels.clear();\n this._log.info('Audio manager torn down.');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Sound Creation\n //------------------------------------------------------------------------------------------------------------------\n\n async createSound(\n name : string,\n url : string,\n channel ?: string,\n options ?: Partial<IStaticSoundOptions>\n ) : Promise<StaticSound>\n {\n let bus : AudioBus | undefined;\n\n if(channel)\n {\n const state = this._channels.get(channel);\n if(!state)\n {\n this._log.warn(`Unknown audio channel \"${ channel }\", sound \"${ name }\" will use default bus.`);\n }\n else\n {\n bus = state.bus;\n }\n }\n\n return this._audioEngine.createSound(name, url, bus, options);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Master Volume\n //------------------------------------------------------------------------------------------------------------------\n\n setMasterVolume(volume : number) : void\n {\n if(!this._masterMuted)\n {\n this._audioEngine.setMasterVolume(volume);\n }\n else\n {\n // Store for when we unmute\n this._masterVolumeBeforeMute = volume;\n }\n }\n\n getMasterVolume() : number\n {\n if(this._masterMuted)\n {\n return this._masterVolumeBeforeMute;\n }\n\n return this._audioEngine.getMasterVolume();\n }\n\n setMasterMuted(muted : boolean) : void\n {\n if(muted === this._masterMuted) { return; }\n\n this._masterMuted = muted;\n\n if(muted)\n {\n this._masterVolumeBeforeMute = this._audioEngine.getMasterVolume();\n this._audioEngine.setMasterVolume(0);\n }\n else\n {\n this._audioEngine.setMasterVolume(this._masterVolumeBeforeMute);\n }\n }\n\n isMasterMuted() : boolean\n {\n return this._masterMuted;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Channel Volume\n //------------------------------------------------------------------------------------------------------------------\n\n setChannelVolume(channel : string, volume : number) : void\n {\n const state = this._channels.get(channel);\n if(!state)\n {\n this._log.warn(`Unknown audio channel: ${ channel }`);\n return;\n }\n\n state.volume = volume;\n\n if(!state.muted)\n {\n this._audioEngine.setBusVolume(state.bus, volume);\n }\n }\n\n getChannelVolume(channel : string) : number\n {\n const state = this._channels.get(channel);\n return state?.volume ?? 1;\n }\n\n setChannelMuted(channel : string, muted : boolean) : void\n {\n const state = this._channels.get(channel);\n if(!state)\n {\n this._log.warn(`Unknown audio channel: ${ channel }`);\n return;\n }\n\n if(muted === state.muted) { return; }\n\n state.muted = muted;\n this._audioEngine.setBusVolume(state.bus, muted ? 0 : state.volume);\n }\n\n isChannelMuted(channel : string) : boolean\n {\n const state = this._channels.get(channel);\n return state?.muted ?? false;\n }\n\n getChannels() : string[]\n {\n return Array.from(this._channels.keys());\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Save Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type { GameEntityManager } from './entity.ts';\nimport type { LevelManager } from './level.ts';\n\n// Utils\nimport { type LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n// Types\n//----------------------------------------------------------------------------------------------------------------------\n\nexport interface SaveData\n{\n version : number;\n levelName : string;\n entities : SerializedEntity[];\n custom : Record<string, unknown>;\n}\n\nexport interface SerializedEntity\n{\n id : string;\n type : string;\n name ?: string;\n tags : string[];\n state : object;\n parentId ?: string;\n transform ?: {\n position : { x : number; y : number; z : number };\n rotation : { x : number; y : number; z : number };\n scaling : { x : number; y : number; z : number };\n };\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\ntype BeforeSerializeHook = () => Record<string, unknown>;\ntype AfterDeserializeHook = (custom : Record<string, unknown>) => void;\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class SaveManager implements Disposable\n{\n private _entityManager : GameEntityManager;\n private _levelManager : LevelManager;\n private _log : LoggerInterface;\n\n private _beforeSerializeHooks : BeforeSerializeHook[] = [];\n private _afterDeserializeHooks : AfterDeserializeHook[] = [];\n\n constructor(\n entityManager : GameEntityManager,\n levelManager : LevelManager,\n logger ?: LoggingUtility\n )\n {\n this._entityManager = entityManager;\n this._levelManager = levelManager;\n this._log = logger?.getLogger('SaveManager') || new SAGELogger('SaveManager');\n this._log.info('SaveManager initialized');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Hook Registration\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a hook that returns custom data to include in save data.\n * Multiple hooks can be registered; their results are merged into the `custom` field.\n */\n onBeforeSerialize(hook : BeforeSerializeHook) : void\n {\n this._beforeSerializeHooks.push(hook);\n }\n\n /**\n * Register a hook to process custom data when loading a save.\n * Multiple hooks can be registered; each receives the full custom data object.\n */\n onAfterDeserialize(hook : AfterDeserializeHook) : void\n {\n this._afterDeserializeHooks.push(hook);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Serialize\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Captures the current game state as a serializable SaveData object.\n * Iterates all entities, captures transforms, state, tags, and parent references.\n * Calls all beforeSerialize hooks to gather custom data.\n */\n serialize() : SaveData\n {\n this._log.debug('Serializing game state...');\n\n const levelName = this._levelManager.currentLevel?.name ?? '';\n const entities : SerializedEntity[] = [];\n\n for(const entity of this._entityManager.getAllEntities())\n {\n const serialized : SerializedEntity = {\n id: entity.id,\n type: entity.type,\n tags: Array.from(entity.tags),\n state: structuredClone(entity.state),\n };\n\n if(entity.name)\n {\n serialized.name = entity.name;\n }\n\n if(entity.parent)\n {\n serialized.parentId = entity.parent.id;\n }\n\n if(entity.node)\n {\n const { position, rotation, scaling } = entity.node;\n serialized.transform = {\n position: { x: position.x, y: position.y, z: position.z },\n rotation: { x: rotation.x, y: rotation.y, z: rotation.z },\n scaling: { x: scaling.x, y: scaling.y, z: scaling.z },\n };\n }\n\n entities.push(serialized);\n }\n\n // Collect custom data from hooks\n let custom : Record<string, unknown> = {};\n for(const hook of this._beforeSerializeHooks)\n {\n const hookData = hook();\n custom = { ...custom, ...hookData };\n }\n\n const saveData : SaveData = {\n version: 1,\n levelName,\n entities,\n custom,\n };\n\n this._log.info(`Serialized ${ entities.length } entities from level \"${ levelName }\"`);\n return saveData;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Deserialize\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Restores game state from a SaveData object.\n *\n * Flow:\n * 1. Transition to the saved level\n * 2. Destroy all existing entities (level-spawned ones)\n * 3. Recreate entities from save data\n * 4. Restore transforms on entities with nodes\n * 5. Re-establish parent-child hierarchy\n * 6. Call afterDeserialize hooks\n */\n async deserialize(data : SaveData) : Promise<void>\n {\n this._log.debug(`Deserializing save data (version ${ data.version }, level \"${ data.levelName }\")...`);\n\n // 1. Transition to the level\n await this._levelManager.transition(data.levelName);\n\n // 2. Destroy all existing entities spawned by the level\n const existingIds : string[] = [];\n for(const entity of this._entityManager.getAllEntities())\n {\n existingIds.push(entity.id);\n }\n\n for(const id of existingIds)\n {\n // eslint-disable-next-line no-await-in-loop\n await this._entityManager.destroyEntity(id);\n }\n\n // 3. Recreate entities — track old ID -> new ID mapping for hierarchy restoration\n const idMap = new Map<string, string>();\n\n for(const serialized of data.entities)\n {\n // eslint-disable-next-line no-await-in-loop\n const entity = await this._entityManager.createEntity(serialized.type, {\n name: serialized.name,\n initialState: serialized.state,\n tags: serialized.tags,\n });\n\n idMap.set(serialized.id, entity.id);\n\n // 4. Restore transform if present\n if(serialized.transform && entity.node)\n {\n const { position, rotation, scaling } = serialized.transform;\n entity.node.position.x = position.x;\n entity.node.position.y = position.y;\n entity.node.position.z = position.z;\n entity.node.rotation.x = rotation.x;\n entity.node.rotation.y = rotation.y;\n entity.node.rotation.z = rotation.z;\n entity.node.scaling.x = scaling.x;\n entity.node.scaling.y = scaling.y;\n entity.node.scaling.z = scaling.z;\n }\n }\n\n // 5. Restore parent-child hierarchy\n for(const serialized of data.entities)\n {\n if(serialized.parentId)\n {\n const newParentId = idMap.get(serialized.parentId);\n const newChildId = idMap.get(serialized.id);\n\n if(newParentId && newChildId)\n {\n this._entityManager.addChild(newParentId, newChildId);\n }\n else\n {\n this._log.warn(\n `Could not restore parent-child: ${ serialized.id } -> ${ serialized.parentId } `\n + `(mapped: ${ newChildId } -> ${ newParentId })`\n );\n }\n }\n }\n\n // 6. Call afterDeserialize hooks\n for(const hook of this._afterDeserializeHooks)\n {\n hook(data.custom);\n }\n\n this._log.info(`Deserialized ${ data.entities.length } entities for level \"${ data.levelName }\"`);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n async $teardown() : Promise<void>\n {\n this._beforeSerializeHooks = [];\n this._afterDeserializeHooks = [];\n this._log.info('SaveManager torn down');\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Keyboard Input Resource Access\n//----------------------------------------------------------------------------------------------------------------------\n\n// Interfaces\nimport type { KeyboardDevice, KeyboardInputState } from '../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Callback type for keyboard device connection events\n */\nexport type KeyboardDeviceCallback = (device : KeyboardDevice) => void;\n\n/**\n * Callback type for keyboard input events\n */\nexport type KeyboardInputCallback = (device : KeyboardDevice, state : KeyboardInputState) => void;\n\n/**\n * Responsible for tracking keyboard state and notifying subscribers of changes.\n */\nexport class KeyboardInputPlugin\n{\n private _keyboardDevice : KeyboardDevice;\n private _keysState : Record<string, boolean> = {};\n\n // Callbacks\n private _onDeviceConnected ?: KeyboardDeviceCallback;\n private _onInputChanged ?: KeyboardInputCallback;\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new KeyboardInputPlugin\n */\n constructor()\n {\n // Initialize keyboard device\n this._keyboardDevice = {\n id: 'keyboard-0',\n name: 'Keyboard',\n type: 'keyboard',\n connected: true,\n };\n\n // Set up event listeners\n this._setupKeyboardEvents();\n\n // Notify that device is connected (on next tick to allow callback registration)\n setTimeout(() => this._notifyDeviceConnected(), 0);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a callback for device connected events\n *\n * @param callback\n */\n public onDeviceConnected(callback : KeyboardDeviceCallback) : void\n {\n this._onDeviceConnected = callback;\n }\n\n /**\n * Register a callback for input changed events\n *\n * @param callback\n */\n public onInputChanged(callback : KeyboardInputCallback) : void\n {\n this._onInputChanged = callback;\n }\n\n /**\n * Get the current keyboard state\n */\n public getState() : KeyboardInputState\n {\n return {\n type: 'keyboard',\n keys: { ...this._keysState },\n delta: {},\n };\n }\n\n /**\n * Get the keyboard device\n */\n public getDevice() : KeyboardDevice\n {\n return { ...this._keyboardDevice };\n }\n\n /**\n * Tears down the keyboard resource access and cleans up event listeners\n */\n public $teardown() : Promise<void>\n {\n // Remove keyboard event listeners\n window.removeEventListener('keydown', this._handleKeyDown);\n window.removeEventListener('keyup', this._handleKeyUp);\n\n return Promise.resolve();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Set up keyboard event listeners\n */\n private _setupKeyboardEvents() : void\n {\n // Bind handlers to this instance\n this._handleKeyDown = this._handleKeyDown.bind(this);\n this._handleKeyUp = this._handleKeyUp.bind(this);\n\n // Add event listeners\n window.addEventListener('keydown', this._handleKeyDown);\n window.addEventListener('keyup', this._handleKeyUp);\n }\n\n /**\n * Handle keyboard key down events\n */\n private _handleKeyDown(event : KeyboardEvent) : void\n {\n this._keysState[event.code] = true;\n\n const delta : Record<string, boolean> = {};\n delta[event.code] = true;\n\n const state : KeyboardInputState = {\n type: 'keyboard',\n keys: { ...this._keysState },\n delta,\n event,\n };\n\n this._notifyInputChanged(state);\n }\n\n /**\n * Handle keyboard key up events\n */\n private _handleKeyUp(event : KeyboardEvent) : void\n {\n this._keysState[event.code] = false;\n\n const delta : Record<string, boolean> = {};\n delta[event.code] = false;\n\n const state : KeyboardInputState = {\n type: 'keyboard',\n keys: { ...this._keysState },\n delta,\n event,\n };\n\n this._notifyInputChanged(state);\n }\n\n /**\n * Notify subscribers of device connected event\n */\n private _notifyDeviceConnected() : void\n {\n if(this._onDeviceConnected)\n {\n this._onDeviceConnected(this._keyboardDevice);\n }\n }\n\n /**\n * Notify subscribers of input changed event\n */\n private _notifyInputChanged(state : KeyboardInputState) : void\n {\n if(this._onInputChanged)\n {\n this._onInputChanged(this._keyboardDevice, state);\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Mouse Input Resource Access\n//----------------------------------------------------------------------------------------------------------------------\n\n// Interfaces\nimport type { ButtonState, MouseDevice, MouseInputState, Position } from '../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Callback type for mouse device connection events\n */\nexport type MouseDeviceCallback = (device : MouseDevice) => void;\n\n/**\n * Callback type for mouse input events\n */\nexport type MouseInputCallback = (device : MouseDevice, state : MouseInputState) => void;\n\n/**\n * Responsible for tracking mouse state and notifying subscribers of changes.\n */\nexport class MouseInputPlugin\n{\n private _targetElement : HTMLElement;\n private _mouseDevice : MouseDevice;\n private _buttonState : Record<string, ButtonState> = {};\n private _axesState : Record<string, number> = {};\n private _position : Position = {\n absolute: { x: 0, y: 0 },\n relative: { x: 0, y: 0 },\n };\n\n private _wheelState = {\n deltaX: 0,\n deltaY: 0,\n deltaZ: 0,\n deltaMode: 0,\n };\n\n // Callbacks\n private _onDeviceConnected ?: MouseDeviceCallback;\n private _onInputChanged ?: MouseInputCallback;\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new MouseInputPlugin\n *\n * @param targetElement - DOM element to attach mouse listeners to (defaults to document.body)\n */\n constructor(targetElement : HTMLElement = document.body)\n {\n this._targetElement = targetElement;\n\n // Initialize mouse device\n this._mouseDevice = {\n id: 'mouse-0',\n name: 'Mouse',\n type: 'mouse',\n connected: true,\n };\n\n // Set up event listeners\n this._setupMouseEvents();\n\n // Initialize axes to zero\n this._axesState['axis-x'] = 0;\n this._axesState['axis-y'] = 0;\n\n // Notify that device is connected (on next tick to allow callback registration)\n setTimeout(() => this._notifyDeviceConnected(), 0);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a callback for device connected events\n *\n * @param callback\n */\n public onDeviceConnected(callback : MouseDeviceCallback) : void\n {\n this._onDeviceConnected = callback;\n }\n\n /**\n * Register a callback for input changed events\n *\n * @param callback\n */\n public onInputChanged(callback : MouseInputCallback) : void\n {\n this._onInputChanged = callback;\n }\n\n /**\n * Get the current mouse state\n */\n public getState() : MouseInputState\n {\n return {\n type: 'mouse',\n buttons: { ...this._buttonState },\n axes: { ...this._axesState },\n position: {\n absolute: { ...this._position.absolute },\n relative: { ...this._position.relative },\n },\n wheel: { ...this._wheelState },\n };\n }\n\n /**\n * Get the mouse device\n */\n public getDevice() : MouseDevice\n {\n return { ...this._mouseDevice };\n }\n\n /**\n * Tears down the mouse resource access and cleans up event listeners\n */\n public $teardown() : Promise<void>\n {\n this._targetElement.removeEventListener('pointerdown', this._handlePointerDown);\n this._targetElement.removeEventListener('pointerup', this._handlePointerUp);\n this._targetElement.removeEventListener('pointermove', this._handlePointerMove);\n this._targetElement.removeEventListener('wheel', this._handleMouseWheel);\n\n return Promise.resolve();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Set up pointer and wheel event listeners. We use pointer events instead of mouse events\n * because BabylonJS calls preventDefault() on pointerdown, which suppresses the\n * corresponding mousedown/mouseup per the Pointer Events spec.\n */\n private _setupMouseEvents() : void\n {\n // Bind handlers to this instance\n this._handlePointerDown = this._handlePointerDown.bind(this);\n this._handlePointerUp = this._handlePointerUp.bind(this);\n this._handlePointerMove = this._handlePointerMove.bind(this);\n this._handleMouseWheel = this._handleMouseWheel.bind(this);\n\n this._targetElement.addEventListener('pointerdown', this._handlePointerDown);\n this._targetElement.addEventListener('pointerup', this._handlePointerUp);\n this._targetElement.addEventListener('pointermove', this._handlePointerMove);\n this._targetElement.addEventListener('wheel', this._handleMouseWheel);\n }\n\n /**\n * Handle pointer down events (mouse buttons)\n */\n private _handlePointerDown(event : PointerEvent) : void\n {\n if(event.pointerType !== 'mouse') { return; }\n\n const buttonKey = `button-${ event.button }`;\n this._buttonState[buttonKey] = { pressed: true };\n\n this._notifyInputChanged({\n type: 'mouse',\n event,\n buttons: { ...this._buttonState },\n axes: { ...this._axesState },\n position: { ...this._position },\n });\n }\n\n /**\n * Handle pointer up events (mouse buttons)\n */\n private _handlePointerUp(event : PointerEvent) : void\n {\n if(event.pointerType !== 'mouse') { return; }\n\n const buttonKey = `button-${ event.button }`;\n this._buttonState[buttonKey] = { pressed: false };\n\n this._notifyInputChanged({\n type: 'mouse',\n event,\n buttons: { ...this._buttonState },\n axes: { ...this._axesState },\n position: { ...this._position },\n });\n }\n\n /**\n * Handle pointer move events (mouse position)\n */\n private _handlePointerMove(event : PointerEvent) : void\n {\n if(event.pointerType !== 'mouse') { return; }\n\n this._position = {\n absolute: {\n x: event.clientX,\n y: event.clientY,\n },\n relative: {\n x: event.movementX,\n y: event.movementY,\n },\n };\n\n this._axesState['axis-x'] = event.clientX;\n this._axesState['axis-y'] = event.clientY;\n\n this._notifyInputChanged({\n type: 'mouse',\n event,\n buttons: { ...this._buttonState },\n axes: { ...this._axesState },\n position: { ...this._position },\n });\n }\n\n /**\n * Handle mouse wheel events\n */\n private _handleMouseWheel(event : WheelEvent) : void\n {\n this._wheelState = {\n deltaX: event.deltaX,\n deltaY: event.deltaY,\n deltaZ: event.deltaZ,\n deltaMode: event.deltaMode,\n };\n\n this._notifyInputChanged({\n type: 'mouse',\n event,\n buttons: { ...this._buttonState },\n axes: { ...this._axesState },\n position: { ...this._position },\n wheel: { ...this._wheelState },\n });\n }\n\n /**\n * Notify subscribers of device connected event\n */\n private _notifyDeviceConnected() : void\n {\n if(this._onDeviceConnected)\n {\n this._onDeviceConnected(this._mouseDevice);\n }\n }\n\n /**\n * Notify subscribers of input changed event\n */\n private _notifyInputChanged(state : MouseInputState) : void\n {\n if(this._onInputChanged)\n {\n this._onInputChanged(this._mouseDevice, state);\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Gamepad Input\n//----------------------------------------------------------------------------------------------------------------------\n\n// Interfaces\nimport type { ButtonState, GamepadDevice, GamepadInputState } from '../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Callback type for gamepad device connection events\n */\nexport type GamepadDeviceCallback = (device : GamepadDevice) => void;\n\n/**\n * Callback type for gamepad input events\n */\nexport type GamepadInputCallback = (device : GamepadDevice, state : GamepadInputState) => void;\n\n/**\n * Responsible for tracking gamepad state and notifying subscribers of changes.\n */\nexport class GamepadInputPlugin\n{\n private _gamepadDevices : Record<number, GamepadDevice | undefined> = {};\n private _buttonStates : Record<number, Record<string, ButtonState> | undefined> = {};\n private _axesStates : Record<number, Record<string, number> | undefined> = {};\n\n // Callbacks\n private _onDeviceConnected ?: GamepadDeviceCallback;\n private _onDeviceDisconnected ?: GamepadDeviceCallback;\n private _onInputChanged ?: GamepadInputCallback;\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new GamepadInputPlugin\n */\n constructor()\n {\n // Set up event listeners for gamepad connection and disconnection\n this._setupGamepadEvents();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a callback for device connected events\n *\n * @param callback\n */\n public onDeviceConnected(callback : GamepadDeviceCallback) : void\n {\n this._onDeviceConnected = callback;\n }\n\n /**\n * Register a callback for device disconnected events\n *\n * @param callback\n */\n public onDeviceDisconnected(callback : GamepadDeviceCallback) : void\n {\n this._onDeviceDisconnected = callback;\n }\n\n /**\n * Register a callback for input changed events\n *\n * @param callback\n */\n public onInputChanged(callback : GamepadInputCallback) : void\n {\n this._onInputChanged = callback;\n }\n\n /**\n * Get all connected gamepad devices\n */\n public getDevices() : GamepadDevice[]\n {\n return Object.values(this._gamepadDevices).map((device) => ({ ...device }) as GamepadDevice);\n }\n\n /**\n * Get all connected gamepad states\n */\n public getStates() : Record<number, GamepadInputState>\n {\n const states : Record<number, GamepadInputState> = {};\n\n Object.keys(this._buttonStates).forEach((indexStr) =>\n {\n const index = Number(indexStr);\n const buttons = this._buttonStates[index] || {};\n const axes = this._axesStates[index] || {};\n\n states[index] = {\n type: 'gamepad',\n buttons: { ...buttons },\n axes: { ...axes },\n };\n });\n\n return states;\n }\n\n /**\n * Get a specific gamepad device by index\n *\n * @param index\n */\n public getDevice(index : number) : GamepadDevice | null\n {\n const device = this._gamepadDevices[index];\n return device ? { ...device } : null;\n }\n\n /**\n * Get a specific gamepad state by index\n *\n * @param index\n */\n public getState(index : number) : GamepadInputState | null\n {\n const buttons = this._buttonStates[index];\n const axes = this._axesStates[index];\n\n if(!buttons && !axes) { return null; }\n\n return {\n type: 'gamepad',\n buttons: buttons ? { ...buttons } : {},\n axes: axes ? { ...axes } : {},\n };\n }\n\n /**\n * Poll for gamepad state updates - call this in your game loop\n */\n public pollGamepads() : void\n {\n /* eslint-disable no-continue */\n\n if(!navigator.getGamepads) { return; }\n\n const gamepads = navigator.getGamepads();\n for(const gamepad of gamepads)\n {\n if(!gamepad) { continue; }\n\n const index = gamepad.index;\n\n if(!this._gamepadDevices[index])\n {\n this._handleGamepadConnected(gamepad);\n continue;\n }\n\n // Get device\n const device = this._gamepadDevices[index];\n if(!device) { continue; }\n\n // Get current button and axis states or initialize them\n const currentButtons = this._buttonStates[index] || {};\n const currentAxes = this._axesStates[index] || {};\n\n // Build new button state object\n const newButtons : Record<string, ButtonState> = {};\n let buttonsChanged = false;\n\n gamepad.buttons.forEach((btn, i) =>\n {\n const buttonKey = `button-${ i }`;\n const buttonState : ButtonState = {\n pressed: btn.pressed,\n touched: btn.touched,\n value: btn.value,\n };\n\n newButtons[buttonKey] = buttonState;\n\n const prevButton = currentButtons[buttonKey];\n if(!prevButton\n || prevButton.pressed !== buttonState.pressed\n || prevButton.touched !== buttonState.touched\n || prevButton.value !== buttonState.value)\n {\n buttonsChanged = true;\n }\n });\n\n // Build new axis state object\n const newAxes : Record<string, number> = {};\n let axesChanged = false;\n\n gamepad.axes.forEach((axisValue, i) =>\n {\n const axisKey = `axis-${ i }`;\n newAxes[axisKey] = axisValue;\n\n const prevAxis = currentAxes[axisKey];\n if(prevAxis !== axisValue)\n {\n axesChanged = true;\n }\n });\n\n // Update state objects\n this._buttonStates[index] = newButtons;\n this._axesStates[index] = newAxes;\n\n // Notify if state changed\n if(buttonsChanged || axesChanged)\n {\n const state : GamepadInputState = {\n type: 'gamepad',\n buttons: { ...newButtons },\n axes: { ...newAxes },\n };\n\n this._notifyInputChanged(device, state);\n }\n }\n\n /* eslint-enable no-continue */\n }\n\n /**\n * Tears down the gamepad resource access and cleans up event listeners\n */\n public $teardown() : Promise<void>\n {\n // Remove gamepad event listeners\n window.removeEventListener('gamepadconnected', this._handleGamepadConnected);\n window.removeEventListener('gamepaddisconnected', this._handleGamepadDisconnected);\n\n return Promise.resolve();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Set up gamepad event listeners\n */\n private _setupGamepadEvents() : void\n {\n // Bind handlers to this instance\n this._handleGamepadConnected = this._handleGamepadConnected.bind(this);\n this._handleGamepadDisconnected = this._handleGamepadDisconnected.bind(this);\n\n // Add event listeners\n window.addEventListener('gamepadconnected', this._handleGamepadConnected);\n window.addEventListener('gamepaddisconnected', this._handleGamepadDisconnected);\n\n // Check for already connected gamepads (browsers sometimes miss the connected event)\n if(navigator.getGamepads)\n {\n const gamepads = navigator.getGamepads();\n for(const gamepad of gamepads)\n {\n if(gamepad)\n {\n this._handleGamepadConnected(gamepad);\n }\n }\n }\n }\n\n /**\n * Handle gamepad connected event\n */\n private _handleGamepadConnected(event : GamepadEvent | Gamepad) : void\n {\n const gamepad = event instanceof GamepadEvent ? event.gamepad : event;\n const index = gamepad.index;\n\n // Create gamepad device object\n const gamepadDevice : GamepadDevice = {\n id: `gamepad-${ index }`,\n name: gamepad.id,\n type: 'gamepad',\n connected: true,\n index,\n mapping: gamepad.mapping,\n axes: Array.from(gamepad.axes),\n buttons: Array.from(gamepad.buttons),\n };\n\n // Store the device\n this._gamepadDevices[index] = gamepadDevice;\n\n // Initialize button state object\n const buttonStateObj : Record<string, ButtonState> = {};\n Array.from(gamepad.buttons).forEach((btn, i) =>\n {\n buttonStateObj[`button-${ i }`] = {\n pressed: btn.pressed,\n touched: btn.touched,\n value: btn.value,\n };\n });\n this._buttonStates[index] = buttonStateObj;\n\n // Initialize axis state object\n const axesStateObj : Record<string, number> = {};\n Array.from(gamepad.axes).forEach((axisValue, i) =>\n {\n axesStateObj[`axis-${ i }`] = axisValue;\n });\n this._axesStates[index] = axesStateObj;\n\n // Notify device connected with initial state\n this._notifyDeviceConnected(gamepadDevice);\n\n // Emit input changed event with initial state\n this._notifyInputChanged(gamepadDevice, {\n type: 'gamepad',\n buttons: { ...buttonStateObj },\n axes: { ...axesStateObj },\n event: event instanceof GamepadEvent ? event : undefined,\n });\n }\n\n /**\n * Handle gamepad disconnected event\n */\n private _handleGamepadDisconnected(event : GamepadEvent) : void\n {\n const gamepad = event.gamepad;\n const index = gamepad.index;\n\n // Get the device\n const gamepadDevice = this._gamepadDevices[index];\n\n if(gamepadDevice)\n {\n // Update connection status\n gamepadDevice.connected = false;\n\n // Notify device disconnected\n this._notifyDeviceDisconnected(gamepadDevice);\n\n // Remove from tracking objects\n this._gamepadDevices[index] = undefined;\n this._buttonStates[index] = undefined;\n this._axesStates[index] = undefined;\n }\n }\n\n /**\n * Notify subscribers of device connected event\n */\n private _notifyDeviceConnected(device : GamepadDevice) : void\n {\n if(this._onDeviceConnected)\n {\n this._onDeviceConnected(device);\n }\n }\n\n /**\n * Notify subscribers of device disconnected event\n */\n private _notifyDeviceDisconnected(device : GamepadDevice) : void\n {\n if(this._onDeviceDisconnected)\n {\n this._onDeviceDisconnected(device);\n }\n }\n\n /**\n * Notify subscribers of input changed event\n */\n private _notifyInputChanged(device : GamepadDevice, state : GamepadInputState) : void\n {\n if(this._onInputChanged)\n {\n this._onInputChanged(device, state);\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// User Input Manager\n//----------------------------------------------------------------------------------------------------------------------\n\n// Interfaces\nimport type { GamepadInputState, InputDevice, KeyboardInputState, MouseInputState } from '../interfaces/input.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\n\n// Classes\nimport { GameEventBus } from '../classes/eventBus.ts';\n\n// Utils\nimport { LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n// Resource Access\nimport { KeyboardInputPlugin } from '../classes/input/keyboard.ts';\nimport { MouseInputPlugin } from '../classes/input/mouse.ts';\nimport { GamepadInputPlugin } from '../classes/input/gamepad.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Manager for handling user input from various devices (keyboard, mouse, gamepad)\n */\nexport class UserInputManager implements Disposable\n{\n private _eventBus : GameEventBus;\n\n private _keyboardRA : KeyboardInputPlugin;\n private _mouseRA : MouseInputPlugin;\n private _gamepadRA : GamepadInputPlugin;\n\n /** Logger instance */\n private _log : LoggerInterface;\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new UserInputManager.\n *\n * @param eventBus\n * @param canvas - The DOM element to attach input listeners to (used by mouse input)\n * @param logger\n */\n constructor(\n eventBus : GameEventBus,\n canvas : HTMLElement,\n logger ?: LoggingUtility\n )\n {\n this._eventBus = eventBus;\n\n // Initialize logger\n this._log = logger?.getLogger('UserInputManager') || new SAGELogger('UserInputManager');\n\n this._log.info('Initializing UserInputManager');\n\n // Initialize resource access classes\n this._log.debug('Initializing input resource access classes');\n this._keyboardRA = new KeyboardInputPlugin();\n this._mouseRA = new MouseInputPlugin(canvas);\n this._gamepadRA = new GamepadInputPlugin();\n\n // Register callbacks\n this._log.debug('Registering input event callbacks');\n this._keyboardRA.onDeviceConnected(this._publishDeviceConnected.bind(this));\n this._keyboardRA.onInputChanged(this._publishInputChanged.bind(this));\n this._mouseRA.onDeviceConnected(this._publishDeviceConnected.bind(this));\n this._mouseRA.onInputChanged(this._publishInputChanged.bind(this));\n this._gamepadRA.onDeviceConnected(this._publishDeviceConnected.bind(this));\n this._gamepadRA.onDeviceDisconnected(this._publishDeviceDisconnected.bind(this));\n this._gamepadRA.onInputChanged(this._publishInputChanged.bind(this));\n\n this._log.info('UserInputManager initialized successfully');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Publish device connected event to the event bus\n */\n private _publishDeviceConnected(device : InputDevice) : void\n {\n this._log.debug(`Device connected: ${ device.id } (${ device.name })`);\n this._eventBus.publish({ type: 'input:device:connected', payload: { device } });\n }\n\n /**\n * Publish device disconnected event to the event bus\n */\n private _publishDeviceDisconnected(device : InputDevice) : void\n {\n this._log.debug(`Device disconnected: ${ device.id } (${ device.name })`);\n this._eventBus.publish({ type: 'input:device:disconnected', payload: { device } });\n }\n\n /**\n * Publish input changed event to the event bus, used by all device types\n */\n private _publishInputChanged(\n device : InputDevice,\n state : KeyboardInputState | MouseInputState | GamepadInputState\n ) : void\n {\n this._log.trace(`Input changed: ${ device.id }`, state);\n this._eventBus.publish({\n type: 'input:changed',\n payload: {\n deviceId: device.id,\n device,\n state,\n },\n });\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Internal methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Tears down the input manager and clean up event listeners\n */\n public $teardown() : Promise<void>\n {\n this._log.info('Tearing down UserInputManager');\n\n // Cleanup all resource access instances\n this._log.debug('Cleaning up input resource access instances');\n\n // Using Promise.all to handle multiple async teardowns\n return Promise.all([\n this._keyboardRA.$teardown(),\n this._mouseRA.$teardown(),\n this._gamepadRA.$teardown(),\n ]).then(() =>\n {\n this._log.info('UserInputManager torn down successfully');\n });\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Get all connected input devices (keyboard, mouse, and any gamepads).\n */\n public listDevices() : InputDevice[]\n {\n this._log.debug('Getting all connected input devices');\n\n const devices : InputDevice[] = [];\n\n // Add keyboard and mouse\n devices.push(this._keyboardRA.getDevice());\n devices.push(this._mouseRA.getDevice());\n\n // Add all gamepads\n devices.push(...this._gamepadRA.getDevices());\n\n this._log.debug(`Found ${ devices.length } connected devices`);\n return devices;\n }\n\n /**\n * Get a specific input device by ID.\n * Uses device ID prefix (keyboard-, mouse-, gamepad-) for fast lookup.\n *\n * @param deviceId\n */\n public getDevice(deviceId : string) : InputDevice | null\n {\n this._log.debug(`Getting device: ${ deviceId }`);\n\n // Fast path for known device prefixes\n if(deviceId.startsWith('keyboard-'))\n {\n return this._keyboardRA.getDevice();\n }\n\n if(deviceId.startsWith('mouse-'))\n {\n return this._mouseRA.getDevice();\n }\n\n if(deviceId.startsWith('gamepad-'))\n {\n const index = parseInt(deviceId.split('-')[1], 10);\n return this._gamepadRA.getDevice(index);\n }\n\n // Slow path fallback - iterate through all devices\n const device = this.listDevices().find((dev) => dev.id === deviceId);\n if(device)\n {\n return device;\n }\n\n this._log.warn(`Device not found: ${ deviceId }`);\n return null;\n }\n\n /**\n * Poll for gamepad state updates - call this in your game loop\n */\n public pollGamepads() : void\n {\n this._gamepadRA.pollGamepads();\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n","//----------------------------------------------------------------------------------------------------------------------\n// Graphics Utility\n//----------------------------------------------------------------------------------------------------------------------\n\nimport {\n AbstractEngine,\n Engine,\n type EngineOptions,\n NullEngine,\n type NullEngineOptions,\n WebGPUEngine,\n type WebGPUEngineOptions,\n} from '@babylonjs/core';\n\n// Interfaces\nimport type { BabylonEngineOptions, GameCanvas, RenderEngineOptions } from '../interfaces/game.ts';\n\n// Utilities\nimport { hasWebGPU } from './capabilities.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Builds and initializes a WebGPU engine.\n *\n * @param canvas\n * @param options\n */\nasync function _buildWebGPUEngine(\n canvas : HTMLCanvasElement | OffscreenCanvas,\n options : WebGPUEngineOptions\n) : Promise<WebGPUEngine>\n{\n const engine = new WebGPUEngine(canvas, options);\n await engine.initAsync();\n return engine;\n}\n\n/**\n * Builds a WebGL engine.\n *\n * @param canvas\n * @param options\n */\nfunction _buildWebGLEngine(canvas : GameCanvas, options : BabylonEngineOptions) : Engine\n{\n return new Engine(canvas, options.antialias, options.options, options.adaptToDeviceRatio);\n}\n\n/**\n * Builds a Null engine, which is used when no rendering is required.\n *\n * @param options\n */\nfunction _buildNullEngine(options : NullEngineOptions) : NullEngine\n{\n return new NullEngine(options as NullEngineOptions);\n}\n\n//------------------------------------------------------------------------------------------------------------------\n\n/**\n * Creates an appropriate engine based on the provided canvas and options.\n * Passing null for canvas creates a NullEngine. Otherwise auto-detects WebGPU with WebGL fallback,\n * unless a specific engine type is forced via `options.engine`.\n *\n * @param canvas - Pass null to get a NullEngine (headless/testing)\n * @param options\n */\nexport async function createEngine(\n canvas : GameCanvas | null,\n options : RenderEngineOptions,\n largeWorldRendering = false\n) : Promise<AbstractEngine>\n{\n // Check if we should use a null engine (no canvas provided)\n if(canvas === null)\n {\n console.debug('Using Null Engine');\n return _buildNullEngine(options as NullEngineOptions);\n }\n\n // Inject large world rendering into engine options if requested. We have to build new objects\n // here because BabylonJS declares useLargeWorldRendering as readonly on its option types.\n if(largeWorldRendering)\n {\n if('options' in options)\n {\n const babylonOpts = options as BabylonEngineOptions;\n options = {\n ...babylonOpts,\n options: { ...babylonOpts.options, useLargeWorldRendering: true },\n } as RenderEngineOptions;\n }\n else\n {\n // WebGPUEngineOptions -- useLargeWorldRendering lives at the top level\n options = { ...options, useLargeWorldRendering: true } as RenderEngineOptions;\n }\n }\n\n // Extract the forceEngine option if available\n const forceEngine = options.engine || 'auto';\n\n // Force WebGPU engine if specified and available\n if(forceEngine === 'webgpu')\n {\n if(hasWebGPU())\n {\n try\n {\n console.debug('Using forced WebGPU engine');\n return await _buildWebGPUEngine(canvas, options);\n }\n catch (error)\n {\n console.error('Forced WebGPU initialization failed:', error);\n throw new Error(\n 'Forced WebGPU failed to initialize. If WebGPU is required, check browser compatibility.',\n { cause: error }\n );\n }\n }\n else\n {\n throw new Error('WebGPU was forced but is not available in this browser.');\n }\n }\n\n // Force WebGL engine if specified\n if(forceEngine === 'webgl')\n {\n console.debug('Using forced WebGL engine');\n return _buildWebGLEngine(canvas, options as BabylonEngineOptions);\n }\n\n // Auto detection (default behavior)\n if(hasWebGPU())\n {\n try\n {\n console.debug('Using WebGPU');\n\n return _buildWebGPUEngine(canvas, options)\n .catch((error) =>\n {\n console.warn('WebGPU initialization failed, falling back to WebGL:', error);\n return _buildWebGLEngine(canvas, options as EngineOptions);\n });\n }\n catch (error)\n {\n console.warn('WebGPU initialization failed, falling back to WebGL:', error);\n }\n }\n else\n {\n console.warn('WebGPU not supported, falling back to WebGL.');\n }\n\n console.debug('Using WebGL');\n\n return _buildWebGLEngine(canvas, options as EngineOptions);\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n","//----------------------------------------------------------------------------------------------------------------------\n// Physics Utility\n//----------------------------------------------------------------------------------------------------------------------\n\nimport HavokPhysics, { type HavokPhysicsWithBindings } from '@babylonjs/havok';\nimport { HavokPlugin } from '@babylonjs/core';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Initializes the Havok WASM module and returns the raw bindings.\n *\n * Each BabylonJS scene needs its own HavokPlugin instance (because scene.dispose() disposes the\n * plugin attached to it). The raw WASM module can be shared across plugins safely.\n */\nexport async function initHavok() : Promise<HavokPhysicsWithBindings>\n{\n return await HavokPhysics();\n}\n\n/**\n * Creates a new HavokPlugin from an existing WASM instance.\n */\nexport function createHavokPlugin(havok : HavokPhysicsWithBindings) : HavokPlugin\n{\n return new HavokPlugin(true, havok);\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Raycast Utilities\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { AbstractMesh, Camera, PickingInfo, Ray, Scene, TransformNode, Vector3 } from '@babylonjs/core';\n\nimport type { GameEntity } from '../classes/entity.ts';\nimport type { GameEntityManager } from '../managers/entity.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport interface EntityPickResult\n{\n entity : GameEntity;\n point : Vector3;\n distance : number;\n normal : Vector3;\n mesh : AbstractMesh;\n pickingInfo : PickingInfo;\n}\n\nexport interface EntityPickFilter\n{\n type ?: string;\n tags ?: string[];\n predicate ?: (entity : GameEntity) => boolean;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class RaycastHelper\n{\n private _entityManager : GameEntityManager;\n\n constructor(entityManager : GameEntityManager)\n {\n this._entityManager = entityManager;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n pickEntity(scene : Scene, x : number, y : number, filter ?: EntityPickFilter) : EntityPickResult | null\n {\n const info = scene.pick(x, y);\n return this._toEntityResult(info, filter);\n }\n\n pickEntities(scene : Scene, x : number, y : number, filter ?: EntityPickFilter) : EntityPickResult[]\n {\n const infos = scene.multiPick(x, y);\n if(!infos) { return []; }\n\n const results : EntityPickResult[] = [];\n for(const info of infos)\n {\n const result = this._toEntityResult(info, filter);\n if(result)\n {\n results.push(result);\n }\n }\n return results;\n }\n\n pickEntityWithRay(scene : Scene, ray : Ray, filter ?: EntityPickFilter) : EntityPickResult | null\n {\n const info = scene.pickWithRay(ray);\n if(!info) { return null; }\n return this._toEntityResult(info, filter);\n }\n\n pickEntitiesWithRay(scene : Scene, ray : Ray, filter ?: EntityPickFilter) : EntityPickResult[]\n {\n const infos = scene.multiPickWithRay(ray);\n if(!infos) { return []; }\n\n const results : EntityPickResult[] = [];\n for(const info of infos)\n {\n const result = this._toEntityResult(info, filter);\n if(result)\n {\n results.push(result);\n }\n }\n return results;\n }\n\n pickEntityForward(\n scene : Scene,\n camera : Camera,\n maxDistance = 1000,\n filter ?: EntityPickFilter\n ) : EntityPickResult | null\n {\n const ray = camera.getForwardRay(maxDistance);\n return this.pickEntityWithRay(scene, ray, filter);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private\n //------------------------------------------------------------------------------------------------------------------\n\n private _toEntityResult(info : PickingInfo, filter ?: EntityPickFilter) : EntityPickResult | null\n {\n if(!info.hit || !info.pickedMesh || !info.pickedPoint)\n {\n return null;\n }\n\n const mesh = info.pickedMesh;\n\n // Look up entity on the picked mesh, then walk up parent chain\n let entity = this._entityManager.getByNode(mesh as unknown as TransformNode);\n if(!entity)\n {\n let current : TransformNode | null = (mesh as unknown as TransformNode).parent as TransformNode | null;\n while(current)\n {\n entity = this._entityManager.getByNode(current);\n if(entity) { break; }\n current = current.parent as TransformNode | null;\n }\n }\n\n if(!entity)\n {\n return null;\n }\n\n // Apply filter\n if(filter)\n {\n if(filter.type && entity.type !== filter.type)\n {\n return null;\n }\n\n if(filter.tags)\n {\n for(const tag of filter.tags)\n {\n if(!entity.hasTag(tag))\n {\n return null;\n }\n }\n }\n\n if(filter.predicate && !filter.predicate(entity))\n {\n return null;\n }\n }\n\n const normal = info.getNormal(true, true);\n\n return {\n entity,\n point: info.pickedPoint,\n distance: info.distance,\n normal: normal ?? { x: 0, y: 0, z: 0 } as unknown as Vector3,\n mesh,\n pickingInfo: info,\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// GameTimer - Pause-aware timer utilities\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A pollable cooldown handle.\n */\nexport interface CooldownHandle\n{\n readonly ready : boolean;\n reset() : void;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface DelayEntry\n{\n id : number;\n remaining : number;\n callback : () => void;\n}\n\ninterface IntervalEntry\n{\n id : number;\n periodMs : number;\n elapsed : number;\n callback : () => void;\n}\n\ninterface CooldownEntry\n{\n id : number;\n periodMs : number;\n remaining : number;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Pause-aware game timer providing delay, interval, and cooldown primitives.\n *\n * All times are in game-time milliseconds. When the game is paused, `tick()` is simply not called, so all timers\n * freeze automatically.\n */\nexport class GameTimer\n{\n private _nextId = 0;\n private _delays : DelayEntry[] = [];\n private _intervals : IntervalEntry[] = [];\n private _cooldowns : CooldownEntry[] = [];\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * One-shot timer. Fires the callback after `ms` milliseconds of game time.\n *\n * @returns A cancel function that prevents the callback from firing.\n */\n delay(ms : number, callback : () => void) : () => void\n {\n if(ms < 0)\n {\n throw new RangeError(`delay() requires ms >= 0, got ${ ms }`);\n }\n\n const id = this._nextId++;\n this._delays.push({ id, remaining: ms, callback });\n\n return () =>\n {\n this._delays = this._delays.filter((entry) => entry.id !== id);\n };\n }\n\n /**\n * Repeating timer. Fires the callback every `ms` milliseconds of game time.\n *\n * @returns A cancel function that stops the interval.\n */\n interval(ms : number, callback : () => void) : () => void\n {\n if(ms <= 0)\n {\n throw new RangeError(`interval() requires ms > 0, got ${ ms }`);\n }\n\n const id = this._nextId++;\n this._intervals.push({ id, periodMs: ms, elapsed: 0, callback });\n\n return () =>\n {\n this._intervals = this._intervals.filter((entry) => entry.id !== id);\n };\n }\n\n /**\n * Pollable cooldown. Starts ready; after calling `reset()`, becomes not-ready until `ms` elapses.\n */\n cooldown(ms : number) : CooldownHandle\n {\n const id = this._nextId++;\n const entry : CooldownEntry = { id, periodMs: ms, remaining: 0 };\n this._cooldowns.push(entry);\n\n return {\n get ready() : boolean\n {\n return entry.remaining <= 0;\n },\n reset() : void\n {\n entry.remaining = entry.periodMs;\n },\n };\n }\n\n /**\n * Cancel all active delays, intervals, and cooldowns.\n */\n cancelAll() : void\n {\n this._delays = [];\n this._intervals = [];\n\n // Set all cooldowns to ready before clearing, so existing CooldownHandle references aren't stuck\n for(const entry of this._cooldowns)\n {\n entry.remaining = 0;\n }\n\n this._cooldowns = [];\n }\n\n /**\n * Advance all timers by `dtMs` milliseconds. Called once per frame by the game loop.\n */\n tick(dtMs : number) : void\n {\n // Process delays\n const firedDelays : number[] = [];\n for(const entry of this._delays)\n {\n entry.remaining -= dtMs;\n if(entry.remaining <= 0)\n {\n entry.callback();\n firedDelays.push(entry.id);\n }\n }\n\n if(firedDelays.length > 0)\n {\n this._delays = this._delays.filter((entry) => !firedDelays.includes(entry.id));\n }\n\n // Process intervals\n for(const entry of this._intervals)\n {\n entry.elapsed += dtMs;\n while(entry.elapsed >= entry.periodMs)\n {\n entry.callback();\n entry.elapsed -= entry.periodMs;\n }\n }\n\n // Process cooldowns\n for(const entry of this._cooldowns)\n {\n if(entry.remaining > 0)\n {\n entry.remaining = Math.max(0, entry.remaining - dtMs);\n }\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Logger Interfaces\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Defines the available logging levels as string literals.\n */\nexport const LogLevels = [\n 'trace',\n 'debug',\n 'info',\n 'warn',\n 'error',\n 'none',\n] as const;\n\n/**\n * Type for LogLevel values\n */\nexport type LogLevel = typeof LogLevels[number];\n\n/**\n * Interface for a logging backend implementation.\n * Backends handle the actual output of log messages.\n */\nexport interface LoggingBackend\n{\n /**\n * Log a message at TRACE level\n *\n * @param category - Typically the class or module name\n * @param message\n * @param args\n */\n trace(category : string, message : string, ...args : unknown[]) : void;\n\n /**\n * Log a message at DEBUG level\n *\n * @param category - Typically the class or module name\n * @param message\n * @param args\n */\n debug(category : string, message : string, ...args : unknown[]) : void;\n\n /**\n * Log a message at INFO level\n *\n * @param category - Typically the class or module name\n * @param message\n * @param args\n */\n info(category : string, message : string, ...args : unknown[]) : void;\n\n /**\n * Log a message at WARN level\n *\n * @param category - Typically the class or module name\n * @param message\n * @param args\n */\n warn(category : string, message : string, ...args : unknown[]) : void;\n\n /**\n * Log a message at ERROR level\n *\n * @param category - Typically the class or module name\n * @param message\n * @param args\n */\n error(category : string, message : string, ...args : unknown[]) : void;\n\n /**\n * Start a timer with the specified label.\n *\n * @param category - Typically the class or module name\n * @param label - Must be unique; used to match with timeEnd()\n */\n time(category : string, label : string) : void;\n\n /**\n * End a timer with the specified label and log the time elapsed.\n *\n * @param category - Typically the class or module name\n * @param label - Must match a previous time() call\n */\n timeEnd(category : string, label : string) : void;\n}\n\n/**\n * Interface for a logger instance.\n * Each instance is typically associated with a specific category/component.\n */\nexport interface LoggerInterface\n{\n /** Log a message at TRACE level */\n trace(message : string, ...args : unknown[]) : void;\n\n /** Log a message at DEBUG level */\n debug(message : string, ...args : unknown[]) : void;\n\n /** Log a message at INFO level */\n info(message : string, ...args : unknown[]) : void;\n\n /** Log a message at WARN level */\n warn(message : string, ...args : unknown[]) : void;\n\n /** Log a message at ERROR level */\n error(message : string, ...args : unknown[]) : void;\n\n /** Start a timer with the given label */\n time(label : string) : void;\n\n /** End a timer and log the elapsed time */\n timeEnd(label : string) : void;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Sound Behavior\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { SoundState, type StaticSound, type TransformNode } from '@babylonjs/core';\n\nimport { GameEntityBehavior } from '../classes/entity.ts';\nimport type { GameEngine } from '../classes/gameEngine.ts';\nimport type { GameEvent } from '../classes/eventBus.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type { AudioManager } from '../managers/audio.ts';\nimport { SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Configuration for a single sound.\n */\nexport interface SoundConfig\n{\n /** Path to the sound file */\n url : string;\n\n /** Volume level (0-1), default 1 */\n volume ?: number;\n\n /** Whether to loop the sound, default false */\n loop ?: boolean;\n\n /** Whether to use spatial 3D audio, default false */\n spatial ?: boolean;\n\n /** Maximum distance for spatial audio, default 100 */\n maxDistance ?: number;\n\n /** Whether to autoplay when registered, default false */\n autoplay ?: boolean;\n\n /** Audio channel name (e.g., 'sfx', 'music'). Used with AudioManager. */\n channel ?: string;\n}\n\n/**\n * State interface for entities using SoundBehavior.\n * Entity state should include a `sounds` property with sound configurations.\n */\nexport interface SoundEntityState\n{\n /** Map of sound name to configuration */\n sounds ?: Record<string, SoundConfig>;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Behavior for managing sounds on an entity.\n *\n * Sounds are configured in the entity's state under the `sounds` property.\n * The behavior creates sounds via AudioManager (BabylonJS AudioV2).\n *\n * Requires AudioManager to be configured on the GameEngine. If no AudioManager is found,\n * a warning is logged and no sounds are created.\n *\n * @example\n * ```typescript\n * // Entity definition\n * {\n * type: 'player',\n * behaviors: [SoundBehavior, ...],\n * defaultState: {\n * sounds: {\n * jump: { url: 'sounds/jump.mp3', volume: 0.8, channel: 'sfx' },\n * footstep: { url: 'sounds/footstep.mp3', loop: true, channel: 'sfx' }\n * }\n * }\n * }\n *\n * // In another behavior, get the sound behavior and control sounds\n * const soundBehavior = entity.getBehavior(SoundBehavior);\n * soundBehavior?.play('jump');\n * ```\n */\nexport class SoundBehavior extends GameEntityBehavior<SoundEntityState>\n{\n name = 'sound';\n eventSubscriptions : string[] = [];\n\n /** Map of sound name to StaticSound instance */\n private _sounds = new Map<string, StaticSound>();\n\n /** Whether sounds have been initialized */\n private _initialized = false;\n\n /** AudioManager reference */\n private _audioManager : AudioManager | null = null;\n\n /** Node reference for spatial detach on cleanup */\n private _node : TransformNode | null = null;\n\n private _log : LoggerInterface = new SAGELogger('SoundBehavior');\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n onNodeAttached(node : TransformNode, gameEngine : GameEngine) : void\n {\n if(this._initialized)\n {\n return;\n }\n\n this._node = node;\n this._initialized = true;\n\n this._log = gameEngine.logger.getLogger('SoundBehavior');\n\n if(gameEngine.managers.audioManager)\n {\n this._audioManager = gameEngine.managers.audioManager;\n }\n else\n {\n this._log.warn('No AudioManager configured. Sounds will not be created.');\n return;\n }\n\n if(!this.entity)\n {\n return;\n }\n\n const state = this.entity.state;\n const soundConfigs = state.sounds;\n if(!soundConfigs)\n {\n return;\n }\n\n for(const [ name, config ] of Object.entries(soundConfigs))\n {\n this._createSound(name, config as SoundConfig);\n }\n }\n\n processEvent(_event : GameEvent, _state : SoundEntityState) : boolean\n {\n return false;\n }\n\n onReset(_state : SoundEntityState) : void\n {\n this._disposeSounds();\n }\n\n async destroy() : Promise<void>\n {\n this._disposeSounds();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods\n //------------------------------------------------------------------------------------------------------------------\n\n private _createSound(name : string, config : SoundConfig) : void\n {\n if(!this._audioManager || !this.entity)\n {\n return;\n }\n\n const entityId = this.entity.id;\n const node = this._node;\n\n this._audioManager.createSound(\n `${ entityId }_${ name }`,\n config.url,\n config.channel,\n {\n loop: config.loop ?? false,\n autoplay: config.autoplay ?? false,\n volume: config.volume ?? 1,\n spatialEnabled: config.spatial ?? false,\n spatialMaxDistance: config.maxDistance ?? 100,\n spatialDistanceModel: 'linear',\n }\n )\n .then((sound) =>\n {\n this._sounds.set(name, sound);\n\n if((config.spatial ?? false) && node)\n {\n sound.spatial.attach(node);\n }\n })\n .catch((err) =>\n {\n this._log.error(`Failed to create sound \"${ name }\": ${ err }`);\n });\n }\n\n private _disposeSounds() : void\n {\n for(const sound of this._sounds.values())\n {\n sound.dispose();\n }\n this._sounds.clear();\n this._initialized = false;\n this._node = null;\n this._audioManager = null;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Play a sound by name.\n *\n * @param name - The name of the sound to play. If omitted and only one sound exists, plays that sound.\n * @param time - Optional time offset in seconds to start playing from\n */\n play(name ?: string, time ?: number) : void\n {\n const sound = this._getSound(name);\n if(sound)\n {\n if(time !== undefined)\n {\n sound.play({ startOffset: time });\n }\n else\n {\n sound.play();\n }\n }\n }\n\n /**\n * Stop a sound by name.\n *\n * @param name - The name of the sound to stop. If omitted, stops all sounds.\n */\n stop(name ?: string) : void\n {\n if(name)\n {\n const sound = this._sounds.get(name);\n if(sound)\n {\n sound.stop();\n }\n }\n else\n {\n for(const sound of this._sounds.values())\n {\n sound.stop();\n }\n }\n }\n\n /**\n * Pause a sound by name.\n *\n * @param name - The name of the sound to pause. If omitted and only one sound exists, pauses that sound.\n */\n pause(name ?: string) : void\n {\n const sound = this._getSound(name);\n if(sound)\n {\n sound.pause();\n }\n }\n\n /**\n * Set the volume of a sound.\n *\n * @param volume - Volume level (0-1)\n * @param name - The name of the sound. If omitted, sets volume for all sounds.\n */\n setVolume(volume : number, name ?: string) : void\n {\n if(name)\n {\n const sound = this._sounds.get(name);\n if(sound)\n {\n sound.volume = volume;\n }\n }\n else\n {\n for(const sound of this._sounds.values())\n {\n sound.volume = volume;\n }\n }\n }\n\n /**\n * Check if a sound is currently playing.\n *\n * @param name - The name of the sound. If omitted and only one sound exists, checks that sound.\n */\n isPlaying(name ?: string) : boolean\n {\n const sound = this._getSound(name);\n return sound?.state === SoundState.Started;\n }\n\n /**\n * Get all registered sound names.\n */\n getSoundNames() : string[]\n {\n return Array.from(this._sounds.keys());\n }\n\n /**\n * Check if a sound with the given name exists.\n *\n * @param name\n */\n hasSound(name : string) : boolean\n {\n return this._sounds.has(name);\n }\n\n /**\n * Register a new sound at runtime. Replaces any existing sound with the same name.\n *\n * @param name\n * @param config\n */\n registerSound(name : string, config : SoundConfig) : void\n {\n if(!this._initialized || !this._audioManager)\n {\n throw new Error('SoundBehavior must be initialized before registering sounds');\n }\n\n const existing = this._sounds.get(name);\n if(existing)\n {\n existing.dispose();\n }\n\n this._createSound(name, config);\n }\n\n /**\n * Unregister and dispose a sound.\n *\n * @param name\n */\n unregisterSound(name : string) : void\n {\n const sound = this._sounds.get(name);\n if(sound)\n {\n sound.dispose();\n this._sounds.delete(name);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Helper Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Get a sound by name. If name is omitted and only one sound exists, returns that sound.\n */\n private _getSound(name ?: string) : StaticSound | undefined\n {\n if(name)\n {\n return this._sounds.get(name);\n }\n\n if(this._sounds.size === 1)\n {\n return this._sounds.values().next().value;\n }\n\n return undefined;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// State Machine\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { GameEventBus } from '../classes/eventBus.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface TransitionDef\n{\n guard ?: () => boolean;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A generic finite state machine with optional guard functions, lifecycle callbacks, and event bus integration.\n *\n * @template States - A string union of valid state names.\n */\nexport class StateMachine<States extends string>\n{\n private _currentState : States;\n private readonly transitions = new Map<string, TransitionDef>();\n private readonly wildcardTransitions = new Map<States, TransitionDef>();\n private readonly enterCallbacks = new Map<States, (() => void)[]>();\n private readonly exitCallbacks = new Map<States, (() => void)[]>();\n private readonly eventBus ?: GameEventBus<Record<string, unknown>>;\n\n constructor(initialState : States, eventBus ?: GameEventBus<Record<string, unknown>>)\n {\n this._currentState = initialState;\n this.eventBus = eventBus;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Properties\n //------------------------------------------------------------------------------------------------------------------\n\n get currentState() : States\n {\n return this._currentState;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Configuration\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a valid transition from one state to another, with an optional guard function.\n */\n addTransition(from : States, to : States, guard ?: () => boolean) : void\n {\n const key = `${ from }->${ to }`;\n this.transitions.set(key, { guard });\n }\n\n /**\n * Register a wildcard transition: allows transitioning to the given state from any state.\n */\n addTransitionFromAny(to : States, guard ?: () => boolean) : void\n {\n this.wildcardTransitions.set(to, { guard });\n }\n\n /**\n * Register a callback to run when entering a state.\n */\n onEnter(state : States, callback : () => void) : void\n {\n const callbacks = this.enterCallbacks.get(state) ?? [];\n callbacks.push(callback);\n this.enterCallbacks.set(state, callbacks);\n }\n\n /**\n * Register a callback to run when exiting a state.\n */\n onExit(state : States, callback : () => void) : void\n {\n const callbacks = this.exitCallbacks.get(state) ?? [];\n callbacks.push(callback);\n this.exitCallbacks.set(state, callbacks);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Queries\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Check if a transition to the given state is defined (ignoring guards).\n */\n canTransition(to : States) : boolean\n {\n const key = `${ this._currentState }->${ to }`;\n return this.transitions.has(key) || this.wildcardTransitions.has(to);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Transitions\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Execute a transition to the target state.\n * Flow: validate -> run guard -> call exit callbacks -> change state -> call enter callbacks.\n *\n * @throws If no transition is defined from the current state to the target, or if a guard rejects.\n */\n transition(to : States) : void\n {\n const key = `${ this._currentState }->${ to }`;\n let transitionDef = this.transitions.get(key);\n\n // Fall back to wildcard\n if(!transitionDef)\n {\n transitionDef = this.wildcardTransitions.get(to);\n }\n\n if(!transitionDef)\n {\n throw new Error(\n `No transition defined from '${ this._currentState }' to '${ to }'.`\n );\n }\n\n // Run guard\n if(transitionDef.guard && !transitionDef.guard())\n {\n throw new Error(\n `Guard rejected transition from '${ this._currentState }' to '${ to }'.`\n );\n }\n\n const previousState = this._currentState;\n\n // Exit callbacks\n const exitCbs = this.exitCallbacks.get(previousState);\n if(exitCbs)\n {\n for(const cb of exitCbs)\n {\n cb();\n }\n }\n\n // Emit exit event -- cast needed because template literal event types can't resolve conditionally\n if(this.eventBus)\n {\n this.eventBus.publish({\n type: `state:exit:${ previousState }`,\n payload: { from: previousState, to },\n } as { type : string; payload : unknown });\n }\n\n // Change state\n this._currentState = to;\n\n // Enter callbacks\n const enterCbs = this.enterCallbacks.get(to);\n if(enterCbs)\n {\n for(const cb of enterCbs)\n {\n cb();\n }\n }\n\n // Emit enter event -- cast needed because template literal event types can't resolve conditionally\n if(this.eventBus)\n {\n this.eventBus.publish({\n type: `state:enter:${ to }`,\n payload: { from: previousState, to },\n } as { type : string; payload : unknown });\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// State Machine Behavior\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { GameEntityBehavior } from '../classes/entity.ts';\nimport type { GameEvent } from '../classes/eventBus.ts';\nimport { StateMachine } from '../utils/stateMachine.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A single transition definition for the state machine behavior configuration.\n */\nexport interface TransitionConfig<States extends string>\n{\n from : States;\n to : States;\n guard ?: () => boolean;\n}\n\n/**\n * A wildcard transition definition (from any state).\n */\nexport interface WildcardTransitionConfig<States extends string>\n{\n to : States;\n guard ?: () => boolean;\n}\n\n/**\n * Configuration for creating a StateMachineBehavior subclass.\n */\nexport interface StateMachineBehaviorConfig<States extends string, EntityState extends object>\n{\n /** The initial state of the state machine. */\n initialState : States;\n\n /** The key in the entity's state object to update when state changes. */\n stateKey : keyof EntityState & string;\n\n /** Valid transitions between states. */\n transitions : TransitionConfig<States>[];\n\n /** Wildcard transitions (from any state). */\n wildcardTransitions ?: WildcardTransitionConfig<States>[];\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A behavior that wraps a {@link StateMachine} and ties it to entity state.\n *\n * Since behaviors are instantiated with `new BehaviorClass()` (no constructor args),\n * use the static factory `StateMachineBehavior.create(config)` to produce a configured subclass.\n *\n * @example\n * ```typescript\n * const EnemyAI = StateMachineBehavior.create<EnemyStates, EnemyState>({\n * initialState: 'idle',\n * stateKey: 'aiState',\n * transitions: [\n * { from: 'idle', to: 'patrol' },\n * { from: 'patrol', to: 'chase' },\n * { from: 'chase', to: 'attack' },\n * ],\n * wildcardTransitions: [\n * { to: 'dead' },\n * ],\n * });\n * ```\n */\nexport class StateMachineBehavior<\n States extends string = string,\n EntityState extends object = object,\n> extends GameEntityBehavior<EntityState>\n{\n name = 'stateMachine';\n eventSubscriptions : string[] = [];\n\n protected stateMachine : StateMachine<States>;\n protected stateKey : keyof EntityState & string;\n\n protected constructor()\n {\n super();\n\n // These will be overridden by the factory-produced subclass constructor.\n // Default to a dummy state machine that will be replaced.\n this.stateMachine = new StateMachine<States>('' as States);\n this.stateKey = '' as keyof EntityState & string;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * The current state of the state machine.\n */\n get currentState() : States\n {\n return this.stateMachine.currentState;\n }\n\n /**\n * Transition to a new state. Updates the entity's state and emits a state changed event.\n *\n * @throws If the transition is not defined or a guard rejects it.\n */\n transition(to : States) : void\n {\n this.stateMachine.transition(to);\n\n // Update entity state\n if(this.entity)\n {\n (this.entity.state as Record<string, unknown>)[this.stateKey] = to;\n this.$emitStateChanged();\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Behavior Implementation\n //------------------------------------------------------------------------------------------------------------------\n\n processEvent(_event : GameEvent, _state : EntityState) : boolean\n {\n return false;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Static Factory\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Creates a configured StateMachineBehavior subclass with the given state machine configuration baked in.\n * The returned class can be used directly as a behavior constructor.\n */\n static create<States extends string, EntityState extends object>(\n config : StateMachineBehaviorConfig<States, EntityState>\n ) : new () => StateMachineBehavior<States, EntityState>\n {\n // Capture config in closure\n const { initialState, stateKey, transitions, wildcardTransitions } = config;\n\n class ConfiguredStateMachineBehavior extends StateMachineBehavior<States, EntityState>\n {\n constructor()\n {\n super();\n\n this.stateKey = stateKey;\n this.stateMachine = new StateMachine<States>(initialState);\n\n for(const tr of transitions)\n {\n this.stateMachine.addTransition(tr.from, tr.to, tr.guard);\n }\n\n if(wildcardTransitions)\n {\n for(const wt of wildcardTransitions)\n {\n this.stateMachine.addTransitionFromAny(wt.to, wt.guard);\n }\n }\n }\n }\n\n return ConfiguredStateMachineBehavior;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Collider Property Handler\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { AbstractMesh, Mesh, PhysicsAggregate, PhysicsShapeType } from '@babylonjs/core';\n\nimport type { LevelManager } from '../managers/level.ts';\nimport { SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nconst logger = new SAGELogger('ColliderHandler');\n\n/**\n * Creates a physics aggregate for a mesh with the given shape type.\n */\nfunction createPhysicsAggregate(mesh : Mesh, shapeType : PhysicsShapeType, mass : number) : void\n{\n const scene = mesh.getScene();\n new PhysicsAggregate(mesh, shapeType, { mass }, scene);\n}\n\n/**\n * Handles mesh collider setup, using a child mesh if marked with collider_mesh.\n */\nfunction handleMeshCollider(mesh : Mesh, mass : number) : void\n{\n const colliderChild = mesh.getChildMeshes().find(\n (child) => child.metadata?.collider_mesh === true\n );\n\n if(colliderChild instanceof Mesh)\n {\n colliderChild.isVisible = false;\n createPhysicsAggregate(colliderChild, PhysicsShapeType.MESH, mass);\n }\n else\n {\n createPhysicsAggregate(mesh, PhysicsShapeType.MESH, mass);\n }\n}\n\n/**\n * Register the collider property handler.\n *\n * Supports the following properties on scene nodes:\n * - `collider` (string): Collider type - \"box\", \"sphere\", \"mesh\", or \"none\"\n * - `collider_mesh` (boolean): On a child node, marks it as the collision source for parent's \"mesh\" collider\n * - `collider_mass` (number): Mass for the physics body. Defaults to 0 (static). Set > 0 for dynamic bodies.\n * - `collider_kinematic` (boolean): When true, sets `disablePreStep = false` so the physics body\n * tracks mesh transform each frame. Required for animated colliders (doors, elevators, platforms).\n *\n * For \"mesh\" colliders, if a child node has `collider_mesh: true`, that child's geometry\n * is used for collision and the child is made invisible.\n *\n * @param levelManager\n */\nexport function registerColliderHandler(levelManager : LevelManager) : void\n{\n levelManager.registerPropertyHandler('collider', (node, value, level, _gameEngine) =>\n {\n const colliderType = value as string;\n logger.debug(`Processing collider: ${ node.name } -> ${ colliderType } (isMesh: ${ node instanceof Mesh })`);\n\n if(!level.scene)\n {\n throw new Error('Scene not available for collider handler');\n }\n\n if(!(node instanceof Mesh))\n {\n logger.warn(`Skipping collider for ${ node.name }: not a Mesh instance`);\n return;\n }\n\n const mass = (node.metadata?.collider_mass as number | undefined) ?? 0;\n\n switch (colliderType)\n {\n case 'box':\n createPhysicsAggregate(node, PhysicsShapeType.BOX, mass);\n break;\n\n case 'sphere':\n createPhysicsAggregate(node, PhysicsShapeType.SPHERE, mass);\n break;\n\n case 'mesh':\n handleMeshCollider(node, mass);\n break;\n\n case 'none':\n node.checkCollisions = false;\n break;\n\n default:\n logger.warn(`Unknown collider type: ${ colliderType }`);\n }\n\n // Enable kinematic mode -- physics body tracks mesh transform each frame\n if(node.metadata?.collider_kinematic)\n {\n const mesh = node as unknown as AbstractMesh;\n if(mesh.physicsBody)\n {\n mesh.physicsBody.disablePreStep = false;\n }\n }\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// LOD (Level of Detail) Property Handler\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { Mesh } from '@babylonjs/core';\n\nimport type { LevelManager } from '../managers/level.ts';\nimport { SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nconst logger = new SAGELogger('LodHandler');\n\n/**\n * Parses a comma-separated string of distances into an array of numbers.\n * Filters out invalid (NaN) values.\n */\nfunction parseDistances(distancesStr : string) : number[]\n{\n return distancesStr\n .split(',')\n .map((str) => parseFloat(str.trim()))\n .filter((num) => !isNaN(num));\n}\n\n/**\n * Register the LOD (Level of Detail) property handler.\n *\n * Supports the following property on scene nodes:\n * - `lod_distances` (string): Comma-separated distances for LOD levels\n *\n * The mesh's children are used as LOD levels in order.\n * Example: \"10,25,50\" means:\n * - 0-10 units: use first child mesh\n * - 10-25 units: use second child mesh\n * - 25-50 units: use third child mesh\n * - 50+ units: use null (not visible)\n *\n * @param levelManager\n */\nexport function registerLodHandler(levelManager : LevelManager) : void\n{\n levelManager.registerPropertyHandler('lod_distances', (node, value, _level, _gameEngine) =>\n {\n const distancesStr = value as string;\n\n if(!(node instanceof Mesh))\n {\n return;\n }\n\n const distances = parseDistances(distancesStr);\n\n if(distances.length === 0)\n {\n logger.warn(`Invalid lod_distances value: ${ distancesStr }`);\n return;\n }\n\n const children = node.getChildMeshes(true) as Mesh[];\n const levelCount = Math.min(distances.length, children.length);\n\n // Add LOD levels for each distance/child pair\n for(let i = 0; i < levelCount; i++)\n {\n node.addLODLevel(distances[i], children[i]);\n }\n\n // Add null LOD for beyond the last distance (not visible)\n const lastDistance = distances[distances.length - 1];\n node.addLODLevel(lastDistance, null);\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Occluder Property Handler\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { Mesh } from '@babylonjs/core';\n\nimport type { LevelManager } from '../managers/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Register the occluder property handler.\n *\n * Supports the following property on scene nodes:\n * - `occluder` (boolean): If true, marks the mesh as an occluder for occlusion culling\n *\n * Occluder meshes are used by the engine to hide objects that are behind them,\n * improving rendering performance. The occluder mesh itself is made invisible.\n *\n * @param levelManager\n */\nexport function registerOccluderHandler(levelManager : LevelManager) : void\n{\n levelManager.registerPropertyHandler('occluder', (node, value, _level, _gameEngine) =>\n {\n if(!value)\n {\n return;\n }\n\n // Only meshes can be occluders\n if(!(node instanceof Mesh))\n {\n return;\n }\n\n // Mark as occluder and hide\n // Note: isOccluder exists at runtime but is missing from @babylonjs/core type definitions as of 9.0\n (node as Mesh & { isOccluder : boolean }).isOccluder = true;\n node.isVisible = false;\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Metadata Utilities\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Collect properties with a common prefix into a single object.\n * Useful for gathering related properties from node metadata.\n *\n * For example, given metadata with:\n * - sound_volume: 0.5\n * - sound_loop: true\n * - sound_spatial: false\n *\n * Calling collectPrefixedProperties(metadata, 'sound') returns:\n * { volume: 0.5, loop: true, spatial: false }\n *\n * @param metadata - The node metadata to search\n * @param prefix - The prefix to match (without trailing underscore)\n * @returns Object with sub-properties (prefix and underscore removed from keys)\n */\nexport function collectPrefixedProperties(\n metadata : Record<string, unknown>,\n prefix : string\n) : Record<string, unknown>\n{\n const result : Record<string, unknown> = {};\n const prefixWithUnderscore = `${ prefix }_`;\n\n for(const [ key, value ] of Object.entries(metadata))\n {\n if(key.startsWith(prefixWithUnderscore))\n {\n const subKey = key.slice(prefixWithUnderscore.length);\n result[subKey] = value;\n }\n }\n\n return result;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Sound Property Handler\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { collectPrefixedProperties } from '../utils/metadata.ts';\nimport { SAGELogger } from '../utils/logger.ts';\nimport type { LevelManager } from '../managers/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nconst logger = new SAGELogger('SoundHandler');\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Register the sound property handler.\n *\n * Supports the following properties on scene nodes:\n * - `sound` (string): Path to the sound file\n * - `sound_volume` (number): Volume 0-1, default 1\n * - `sound_loop` (boolean): Whether to loop, default true\n * - `sound_spatial` (boolean): Whether to use 3D spatial audio, default true\n * - `sound_distance` (number): Maximum distance for spatial audio, default 100\n * - `sound_autoplay` (boolean): Whether to autoplay, default true\n * - `sound_channel` (string): Audio channel name, default 'ambient'\n *\n * @param levelManager\n */\nexport function registerSoundHandler(levelManager : LevelManager) : void\n{\n levelManager.registerPropertyHandler('sound', (node, value, level, gameEngine) =>\n {\n const soundPath = value as string;\n const scene = level.scene;\n\n if(!scene)\n {\n throw new Error('Scene not available for sound handler');\n }\n\n // Collect prefixed properties\n const extras = collectPrefixedProperties(\n node.metadata as Record<string, unknown>,\n 'sound'\n );\n\n const volume = (extras.volume as number) ?? 1;\n const loop = (extras.loop as boolean) ?? true;\n const spatial = (extras.spatial as boolean) ?? true;\n const maxDistance = (extras.distance as number) ?? 100;\n const autoplay = (extras.autoplay as boolean) ?? true;\n const channel = (extras.channel as string) ?? 'ambient';\n\n const audioManager = gameEngine.managers.audioManager;\n\n if(!audioManager)\n {\n logger.warn(`No AudioManager available. Sound for \"${ node.name }\" skipped.`);\n return;\n }\n\n audioManager.createSound(\n `${ node.name }_sound`,\n soundPath,\n channel,\n {\n loop,\n autoplay,\n volume,\n spatialEnabled: spatial,\n spatialMaxDistance: maxDistance,\n spatialDistanceModel: 'linear',\n }\n )\n .then((sound) =>\n {\n if(spatial)\n {\n sound.spatial.attach(node);\n }\n })\n .catch((err) =>\n {\n logger.error(`Failed to create sound for node \"${ node.name }\": ${ err }`);\n });\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Trigger Property Handler\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { AbstractMesh, ActionManager, ExecuteCodeAction } from '@babylonjs/core';\n\nimport type { GameEventBus } from '../classes/eventBus.ts';\nimport type { LevelManager } from '../managers/level.ts';\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Registers an intersection action (enter or exit) for a trigger mesh against a target mesh.\n */\nfunction registerIntersectionAction(\n actionManager : ActionManager,\n triggerName : string,\n eventBus : GameEventBus,\n targetMesh : AbstractMesh,\n actionType : 'enter' | 'exit'\n) : void\n{\n const triggerType = actionType === 'enter'\n ? ActionManager.OnIntersectionEnterTrigger\n : ActionManager.OnIntersectionExitTrigger;\n\n const eventType = `trigger:${ actionType }` as const;\n\n actionManager.registerAction(\n new ExecuteCodeAction(\n {\n trigger: triggerType,\n parameter: { mesh: targetMesh },\n },\n (evt) =>\n {\n eventBus.publish({\n type: eventType,\n payload: {\n trigger: triggerName,\n other: evt.source as AbstractMesh,\n },\n });\n }\n )\n );\n}\n\n/**\n * Sets up intersection triggers between a trigger mesh and a target mesh.\n */\nfunction registerTriggersForMesh(\n actionManager : ActionManager,\n triggerName : string,\n eventBus : GameEventBus,\n targetMesh : AbstractMesh\n) : void\n{\n registerIntersectionAction(actionManager, triggerName, eventBus, targetMesh, 'enter');\n registerIntersectionAction(actionManager, triggerName, eventBus, targetMesh, 'exit');\n}\n\n/**\n * Register the trigger property handler.\n *\n * Supports the following property on scene nodes:\n * - `trigger` (string): The trigger zone name\n *\n * When an object enters or exits the trigger zone, events are published:\n * - `trigger:enter` with payload { trigger: string, other: AbstractMesh }\n * - `trigger:exit` with payload { trigger: string, other: AbstractMesh }\n *\n * The trigger mesh is made invisible but retains collision detection.\n * Intersection checks are registered against all non-trigger meshes in the scene,\n * including meshes added after the handler runs (e.g. dynamically spawned entities).\n *\n * @param levelManager\n */\nexport function registerTriggerHandler(levelManager : LevelManager) : void\n{\n levelManager.registerPropertyHandler('trigger', (node, value, level, gameEngine) =>\n {\n const triggerName = value as string;\n const scene = level.scene;\n\n if(!scene)\n {\n throw new Error('Scene not available for trigger handler');\n }\n\n if(!(node instanceof AbstractMesh))\n {\n return;\n }\n\n const eventBus = gameEngine.eventBus;\n\n // Make the trigger invisible but keep collision\n node.isVisible = false;\n node.checkCollisions = true;\n\n // Create action manager if needed\n const actionManager = (node.actionManager as ActionManager) ?? new ActionManager(scene);\n node.actionManager = actionManager;\n\n // Register intersection triggers against all existing non-trigger meshes\n for(const mesh of scene.meshes)\n {\n if(mesh !== node && !mesh.metadata?.trigger)\n {\n registerTriggersForMesh(actionManager, triggerName, eventBus, mesh);\n }\n }\n\n // Register triggers for meshes added later (e.g. dynamically spawned entities)\n const observer = scene.onNewMeshAddedObservable.add((mesh) =>\n {\n if(mesh !== node && !mesh.metadata?.trigger)\n {\n registerTriggersForMesh(actionManager, triggerName, eventBus, mesh);\n }\n });\n\n // Clean up observer when the trigger mesh is disposed\n node.onDisposeObservable.add(() =>\n {\n scene.onNewMeshAddedObservable.remove(observer);\n });\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Visible Property Handler\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { AbstractMesh, type TransformNode } from '@babylonjs/core';\nimport type { LevelManager } from '../managers/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Register the visible property handler.\n *\n * Supports the following property on scene nodes:\n * - `visible` (boolean | string | number): Controls mesh visibility.\n * Accepts true/false, 'true'/'false', or 1/0.\n *\n * @param levelManager\n */\nexport function registerVisibleHandler(levelManager : LevelManager) : void\n{\n levelManager.registerPropertyHandler('visible', (node : TransformNode, value : unknown, _level, _gameEngine) =>\n {\n // Only meshes have isVisible property\n if(!(node instanceof AbstractMesh))\n {\n return;\n }\n\n // Parse value as boolean\n // Supports: true/false (boolean), 'true'/'false' (string), 1/0 (number)\n let visible = true;\n\n if(typeof value === 'boolean')\n {\n visible = value;\n }\n else if(typeof value === 'string')\n {\n visible = value.toLowerCase() === 'true' || value === '1';\n }\n else if(typeof value === 'number')\n {\n visible = value !== 0;\n }\n\n node.isVisible = visible;\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Property Handlers Index\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { LevelManager } from '../managers/level.ts';\n\nimport { registerColliderHandler } from './collider.ts';\nimport { registerLodHandler } from './lod.ts';\nimport { registerOccluderHandler } from './occluder.ts';\nimport { applyPostProcessing } from './postProcessing.ts';\nimport { registerSoundHandler } from './sound.ts';\nimport { registerTriggerHandler } from './trigger.ts';\nimport { registerVisibleHandler } from './visible.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport {\n applyPostProcessing,\n registerColliderHandler,\n registerLodHandler,\n registerOccluderHandler,\n registerSoundHandler,\n registerTriggerHandler,\n registerVisibleHandler,\n};\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Register all built-in property handlers with the given LevelManager.\n * Call this during game initialization to enable default property handling.\n *\n * @param levelManager\n */\nexport function registerAllPropertyHandlers(levelManager : LevelManager) : void\n{\n registerColliderHandler(levelManager);\n registerLodHandler(levelManager);\n registerOccluderHandler(levelManager);\n registerSoundHandler(levelManager);\n registerTriggerHandler(levelManager);\n registerVisibleHandler(levelManager);\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// SkewedAspect Game Engine\n//----------------------------------------------------------------------------------------------------------------------\n\n// Interfaces\nimport type { GameCanvas, RenderEngineOptions, SageOptions } from './interfaces/game.ts';\n\n// Base Classes\nimport { GameEngine } from './classes/gameEngine.ts';\nimport { GameEventBus } from './classes/eventBus.ts';\n\n// Engines\nimport { AudioEngine } from './engines/audio.ts';\nimport { SceneEngine } from './engines/scene.ts';\n\n// Managers\nimport { AssetManager } from './managers/asset.ts';\nimport { BindingManager } from './managers/binding.ts';\nimport { GameManager } from './managers/game.ts';\nimport { GameEntityManager } from './managers/entity.ts';\nimport { LevelManager } from './managers/level.ts';\nimport { AudioManager } from './managers/audio.ts';\nimport { SaveManager } from './managers/save.ts';\nimport { UserInputManager } from './managers/input.ts';\n\n// Utils\nimport { createEngine } from './utils/graphics.ts';\nimport { createHavokPlugin, initHavok } from './utils/physics.ts';\nimport { LoggingUtility } from './utils/logger.ts';\nimport { RaycastHelper } from './utils/raycast.ts';\nimport { GameTimer } from './utils/timer.ts';\nimport { VERSION } from './utils/version.ts';\nimport type { GameEntityDefinition } from './interfaces/entity.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Creates and initializes a fully-wired GameEngine with rendering, physics, input, and entity management.\n *\n * @param canvas\n * @param entities - Entity definitions to register before the engine starts\n * @param options\n */\nexport async function createGameEngine(\n canvas : GameCanvas,\n entities : GameEntityDefinition[],\n options : SageOptions = {}\n) : Promise<GameEngine>\n{\n // Initialize logging\n const logger = new LoggingUtility(options.logLevel || 'debug');\n const engineLogger = logger.getLogger('SAGE');\n\n engineLogger.info(`Initializing SAGE Game Engine v${ VERSION }...`);\n\n // Initialize BabylonJS stuff\n engineLogger.debug('Creating rendering engine...');\n\n const renderOptions : RenderEngineOptions = {\n antialias: true,\n adaptToDeviceRatio: true,\n ...options.renderOptions,\n };\n\n const engine = await createEngine(canvas, renderOptions, options.largeWorldRendering ?? false);\n\n engineLogger.debug('Creating physics engine...');\n const havok = await initHavok();\n const physics = createHavokPlugin(havok);\n\n // Initialize internals\n engineLogger.debug('Creating event bus...');\n const eventBus = new GameEventBus(logger);\n\n // Initialize SAGE Engines\n engineLogger.debug('Creating scene engine...');\n const sceneEngine = new SceneEngine(canvas, engine, havok, logger);\n\n // Initialize SAGE Managers\n engineLogger.debug('Creating managers...');\n const assetManager = new AssetManager(eventBus, sceneEngine, logger);\n const inputManager = new UserInputManager(eventBus, canvas as HTMLElement, logger);\n const bindingManager = new BindingManager(eventBus, logger);\n const entityManager = new GameEntityManager(eventBus, logger, bindingManager);\n const levelManager = new LevelManager(eventBus, logger);\n const saveManager = new SaveManager(entityManager, levelManager, logger);\n const gameManager = new GameManager(engine, eventBus, entityManager, inputManager, levelManager, logger);\n\n // Initialize raycasting\n engineLogger.debug('Creating raycast helper...');\n const raycast = new RaycastHelper(entityManager);\n\n // Initialize timer\n engineLogger.debug('Creating game timer...');\n const timer = new GameTimer();\n gameManager.registerFrameCallback((dt) => timer.tick(dt * 1000));\n\n // Initialize audio system (opt-in)\n let audioEngine : AudioEngine | undefined;\n let audioManager : AudioManager | undefined;\n\n if(options.audioChannels && options.audioChannels.length > 0)\n {\n engineLogger.debug('Creating audio engine...');\n audioEngine = new AudioEngine(logger);\n await audioEngine.initialize();\n\n engineLogger.debug('Creating audio manager...');\n audioManager = new AudioManager(audioEngine, logger);\n await audioManager.initialize(options.audioChannels);\n }\n\n engineLogger.info(`SAGE Game Engine v${ VERSION } initialized successfully.`);\n\n // Register entities\n engineLogger.debug('Loading entities...');\n for(const entity of entities)\n {\n entityManager.registerEntityDefinition(entity);\n }\n\n // Register default input bindings\n engineLogger.debug('Registering default input bindings...');\n if(options.bindings)\n {\n for(const binding of options.bindings)\n {\n bindingManager.registerBinding(binding);\n }\n }\n\n const ge = new GameEngine(\n canvas,\n engine,\n physics,\n eventBus,\n logger,\n raycast,\n timer,\n\n // Engines\n {\n sceneEngine,\n audioEngine,\n },\n\n // Managers\n {\n assetManager,\n bindingManager,\n entityManager,\n gameManager,\n inputManager,\n levelManager,\n saveManager,\n audioManager,\n },\n\n options.largeWorldRendering ?? false\n );\n\n // Late-bind: these managers are constructed before GameEngine (because GameEngine takes them as\n // constructor args), but they need a back-reference at runtime. This must happen before any level\n // loads or entity attaches, so do it immediately after construction. Do not reorder.\n entityManager.$setGameEngine(ge);\n levelManager.$setGameEngine(ge);\n\n return ge;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n// Exports\n//----------------------------------------------------------------------------------------------------------------------\n\nexport * from './interfaces/action.ts';\nexport * from './interfaces/binding.ts';\nexport * from './interfaces/entity.ts';\nexport * from './interfaces/game.ts';\nexport * from './interfaces/input.ts';\nexport * from './interfaces/logger.ts';\n\n// Export base classes\nexport { AssetManager, AudioEngine, GameEngine, GameEventBus, SceneEngine };\n\nexport * from './classes/eventBus.ts';\nexport * from './classes/entity.ts';\nexport * from './classes/level.ts';\nexport * from './classes/gameLevel.ts';\nexport * from './behaviors/index.ts';\nexport * from './events/index.ts';\nexport * from './handlers/index.ts';\nexport * from './interfaces/level.ts';\nexport * from './interfaces/lifecycle.ts';\nexport * from './utils/logger.ts';\nexport * from './utils/metadata.ts';\nexport * from './utils/timer.ts';\nexport * from './utils/version.ts';\n\n// Export manager types\nexport type { FrameCallback } from './managers/game.ts';\nexport type { CreateEntityOptions } from './managers/entity.ts';\nexport type { TransitionOptions } from './managers/level.ts';\nexport { AudioManager } from './managers/audio.ts';\nexport type { ChannelInfo } from './managers/audio.ts';\nexport { OutlineManager } from './managers/outline.ts';\nexport { SaveManager } from './managers/save.ts';\nexport type { SaveData, SerializedEntity } from './managers/save.ts';\n\n// Export utilities\nexport { generateId } from './utils/id.ts';\nexport { RaycastHelper } from './utils/raycast.ts';\nexport type { EntityPickResult, EntityPickFilter } from './utils/raycast.ts';\nexport { StateMachine } from './utils/stateMachine.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n"],"x_google_ignoreList":[20],"mappings":"40CAMA,IAAa,EAAA,QCwEA,EAAb,KACA,CACI,OACA,aACA,QACA,SACA,QACA,SACA,OACA,QACA,MACA,oBACA,QAAiB,GAEjB,KAGA,iBAA6C,KAC7C,aAAyC,KACzC,gBAA4C,KAY5C,YACI,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EAAsB,GAE1B,CACI,KAAK,OAAS,EACd,KAAK,aAAe,EACpB,KAAK,QAAU,EACf,KAAK,SAAW,EAChB,KAAK,OAAS,EACd,KAAK,QAAU,EACf,KAAK,MAAQ,EACb,KAAK,QAAU,EACf,KAAK,SAAW,EAChB,KAAK,oBAAsB,EAE3B,KAAK,KAAO,EAAO,UAAU,aAAa,CAY9C,cAAc,EACd,CACI,GAAG,KAAK,mBAAqB,KAEzB,MAAU,MAAM,0FAA0F,CAE9G,KAAK,iBAAmB,EAQ5B,QAAQ,EACR,CACI,GAAG,KAAK,eAAiB,KAErB,MAAU,MAAM,uFAAuF,CAE3G,KAAK,aAAe,EAQxB,WAAW,EACX,CACI,GAAG,KAAK,kBAAoB,KAExB,MAAU,MAAM,0FAA0F,CAE9G,KAAK,gBAAkB,EA2B3B,UACI,EACA,EAEJ,CACI,OAAO,KAAK,SAAS,UAAU,EAAW,EAAS,CA6BvD,gBACI,EACA,EAEJ,CACI,OAAO,KAAK,SAAS,UACjB,UAAW,IACX,EACH,CAUL,MAAM,OACN,CACQ,KAAK,QA4BL,KAAK,KAAK,KAAK,kDAAkD,EA1BjE,KAAK,KAAK,KAAK,+CAAgD,EAAS,MAAM,CAG3E,KAAK,mBAEJ,KAAK,KAAK,MAAM,gCAAgC,CAChD,MAAM,KAAK,iBAAiB,KAAK,EAIrC,MAAM,KAAK,SAAS,YAAY,OAAO,CAEvC,KAAK,KAAK,KAAK,mCAAmC,CAG/C,KAAK,eAEJ,KAAK,KAAK,MAAM,4BAA4B,CAC5C,MAAM,KAAK,aAAa,KAAK,EAIjC,KAAK,QAAU,IAWvB,MAAM,MACN,CACI,GAAG,KAAK,QACR,CACI,KAAK,KAAK,KAAK,+CAAgD,EAAS,MAAM,CAE9E,IAAI,EAA+B,KAGnC,GAAG,KAAK,gBAEJ,GACA,CACI,KAAK,KAAK,MAAM,+BAA+B,CAC/C,MAAM,KAAK,gBAAgB,KAAK,OAE7B,EACP,CACI,KAAK,KAAK,MAAM,6BAA8B,IAAO,CACrD,IAAkC,EAK1C,IAAI,IAAM,KAAc,OAAO,KAAK,KAAK,SAAS,CAClD,CACI,IAAM,EAAU,KAAK,SAAS,GAC9B,GAAG,EAEC,GACA,CACI,KAAK,KAAK,MAAM,yBAA0B,IAAc,CAExD,MAAM,EAAQ,WAAW,OAEtB,EACP,CACI,KAAK,KAAK,MAAM,8BAA+B,EAAY,IAAK,IAAO,CACvE,IAAkC,GAM9C,IAAI,IAAM,KAAa,OAAO,KAAK,KAAK,QAAQ,CAChD,CACI,IAAM,EAAS,KAAK,QAAQ,GAC5B,GAAG,EAEC,GACA,CACI,KAAK,KAAK,MAAM,wBAAyB,IAAa,CAEtD,MAAM,EAAO,WAAW,OAErB,EACP,CACI,KAAK,KAAK,MAAM,6BAA8B,EAAW,IAAK,IAAO,CACrE,IAAkC,GAS9C,GAJA,KAAK,QAAU,GACf,KAAK,KAAK,KAAK,mCAAmC,CAG/C,EAEC,MAAM,OAKV,KAAK,KAAK,KAAK,6CAA6C,GCpW3D,EAAb,KACA,CACI,OAEA,aACA,CACI,KAAK,OAAS,IAAI,IAMtB,iBAAyB,EACzB,CAEI,IAAM,EAAsC,CACxC,MAAO,iBACP,MAAO,iBACP,KAAM,iBACN,KAAM,iBACN,MAAO,iBACP,MAAO,iBACP,KAAM,iBACT,CAED,OAAO,EAAO,IAAU,EAAO,KAMnC,cAAsB,EAAmB,EACzC,CAEI,IAAM,EAAM,IAAI,KAYhB,MAAO,CACH,GANc,IANJ,EAAI,UAAU,CAAG,IAAM,GAMP,GALd,EAAI,YAAY,CAAC,UAAU,CACtC,SAAS,EAAG,IAAI,CAIsB,GAH3B,EAAI,YAAY,CAAC,UAAU,CACtC,SAAS,EAAG,IAAI,CAEmC,GAD3C,EAAI,UAAU,EAAI,GAAK,KAAO,KACuB,GAM/C,KAJA,EAAM,aAAa,CAID,MAAO,EAAU,KAClD,KAAK,iBAAiB,EAAM,CAC5B,GACA,GACH,CAGL,MAAM,EAAmB,EAAkB,GAAG,EAC9C,CACI,GAAM,CAAE,EAAQ,EAAc,EAAY,GAAe,KAAK,cAAc,EAAU,QAAQ,CAC9F,QAAQ,MAAM,EAAQ,EAAc,EAAY,EAAY,EAAS,GAAG,EAAK,CAGjF,MAAM,EAAmB,EAAkB,GAAG,EAC9C,CACI,GAAM,CAAE,EAAQ,EAAc,EAAY,GAAe,KAAK,cAAc,EAAU,QAAQ,CAC9F,QAAQ,MAAM,EAAQ,EAAc,EAAY,EAAY,EAAS,GAAG,EAAK,CAGjF,KAAK,EAAmB,EAAkB,GAAG,EAC7C,CACI,GAAM,CAAE,EAAQ,EAAc,EAAY,GAAe,KAAK,cAAc,EAAU,OAAO,CAC7F,QAAQ,KAAK,EAAQ,EAAc,EAAY,EAAY,EAAS,GAAG,EAAK,CAGhF,KAAK,EAAmB,EAAkB,GAAG,EAC7C,CACI,GAAM,CAAE,EAAQ,EAAc,EAAY,GAAe,KAAK,cAAc,EAAU,OAAO,CAC7F,QAAQ,KAAK,EAAQ,EAAc,EAAY,EAAY,EAAS,GAAG,EAAK,CAGhF,MAAM,EAAmB,EAAkB,GAAG,EAC9C,CACI,GAAM,CAAE,EAAQ,EAAc,EAAY,GAAe,KAAK,cAAc,EAAU,QAAQ,CAC9F,QAAQ,MAAM,EAAQ,EAAc,EAAY,EAAY,EAAS,GAAG,EAAK,CAMjF,KAAK,EAAmB,EACxB,CACI,IAAM,EAAW,GAAI,EAAU,GAAI,IACnC,KAAK,OAAO,IAAI,EAAU,YAAY,KAAK,CAAC,CAMhD,QAAQ,EAAmB,EAC3B,CACI,IAAM,EAAW,GAAI,EAAU,GAAI,IAC7B,EAAY,KAAK,OAAO,IAAI,EAAS,CAE3C,GAAG,IAAc,IAAA,GACjB,CACI,IAAM,EAAW,YAAY,KAAK,CAAG,EACrC,KAAK,OAAO,OAAO,EAAS,CAE5B,GAAM,CAAE,EAAQ,EAAc,EAAY,GAAe,KAAK,cAAc,EAAU,QAAQ,CAC9F,QAAQ,KACJ,GAAI,EAAQ,UAAW,EAAO,iBAAkB,EAAS,QAAQ,EAAE,CAAE,IACrE,EACA,EACA,EACH,MAID,QAAQ,KAAK,IAAK,EAAU,YAAa,EAAO,kBAAkB,GCrHjE,EAAb,KACA,CACI,MAAM,EAAoB,EAAmB,GAAG,EAChD,EAIA,MAAM,EAAoB,EAAmB,GAAG,EAChD,EAIA,KAAK,EAAoB,EAAmB,GAAG,EAC/C,EAIA,KAAK,EAAoB,EAAmB,GAAG,EAC/C,EAIA,MAAM,EAAoB,EAAmB,GAAG,EAChD,EAIA,KAAK,EAAoB,EACzB,EAIA,QAAQ,EAAoB,EAC5B,IC1BS,EAAb,KACA,CACI,SACA,QACA,SAEA,YAAY,EAAmB,EAAsB,OAAQ,EAA2B,IAAI,EAC5F,CACI,KAAK,SAAW,EAChB,KAAK,QAAU,EACf,KAAK,SAAW,EAMpB,eAAsB,EAA0B,EAChD,CACI,KAAK,QAAU,EACf,KAAK,SAAW,EAGpB,MAAM,EAAkB,GAAG,EAC3B,CACO,KAAK,UAAU,QAAQ,EAEtB,KAAK,QAAQ,MAAM,KAAK,SAAU,EAAS,GAAG,EAAK,CAI3D,MAAM,EAAkB,GAAG,EAC3B,CACO,KAAK,UAAU,QAAQ,EAEtB,KAAK,QAAQ,MAAM,KAAK,SAAU,EAAS,GAAG,EAAK,CAI3D,KAAK,EAAkB,GAAG,EAC1B,CACO,KAAK,UAAU,OAAO,EAErB,KAAK,QAAQ,KAAK,KAAK,SAAU,EAAS,GAAG,EAAK,CAI1D,KAAK,EAAkB,GAAG,EAC1B,CACO,KAAK,UAAU,OAAO,EAErB,KAAK,QAAQ,KAAK,KAAK,SAAU,EAAS,GAAG,EAAK,CAI1D,MAAM,EAAkB,GAAG,EAC3B,CACO,KAAK,UAAU,QAAQ,EAEtB,KAAK,QAAQ,MAAM,KAAK,SAAU,EAAS,GAAG,EAAK,CAI3D,KAAK,EACL,CACO,KAAK,WAAa,QAEjB,KAAK,QAAQ,KAAK,KAAK,SAAU,EAAM,CAI/C,QAAQ,EACR,CACO,KAAK,WAAa,QAEjB,KAAK,QAAQ,QAAQ,KAAK,SAAU,EAAM,CAOlD,UAAkB,EAClB,CACI,GAAG,KAAK,WAAa,OAEjB,MAAO,GAGX,OAAQ,KAAK,SAAb,CAEI,IAAK,QACD,MAAO,GACX,IAAK,QACD,OAAO,IAAU,QACrB,IAAK,OACD,OAAO,IAAU,QAAU,IAAU,QAAU,IAAU,QAC7D,IAAK,OACD,OAAO,IAAU,QAAU,IAAU,QACzC,IAAK,QACD,OAAO,IAAU,QACrB,QACI,MAAO,MAUV,EAAb,KACA,CACI,QACA,MACA,QAQA,YAAY,EAAmB,QAAS,EAA2B,IAAI,EACvE,CACI,KAAK,QAAU,EACf,KAAK,MAAQ,EACb,KAAK,QAAU,IAAI,IAQvB,WAAkB,EAClB,CACI,KAAK,QAAU,EAGf,KAAK,QAAQ,QAAS,GACtB,CACO,aAAkB,GAEjB,EAAO,eAAe,KAAK,QAAS,KAAK,MAAM,EAErD,CAQN,SAAgB,EAChB,CACI,KAAK,MAAQ,EAGb,KAAK,QAAQ,QAAS,GACtB,CACO,aAAkB,GAEjB,EAAO,eAAe,KAAK,QAAS,KAAK,MAAM,EAErD,CAMN,UACA,CACI,OAAO,KAAK,MAQhB,UAAiB,EACjB,CACI,IAAI,EAAS,KAAK,QAAQ,IAAI,EAAS,CAOvC,OANI,IAEA,EAAS,IAAI,EAAW,EAAU,KAAK,MAAO,KAAK,QAAQ,CAC3D,KAAK,QAAQ,IAAI,EAAU,EAAO,EAG/B,IC5GF,EAAb,KACA,CAMI,UAAoB,IAAI,IAKxB,YAAsB,IAAI,IAK1B,KAOA,YAAY,EACZ,CACI,KAAK,KAAO,GAAQ,UAAU,WAAW,EAAI,IAAI,EAAW,WAAW,CAiB3E,UACI,EACA,EAEJ,CAgBQ,OAfJ,KAAK,KAAK,MAAM,0BAA2B,EAAmB,UAAU,GAAI,CAIxE,aAA8B,QAC1B,OAAO,GAAuB,UAAY,EAAmB,SAAS,IAAI,CAGvE,KAAK,iBACR,EACA,EACH,CAIM,KAAK,eAAe,EAAyB,EAAS,CAYrE,eACI,EACA,EAEJ,CACI,KAAK,KAAK,MAAM,kCAAmC,IAAa,CAEhE,IAAI,EAAO,KAAK,UAAU,IAAI,EAAU,CACpC,IAEA,EAAO,IAAI,IACX,KAAK,UAAU,IAAI,EAAW,EAAK,EAIvC,IAAM,EAA8B,CAChC,SAAW,GAAU,EAAS,EAAmC,CACpE,CAGD,OAFA,EAAK,IAAI,EAAa,KAGtB,CACI,KAAK,KAAK,MAAM,oCAAqC,IAAa,CAClE,EAAK,OAAO,EAAa,CACtB,EAAK,OAAS,GAEb,KAAK,UAAU,OAAO,EAAU,EAgB5C,iBACI,EACA,EAEJ,CACI,IAAI,EAEJ,GAAG,OAAO,GAAoB,SAC9B,CAEI,IAAM,EAAU,EACX,QAAQ,uBAAwB,OAAO,CACvC,QAAQ,MAAO,KAAK,CAGzB,EAAY,OAAO,IAAK,EAAS,GAAG,CACpC,KAAK,KAAK,MACN,2CAA4C,EAAiB,WAAY,EAAM,UAAU,GAC5F,MAKD,EAAQ,EACR,KAAK,KAAK,MAAM,0CAA2C,EAAM,UAAU,GAAI,CAGnF,IAAM,EAA8B,CAChC,QAAS,EACT,SAAW,GAAU,EAAS,EAAoD,CACrF,CAGD,OAFA,KAAK,YAAY,IAAI,EAAa,KAGlC,CACI,KAAK,KAAK,MAAM,kCAAmC,EAAM,UAAU,GAAI,CACvE,KAAK,YAAY,OAAO,EAAa,EAwB7C,QACI,EAEJ,CACI,KAAK,KAAK,MAAM,qBAAsB,EAAM,OAAS,EAAM,CAE3D,IAAI,EAAgB,EAGd,EAAa,KAAK,UAAU,IAAI,EAAM,KAAK,CACjD,GAAG,EACH,CACI,GAAiB,EAAW,KAC5B,IAAI,IAAM,KAAO,EAEb,QAAQ,SAAS,CACZ,SAAW,EAAI,SAAS,EAAM,CAAC,CAC/B,MAAO,GACR,CACI,KAAK,KAAK,MAAM,+BAAgC,EAAM,KAAM,IAAK,EAAI,EACvE,CAKd,IAAI,IAAM,KAAO,KAAK,YAEf,EAAI,SAAW,EAAI,QAAQ,KAAK,EAAM,KAAK,GAE1C,IACA,QAAQ,SAAS,CACZ,SAAW,EAAI,SAAS,EAAM,CAAC,CAC/B,MAAO,GACR,CACI,KAAK,KAAK,MAAM,uCAAwC,EAAM,KAAM,IAAK,EAAI,EAC/E,EAIX,IAAkB,EAEjB,KAAK,KAAK,MAAM,mCAAoC,EAAM,OAAQ,CAIlE,KAAK,KAAK,MAAM,SAAU,EAAM,KAAM,iBAAkB,EAAe,cAAc,CAY7F,MAAa,WACb,CACI,KAAK,KAAK,MAAM,wBAAwB,CACxC,KAAK,UAAU,OAAO,CACtB,KAAK,YAAY,OAAO,GC5TnB,EAAb,KACA,CACI,QAAyC,KACzC,SAAyC,KACzC,OAAiB,IAAI,IACrB,KAEA,YAAY,EACZ,CACI,KAAK,KAAO,EAAO,UAAU,cAAc,CAO/C,MAAM,YACN,CACI,KAAK,KAAK,KAAK,+BAA+B,CAE9C,KAAK,QAAU,MAAA,EAAA,EAAA,yBAA8B,CAC7C,KAAK,SAAW,MAAM,KAAK,QAAQ,mBAAmB,OAAO,CAE7D,KAAK,KAAK,KAAK,4BAA4B,CAG/C,MAAM,WACN,CACI,KAAK,KAAK,KAAK,+BAA+B,CAE9C,KAAK,OAAO,OAAO,CACnB,KAAK,SAAW,KAEhB,AAGI,KAAK,WADL,KAAK,QAAQ,SAAS,CACP,MAGnB,KAAK,KAAK,KAAK,0BAA0B,CAO7C,MAAM,UAAU,EAChB,CACI,GAAG,CAAC,KAAK,SAAW,CAAC,KAAK,SAEtB,MAAU,MAAM,8BAA8B,CAGlD,KAAK,KAAK,MAAM,uBAAwB,IAAQ,CAChD,IAAM,EAAM,MAAM,KAAK,QAAQ,eAAe,EAAM,CAAE,OAAQ,KAAK,SAAU,CAAC,CAE9E,OADA,KAAK,OAAO,IAAI,EAAM,EAAI,CACnB,EAGX,OAAO,EACP,CACI,OAAO,KAAK,OAAO,IAAI,EAAK,CAOhC,MAAM,YACF,EACA,EACA,EACA,EAEJ,CACI,GAAG,CAAC,KAAK,QAEL,MAAU,MAAM,8BAA8B,CAGlD,IAAM,EAAuC,GAAO,KAAK,UAAY,IAAA,GAI/D,EAAe,CACjB,GAAG,EACH,GAAI,EAAS,CAAE,SAAQ,CAAG,EAAE,CAC/B,CAED,OAAO,KAAK,QAAQ,iBAAiB,EAAM,EAAK,EAAa,CAOjE,gBAAgB,EAChB,CACI,GAAG,CAAC,KAAK,QAEL,MAAU,MAAM,8BAA8B,CAGlD,KAAK,QAAQ,OAAS,EAG1B,iBACA,CAMI,OALI,KAAK,QAKF,KAAK,QAAQ,OAHT,EAMf,aAAa,EAAgB,EAC7B,CACI,EAAI,OAAS,KC7GrB,EAAA,EAAA,yBAAwB,CAaxB,IAAa,EAAb,KACA,CACI,QACA,QACA,OACA,KAEA,YAAY,EAAqB,EAAyB,EAAkC,EAC5F,CACI,KAAK,QAAU,EACf,KAAK,QAAU,EACf,KAAK,OAAS,EACd,KAAK,KAAO,EAAO,UAAU,cAAc,CAM/C,aACA,CACI,OAAO,IAAI,EAAA,MAAM,KAAK,QAAQ,CAGlC,cACI,EACA,EAA0B,IAAI,EAAA,QAAQ,EAAG,KAAM,EAAE,CACjD,EAEJ,CACI,KAAK,KAAK,MACN,kCAAmC,EAAc,EAAG,IAAK,EAAc,EAAG,IAAK,EAAc,EAAG,MACnG,CAED,IAAM,EAAgB,IAA8B,IAAA,GAE9C,IAAA,GADA,CAAE,4BAA2B,CAI7B,EAAS,IAAI,EAAA,YAAY,GAAM,KAAK,OAAQ,EAAc,CAChE,EAAM,cAAc,EAAe,EAAO,CAY9C,iBACI,EACA,EACA,EACA,EAEJ,CACI,IAAM,EAAS,IAAI,EAAA,WAAW,EAAM,EAAU,EAAM,CAUpD,OATA,EAAO,UAAU,EAAA,QAAQ,MAAM,CAAC,CAEhC,IAAmB,KAAK,QAErB,GAEC,EAAO,cAAc,EAAQ,GAAK,CAG/B,EAWX,uBACI,EACA,EACA,EACA,EAAY,GAEhB,CACI,IAAM,EAAQ,IAAI,EAAA,iBAAiB,EAAM,EAAW,EAAM,CAE1D,MADA,GAAM,UAAY,EACX,EAUX,uBACI,EACA,EACA,EACA,EAAY,EAEhB,CACI,IAAM,EAAQ,IAAI,EAAA,iBAAiB,EAAM,EAAW,EAAM,CAE1D,MADA,GAAM,UAAY,EACX,EAUX,iBACI,EACA,EACA,EACA,EAAY,EAEhB,CACI,IAAM,EAAQ,IAAI,EAAA,WAAW,EAAM,EAAU,EAAM,CAEnD,MADA,GAAM,UAAY,EACX,EAaX,gBACI,EACA,EACA,EACA,EACA,EACA,EACA,EAAY,EAEhB,CACI,IAAM,EAAQ,IAAI,EAAA,UAAU,EAAM,EAAU,EAAW,EAAO,EAAU,EAAM,CAE9E,MADA,GAAM,UAAY,EACX,EAYX,oBACI,EACA,EACA,EACA,EACA,EACA,EAAY,EAEhB,CACI,IAAM,EAAQ,IAAI,EAAA,cAAc,EAAM,EAAU,EAAO,EAAQ,EAAM,CAErE,MADA,GAAM,UAAY,EACX,EAUX,aACI,EACA,EAAuD,EAAE,CACzD,EAEJ,CAEI,OAAO,EAAA,YAAY,aAAa,EAAM,CADb,SAAU,EAAG,SAAU,GACW,GAAG,EAAS,CAAE,EAAM,CASnF,UACI,EACA,EAAmF,EAAE,CACrF,EAEJ,CAEI,OAAO,EAAA,YAAY,UAAU,EAAM,CADV,KAAM,EACyB,GAAG,EAAS,CAAE,EAAM,CAShF,aACI,EACA,EAA0E,EAAE,CAC5E,EAEJ,CAEI,OAAO,EAAA,YAAY,aAAa,EAAM,CADb,MAAO,EAAG,OAAQ,EAAG,aAAc,EACD,GAAG,EAAS,CAAE,EAAM,CASnF,eACI,EACA,EAAkF,EAAE,CACpF,EAEJ,CAEI,OAAO,EAAA,YAAY,eAAe,EAAM,CADf,OAAQ,EAAG,YAAa,EAAG,eAAgB,EACP,GAAG,EAAS,CAAE,EAAM,CAWrF,WACI,EACA,EACA,EAA+E,EAAE,CACjF,EAEJ,CAEI,OAAO,IAAI,EAAA,iBAAiB,EAAM,EAAW,CADtB,KAAM,EAAG,YAAa,IAAM,SAAU,GACG,GAAG,EAAc,CAAE,EAAM,CAS7F,MAAa,UACT,EACA,EAEJ,CACI,KAAK,KAAK,MAAM,kBAAmB,IAAiB,CAEpD,GACA,CACI,IAAM,EAAS,MAAA,EAAA,EAAA,yBAA8B,GAAI,IAAkB,EAAM,CAEzE,OADA,KAAK,KAAK,MAAM,8BAA+B,IAAiB,CACzD,QAEJ,EACP,CAEI,MADA,KAAK,KAAK,MAAM,yBAA0B,IAAkB,EAAM,CAC5D,GAUd,MAAa,aACT,EACA,EACA,EAUJ,CACI,KAAK,KAAK,MAAM,0BAA2B,IAAiB,CAE5D,GACA,CAEI,IAAM,EAAU,EAAU,OAAS,EAAI,CAAE,YAAW,CAAG,IAAA,GACjD,EAAS,MAAA,EAAA,EAAA,iBAAsB,GAAI,IAAkB,EAAO,EAAQ,CAE1E,OADA,KAAK,KAAK,MAAM,iCAAkC,EAAU,OAAS,EAAI,EAAU,KAAK,KAAK,CAAG,QAAS,CAClG,QAEJ,EACP,CAEI,MADA,KAAK,KAAK,MAAM,iCAAkC,IAAkB,EAAM,CACpE,GAOd,MAAM,WACN,CACI,KAAK,KAAK,KAAK,2BAA2B,CAM1C,KAAK,KAAK,KAAK,qCAAqC,GChW/C,EAAb,KACA,CACI,UACA,aACA,KAGA,YAAsB,IAAI,IAG1B,cAAwB,IAAI,IAI5B,YAAY,EAAyB,EAA2B,EAChE,CACI,KAAK,UAAY,EACjB,KAAK,aAAe,EACpB,KAAK,KAAO,GAAQ,UAAU,eAAe,EAAI,IAAI,EAAW,eAAe,CAUnF,WAAmB,EACnB,CACI,IAAM,EAAU,EAAK,QAAQ,IAAI,CAMjC,OALG,IAAY,GAEJ,CAAE,SAAU,EAAM,CAGtB,CACH,SAAU,EAAK,UAAU,EAAG,EAAQ,CACpC,SAAU,EAAK,UAAU,EAAU,EAAE,CACxC,CAOL,MAAc,eAAe,EAC7B,CACI,IAAM,EAAS,KAAK,YAAY,IAAI,EAAS,CAC7C,GAAG,EAGC,MADA,GAAO,WACA,EAAO,UAGlB,KAAK,KAAK,MAAM,4BAA6B,IAAY,CAGzD,IAAM,EAAY,KAAK,aAAa,aAAa,CAEjD,GACA,CACI,IAAM,EAAY,MAAM,KAAK,aAAa,UAAU,EAAU,EAAU,CAKxE,OAHA,KAAK,YAAY,IAAI,EAAU,CAAE,YAAW,SAAU,EAAG,CAAC,CAC1D,KAAK,KAAK,MAAM,2BAA4B,IAAY,CAEjD,SAGX,CACI,EAAU,SAAS,EAO3B,qBAA6B,EAA4B,EAAmB,EAC5E,CACI,IAAM,EAAO,EAAU,OAAO,KAAM,GAAU,EAAM,OAAS,EAAS,CACtE,GAAG,CAAC,EACJ,CACI,IAAM,EAAY,EAAU,OAAO,IAAK,GAAU,EAAM,KAAK,CAAC,KAAK,KAAK,CACxE,MAAU,MACN,SAAU,EAAU,gCAAiC,EAAU,uBACvC,IAC3B,CAGL,OAAO,EAeX,MAAM,KAAK,EACX,CACI,GAAM,CAAE,WAAU,YAAa,KAAK,WAAW,EAAK,CAEpD,GAAG,EACH,CAEI,IAAM,EAAa,KAAK,cAAc,IAAI,EAAK,CAC/C,GAAG,EACH,CAEI,IAAM,EAAS,KAAK,YAAY,IAAI,EAAS,CAM7C,OALG,GAEC,EAAO,WAGJ,EAIX,IAAM,EAAY,MAAM,KAAK,eAAe,EAAS,CAG/C,EAAO,KAAK,qBAAqB,EAAW,EAAU,EAAK,CAGjE,OAFA,KAAK,cAAc,IAAI,EAAM,EAAK,CAE3B,EAIX,OAAO,KAAK,eAAe,EAAS,CAMxC,SAAS,EACT,CACI,IAAM,EAAa,KAAK,cAAc,IAAI,EAAK,CAC/C,GAAG,CAAC,EAEA,MAAU,MAAM,UAAW,EAAM,qCAAqC,CAG1E,OAAQ,EAAoB,eAAe,GAAI,EAAW,KAAM,WAAW,CAM/E,MAAM,EACN,CACI,IAAM,EAAa,KAAK,cAAc,IAAI,EAAK,CAC/C,GAAG,CAAC,EAEA,MAAU,MAAM,UAAW,EAAM,qCAAqC,CAG1E,OAAQ,EAAoB,MAAM,GAAI,EAAW,KAAM,QAAQ,CAMnE,MAAM,QAAQ,EACd,CACI,IAAM,EAAQ,EAAM,OAEpB,IAAI,IAAI,EAAI,EAAG,EAAI,EAAM,OAAQ,IAG7B,MAAM,KAAK,KAAK,EAAM,GAAG,CAEzB,KAAK,UAAU,QAAQ,CACnB,KAAM,iBACN,QAAS,CACL,KAAM,EAAM,GACZ,OAAQ,EAAI,EACZ,QACH,CACJ,CAAC,CAGN,KAAK,UAAU,QAAQ,CACnB,KAAM,iBACN,QAAS,CAAE,QAAO,CACrB,CAAC,CAON,QAAQ,EACR,CACI,GAAM,CAAE,YAAa,KAAK,WAAW,EAAK,CACpC,EAAS,KAAK,YAAY,IAAI,EAAS,CACzC,OAKJ,EAAO,WAEJ,EAAO,UAAY,GACtB,CACI,KAAK,KAAK,MAAM,8BAA+B,EAAU,uBAAuB,CAGhF,IAAI,GAAM,CAAE,EAAc,KAAU,KAAK,cAElC,EAAa,WAAW,GAAI,EAAU,GAAG,GAExC,EAAK,SAAS,CACd,KAAK,cAAc,OAAO,EAAa,EAI/C,EAAO,UAAU,SAAS,CAC1B,KAAK,YAAY,OAAO,EAAS,EAOzC,YACA,CACI,IAAI,IAAM,KAAU,KAAK,YAAY,QAAQ,CAEzC,EAAO,UAAU,SAAS,CAG9B,IAAI,IAAM,KAAQ,KAAK,cAAc,QAAQ,CAEzC,EAAK,SAAS,CAGlB,KAAK,YAAY,OAAO,CACxB,KAAK,cAAc,OAAO,CAM9B,MAAM,WACN,CACI,KAAK,YAAY,GCvQZ,EAA+B,CACxC,UACA,SACA,QACH,CCfY,EAAmB,CAAE,WAAY,QAAS,UAAW,CAyHlE,SAAgB,EAAgB,EAChC,CACI,OAAO,EAAM,OAAS,WAM1B,SAAgB,EAAa,EAC7B,CACI,OAAO,EAAM,OAAS,QAM1B,SAAgB,EAAe,EAC/B,CACI,OAAO,EAAM,OAAS,UCzF1B,IAAa,GAAb,KACA,CACI,KAAuB,UACvB,OACA,QACA,SACA,WACA,OAGA,UACA,WAGA,kBAA4B,GAS5B,IAAI,SACJ,CACI,MAAO,CACH,SAAU,KAAK,UACf,UAAW,KAAK,WACnB,CAcL,YACI,EACA,EACA,EACA,EACA,EAAkC,EAAE,CAExC,CACI,KAAK,OAAS,EACd,KAAK,SAAW,EAChB,KAAK,WAAa,EAClB,KAAK,OAAS,EACd,KAAK,QAAU,EAAQ,QAGvB,KAAK,UAAY,EAAQ,UAAY,SACrC,KAAK,WAAa,EAAQ,WAAa,GAa3C,QAAe,EAAoB,EACnC,CAEI,IAAM,EAAW,KAAK,OAAO,SAAS,EAAM,EAAI,GAG1C,EAAe,OAAO,GAAa,UACnC,EACA,GAAY,KAAK,WAGnB,EAAgB,GAEpB,OAAQ,KAAK,UAAb,CAEI,IAAK,SACD,EAAgB,GAAgB,CAAC,KAAK,kBACtC,MACJ,IAAK,UACD,EAAgB,CAAC,GAAgB,KAAK,kBACtC,MACJ,IAAK,OACD,EAAgB,IAAiB,KAAK,kBACtC,MAOR,GAHA,KAAK,kBAAoB,EAGtB,EACH,CACI,IAAI,EACJ,GAAG,KAAK,OAAO,OAAS,SACxB,CAEI,IAAI,EAAa,OAAO,GAAa,SAAW,EAAY,EAAW,EAAM,EAG7E,GAAG,KAAK,OAAO,WAAa,IAAA,IAAa,KAAK,OAAO,WAAa,IAAA,GAClE,CACI,IAAM,EAAM,KAAK,OAAO,UAAY,EAC9B,EAAM,KAAK,OAAO,UAAY,EACpC,EAAa,EAAO,GAAc,EAAM,GAE5C,EAAc,OAId,EAAc,GAElB,IAAM,EAAc,CAChB,KAAM,UAAW,KAAK,OAAO,OAC7B,QAAS,CACL,MAAO,EACP,SAAU,KAAK,SACf,QAAS,KAAK,QACjB,CACJ,CACD,EAAS,QAAQ,EAAY,EAQrC,eAAsB,EACtB,CACI,GAAG,EACH,CACI,IAAM,EAAW,KAAK,OAAO,SAAS,EAAM,EAAI,GAChD,KAAK,kBAAoB,OAAO,GAAa,UACvC,EACA,GAAY,KAAK,gBAIvB,KAAK,kBAAoB,GAOjC,QACA,CACI,MAAO,CACH,KAAM,KAAK,KACX,OAAQ,KAAK,OAAO,KACpB,MAAO,CACH,SAAU,KAAK,SACf,GAAG,KAAK,OAAO,QAAQ,CAC1B,CACD,QAAS,KAAK,QACd,QAAS,KAAK,QACjB,GClKI,GAAb,KACA,CACI,KAAuB,SACvB,OACA,QACA,SACA,WACA,OAGA,QACA,WACA,cACA,SACA,UAGA,kBAA4B,GAC5B,aASA,IAAW,OACX,CACI,OAAO,KAAK,aAMhB,IAAW,MAAM,EACjB,CACI,KAAK,aAAe,EAMxB,IAAW,SACX,CACI,OAAO,KAAK,SAMhB,IAAW,UACX,CACI,OAAO,KAAK,UAOhB,IAAW,OACX,CASQ,OARD,KAAK,OAAO,OAAS,SAEb,KAAK,aACL,KAAK,OAAO,UAAY,EACxB,KAAK,OAAO,UAAY,EAIxB,KAAK,aAAe,KAAK,SAAW,KAAK,UAOxD,IAAW,SACX,CACI,MAAO,CACH,OAAQ,KAAK,QACb,UAAW,KAAK,WAChB,aAAc,KAAK,cACnB,QAAS,KAAK,SACd,SAAU,KAAK,UAClB,CAcL,YACI,EACA,EACA,EACA,EACA,EAAiC,EAAE,CAEvC,CACI,KAAK,OAAS,EACd,KAAK,SAAW,EAChB,KAAK,WAAa,EAClB,KAAK,OAAS,EACd,KAAK,QAAU,EAAQ,QAGvB,KAAK,QAAU,EAAQ,QAAU,GACjC,KAAK,WAAa,EAAQ,WAAa,GACvC,KAAK,cAAgB,EAAQ,cAAgB,GAC7C,KAAK,aAAe,KAAK,cAIzB,KAAK,SAAW,EAAQ,SAAW,GACnC,KAAK,UAAY,EAAQ,UAAY,GAazC,QAAe,EAAoB,EACnC,CACI,IAAM,EAAW,KAAK,OAAO,SAAS,EAAM,EAAI,GAC1C,EAAe,OAAO,GAAa,UACnC,EACA,GAAY,KAAK,WAEjB,EAAe,KAAK,QACpB,CAAC,GAAgB,KAAK,kBACtB,GAAgB,CAAC,KAAK,kBAE5B,KAAK,kBAAoB,EAErB,IAEJ,KAAK,aAAe,CAAC,KAAK,aAE1B,EAAS,QAAQ,CACb,KAAM,UAAW,KAAK,OAAO,OAC7B,QAAS,CACL,MAAO,KAAK,MACZ,SAAU,KAAK,SACf,QAAS,KAAK,QACjB,CACJ,CAAC,EAON,eAAsB,EACtB,CACI,GAAG,EACH,CACI,IAAM,EAAW,KAAK,OAAO,SAAS,EAAM,EAAI,GAChD,KAAK,kBAAoB,OAAO,GAAa,UACvC,EACA,GAAY,KAAK,gBAIvB,KAAK,kBAAoB,GAOjC,OACA,CACI,KAAK,aAAe,KAAK,cAM7B,QACA,CACI,MAAO,CACH,KAAM,KAAK,KACX,OAAQ,KAAK,OAAO,KACpB,MAAO,CACH,SAAU,KAAK,SACf,GAAG,KAAK,OAAO,QAAQ,CAC1B,CACD,MAAO,KAAK,aACZ,QAAS,KAAK,QACd,QAAS,KAAK,QACjB,GC5LI,GAAb,KACA,CACI,KAAuB,QACvB,OACA,QACA,SACA,WACA,OAGA,OACA,QACA,QACA,cACA,UACA,SACA,UACA,KACA,KAGA,WAOA,IAAI,SACJ,CACI,MAAO,CACH,MAAO,KAAK,OACZ,OAAQ,KAAK,QACb,OAAQ,KAAK,QACb,aAAc,KAAK,cACnB,SAAU,KAAK,UACf,QAAS,KAAK,SACd,SAAU,KAAK,UACf,IAAK,KAAK,KACV,IAAK,KAAK,KACb,CAaL,YACI,EACA,EACA,EACA,EACA,EAAgC,EAAE,CAEtC,CACI,KAAK,OAAS,EACd,KAAK,SAAW,EAChB,KAAK,WAAa,EAClB,KAAK,OAAS,EACd,KAAK,QAAU,EAAQ,QAGvB,KAAK,OAAS,EAAQ,OAAS,EAC/B,KAAK,QAAU,EAAQ,QAAU,EACjC,KAAK,QAAU,EAAQ,QAAU,GACjC,KAAK,cAAgB,EAAQ,cAAgB,GAC7C,KAAK,UAAY,EAAQ,UAAY,EAIrC,KAAK,SAAW,EAAQ,SAAW,GACnC,KAAK,UAAY,EAAQ,UAAY,GAGrC,IAAM,EAAa,KAAK,OAAO,OAAS,SAClC,KAAK,OAAO,UAAY,KACxB,KACN,KAAK,KAAO,EAAQ,KAAO,EAE3B,IAAM,EAAa,KAAK,OAAO,OAAS,SAClC,KAAK,OAAO,UAAY,IACxB,IACN,KAAK,KAAO,EAAQ,KAAO,EAa/B,QAAe,EAAoB,EACnC,CACI,IAAM,EAAW,KAAK,OAAO,SAAS,EAAM,CAC5C,GAAG,IAAa,IAAA,GAAa,OAE7B,IAAM,EAAe,OAAO,GAAa,UAAa,EAAW,EAAM,EAAO,EAE1E,EAAQ,KAAK,UAAY,GAAK,KAAK,IAAI,EAAa,CAAG,KAAK,UAAY,EAAI,EAChF,GAAU,KAAK,QAAU,CAAC,EAAQ,GAAS,KAAK,OAAU,KAAK,QAG/D,EAAQ,KAAK,IAAI,KAAK,KAAM,KAAK,IAAI,KAAK,KAAM,EAAM,CAAC,CAEpD,OAAK,eAAiB,KAAK,aAAe,KAC7C,KAAK,WAAa,EAElB,EAAS,QAAQ,CACb,KAAM,UAAW,KAAK,OAAO,OAC7B,QAAS,CACL,QACA,SAAU,KAAK,SACf,QAAS,KAAK,QACjB,CACJ,CAAC,EAMN,eAAsB,EACtB,EAOA,QACA,CACI,MAAO,CACH,KAAM,KAAK,KACX,OAAQ,KAAK,OAAO,KACpB,MAAO,CACH,SAAU,KAAK,SACf,GAAG,KAAK,OAAO,QAAQ,CAC1B,CACD,QAAS,KAAK,QACd,QAAS,KAAK,QACjB,GChMI,GAAb,MAAa,CACb,CAII,WAAkD,MAKlD,UAKA,SAQA,YAAY,EAAkB,EAAkC,EAAE,CAClE,CACI,KAAK,UAAY,EACjB,KAAK,SAAW,EAAQ,UAAY,GAQxC,SAAgB,EAChB,CAEI,GAAG,EAAM,OAAS,WAEd,OAGJ,IAAM,EAAgB,EASlB,OAND,KAAK,SAEG,EAAc,MAAM,KAAK,WAIzB,EAAc,KAAK,KAAK,WAUvC,OAAc,WAAW,EAAoB,EAAkC,EAAE,CACjF,CACI,OAAO,IAAI,EAAoB,EAAW,EAAQ,CAMtD,QACA,CACI,MAAO,CAEH,KAAM,WACN,WAAY,KAAK,WACjB,UAAW,KAAK,UAChB,QAAS,CACL,SAAU,KAAK,SAClB,CACJ,GChGI,GAAb,MAAa,CACb,CAII,WAKA,UAQA,YAAY,EAA8B,EAC1C,CACI,KAAK,WAAa,EAClB,KAAK,UAAY,EAQrB,SAAgB,EAChB,CAEO,KAAM,OAAS,QAKlB,OAAQ,KAAK,WAAb,CAEI,IAAK,SACL,CACI,IAAM,EAAc,EAAM,QAAQ,KAAK,WACvC,OAAO,EAAc,EAAY,QAAU,IAAA,GAG/C,IAAK,WACL,CAEI,GAAM,CAAE,EAAS,GAAS,KAAK,UAAU,MAAM,IAAI,CAQnD,MANG,CAAC,GAAW,CAAC,GAAS,IAAY,YAAc,IAAY,YACvD,IAAS,KAAO,IAAS,IAE7B,OAGG,EAAM,SAAS,GAAS,GAGnC,IAAK,QACL,CACI,IAAM,EAAkB,KAAK,YAAc,UAAY,KAAK,YAAc,UACnE,KAAK,YAAc,SAK1B,OAJG,EAAM,OAAS,EAEP,EAAM,MAAM,KAAK,WAE5B,OAGJ,QACI,QASZ,OAAc,WAAW,EACzB,CACI,GAAM,CAAE,EAAY,GAAG,GAAa,EAAiB,MAAM,IAAI,CACzD,EAAY,EAAS,KAAK,IAAI,CAEpC,GAAG,CAAC,GAAc,CAAC,EAEf,MAAU,MAAM,gCAAiC,IAAoB,CAGzE,OAAO,IAAI,EACP,EACA,EACH,CAML,QACA,CAEI,MAAO,CACH,KAAM,QACN,WAAY,KAAK,WACjB,UAAW,KAAK,UACnB,GCpFI,GAAb,MAAa,CACb,CAII,WAKA,UAKA,eAKA,SAKA,OASA,YAAY,EAAgC,EAAoB,EAAiC,EAAE,CACnG,CACI,KAAK,WAAa,EAClB,KAAK,UAAY,EACjB,KAAK,eAAiB,EAAQ,gBAAkB,GAChD,KAAK,SAAW,EAAQ,UAAY,GACpC,KAAK,OAAS,EAAQ,QAAU,GAQpC,SAAgB,EAChB,CAEO,KAAM,OAAS,UAKlB,OAAQ,KAAK,WAAb,CAEI,IAAK,SACL,CACI,IAAM,EAAc,EAAM,QAAQ,KAAK,WAOvC,OALI,EAKG,KAAK,eAAiB,EAAY,MAAQ,EAAY,QAHzD,OAMR,IAAK,OACL,CACI,IAAI,EAAQ,EAAM,KAAK,KAAK,WAc5B,OAZG,IAAU,IAAA,GAET,QAID,KAAK,IAAI,EAAM,CAAG,KAAK,WAEtB,EAAQ,GAIL,KAAK,OAAS,CAAC,EAAQ,GAGlC,QACI,QAUZ,OAAc,WAAW,EAA2B,EAAiC,EAAE,CACvF,CACI,GAAM,CAAE,EAAY,GAAc,EAAiB,MAAM,IAAI,CAE7D,GAAG,CAAC,GAAc,CAAC,EAEf,MAAU,MAAM,kCAAmC,IAAoB,CAG3E,OAAO,IAAI,EACP,EACA,EACA,EACH,CAML,QACA,CACI,MAAO,CACH,KAAM,UACN,WAAY,KAAK,WACjB,UAAW,KAAK,UAChB,QAAS,CACL,eAAgB,KAAK,eACrB,SAAU,KAAK,SACf,OAAQ,KAAK,OAChB,CACJ,GCtGH,GAA4B,IAM5B,GAAwB,GAmBjB,GAAb,KACA,CAEI,UAAoB,IAAI,IAGxB,SAAmB,IAAI,IAGvB,UAAoB,IAAI,IAKxB,gBAA0B,IAAI,IAG9B,sBAAoD,KAGpD,gBAA0B,IAAI,IAG9B,cAA8C,KAG9C,UAGA,kBAAkD,KAGlD,KASA,YAAY,EAAyB,EACrC,CACI,KAAK,UAAY,EAGjB,KAAK,kBAAoB,KAAK,UAAU,UACpC,gBACC,GACD,CACI,IAAM,EAAU,EAAM,QACnB,GAEC,KAAK,aAAa,EAAQ,OAAQ,EAAQ,MAAM,EAG3D,CAED,KAAK,KAAO,GAAQ,UAAU,iBAAiB,EAAI,IAAI,EAAW,iBAAiB,CACnF,KAAK,KAAK,MAAM,6BAA6B,CAajD,wBAAgC,EAChC,CAQI,OANI,EAAQ,QAML,KAAK,gBAAgB,IAAI,EAAQ,QAAQ,CAJrC,GAaf,oBAA4B,EAAsB,EAAY,GAC9D,CACI,IAAI,EAAU,KAAK,UAAU,IAAI,EAAY,CAa7C,OAXI,IAEA,EAAU,CACN,KAAM,EACN,YACH,CAED,KAAK,UAAU,IAAI,EAAa,EAAQ,CACxC,KAAK,KAAK,MAAM,yBAA0B,EAAa,gBAAiB,EAAW,GAAG,EAGnF,EASX,6BAAqC,EACrC,CACI,IAAM,EAAiC,EAAE,CAGzC,IAAI,IAAM,KAAe,KAAK,gBAGvB,IAAgB,GAKH,KAAK,UAAU,IAAI,EAAY,EAGnC,YAER,KAAK,gBAAgB,OAAO,EAAY,CACxC,EAAoB,KAAK,EAAY,EAI7C,OAAO,EASX,0BAAkC,EAClC,CAeI,OAdG,EAAa,EAAM,CAGX,OAAO,OAAO,EAAM,QAAQ,CAAC,KAAM,GAAQ,EAAI,QAAQ,EAAI,EAAM,QAAU,IAAA,GAGnF,EAAe,EAAM,CAGb,OAAO,OAAO,EAAM,QAAQ,CAAC,KAAM,GAAQ,EAAI,QAAQ,EACvD,OAAO,OAAO,EAAM,KAAK,CAAC,KAAM,GAAQ,KAAK,IAAI,EAAI,CAAG,GAA0B,CAItF,GASX,6BAAqC,EACrC,CAEI,IAAM,EAAS,KAAK,SAAS,IAAI,EAAW,OAAO,CAGnD,GAAG,CAAC,EAGA,OADA,KAAK,KAAK,KAAK,kCAAmC,EAAW,OAAQ,cAAc,CAC5E,KAIX,GAAM,CAAE,WAAU,GAAG,GAAc,EAAW,MAE9C,OAAQ,EAAW,KAAnB,CAEI,IAAK,UAED,OAAO,IAAI,GACP,EACA,EACA,EAAW,MAAM,KACjB,KAAK,iCAAiC,EAAU,CAChD,CACI,GAAI,EAAW,SAAW,EAAE,CAC5B,QAAS,EAAW,QACvB,CACJ,CAGL,IAAK,SAED,OAAO,IAAI,GACP,EACA,EACA,EAAW,MAAM,KACjB,KAAK,iCAAiC,EAAU,CAChD,CACI,GAAI,EAAW,SAAW,EAAE,CAC5B,QAAS,EAAW,QACvB,CACJ,CAGL,IAAK,QAED,OAAO,IAAI,GACP,EACA,EACA,EAAW,MAAM,KACjB,KAAK,iCAAiC,EAAU,CAChD,CACI,GAAI,EAAW,SAAW,EAAE,CAC5B,QAAS,EAAW,QACvB,CACJ,CAGL,QAEI,OADA,KAAK,KAAK,MAAM,iCAAmC,EAAiC,OAAQ,CACrF,MAUnB,iCAAyC,EACzC,CAEI,OAAQ,EAAW,KAAnB,CAEI,IAAK,WACD,OAAO,IAAI,GACP,EAAW,UACX,EAAW,QACd,CAEL,IAAK,QACL,CAEI,IAAM,EAAa,EAAW,WAC9B,GAAG,EAAE,IAAe,UAAY,IAAe,YAAc,IAAe,SAExE,MAAU,MAAM,8BAA+B,IAAc,CAGjE,OAAO,IAAI,GACP,EACA,EAAW,UACd,CAGL,IAAK,UACL,CAEI,IAAM,EAAa,EAAW,WAC9B,GAAG,EAAE,IAAe,UAAY,IAAe,QAE3C,MAAU,MAAM,gCAAiC,IAAc,CAEnE,OAAO,IAAI,GACP,EACA,EAAW,UACX,EAAW,QACd,CAGL,QACI,MAAU,MAAM,kCAAoC,EAA2C,OAAQ,EAWnH,qBAA6B,EAAsB,EACnD,CACI,IAAM,EAAU,KAAK,cACrB,GAAG,CAAC,EAEA,OAGJ,GAAM,CAAE,WAAY,EAGpB,GAAG,CAAC,EAAQ,YAAY,SAAS,EAAO,KAAK,CAEzC,OAGJ,IAAI,EAAkC,KAEtC,GAAG,EAAgB,EAAM,CACzB,CAEI,GAAG,EAAM,OAAO,SAAW,GAEvB,OAIJ,IAAI,IAAM,KAAO,OAAO,KAAK,EAAM,MAAM,CAErC,GAAG,EAAM,MAAM,KAAS,GACxB,CACI,EAAS,CAAE,KAAM,WAAY,WAAY,MAAO,UAAW,EAAK,SAAU,EAAO,GAAI,CACrF,eAIJ,EAAa,EAAM,MAGnB,IAAM,KAAO,OAAO,KAAK,EAAM,QAAQ,CAEvC,GAAG,EAAM,QAAQ,GAAK,QACtB,CACI,EAAS,CAAE,KAAM,QAAS,WAAY,SAAU,UAAW,EAAK,SAAU,EAAO,GAAI,CACrF,eAIJ,EAAe,EAAM,CAC7B,CAEI,IAAI,IAAM,KAAO,OAAO,KAAK,EAAM,QAAQ,CAEvC,GAAG,EAAM,QAAQ,GAAK,QACtB,CACI,EAAS,CAAE,KAAM,UAAW,WAAY,SAAU,UAAW,EAAK,SAAU,EAAO,GAAI,CACvF,MAKR,GAAG,CAAC,OAEI,IAAM,KAAO,OAAO,KAAK,EAAM,KAAK,CAEpC,GAAG,KAAK,IAAI,EAAM,KAAK,GAAK,CAAG,GAC/B,CACI,EAAS,CAAE,KAAM,UAAW,WAAY,OAAQ,UAAW,EAAK,SAAU,EAAO,GAAI,CACrF,QAahB,GANG,CAAC,GAMD,EAAQ,aACJ,CAAC,EAAQ,YAAY,SAAS,EAAO,WAAiD,CAGzF,OAIJ,GAAM,CAAE,WAAY,EACpB,KAAK,iBAAiB,CACtB,EAAQ,EAAO,CAOnB,iBACA,CACI,IAAM,EAAU,KAAK,cACrB,GAAG,CAAC,EAEA,OAID,EAAQ,cAEP,EAAQ,cAAc,CAG1B,GAAM,CAAE,YAAa,EACrB,KAAK,cAAgB,KAGrB,KAAK,kBAAkB,mBAAmB,CAC1C,KAAK,UAAU,OAAO,mBAAmB,CAEzC,IAAI,IAAM,KAAe,EAErB,KAAK,gBAAgB,EAAY,CAoBzC,aAAoB,EAAsB,EAC1C,CAMI,GAJA,KAAK,gBAAgB,IAAI,EAAO,GAAI,EAAM,CAIvC,KAAK,0BAA0B,EAAM,EAAI,EAAO,OAAS,KAAK,sBACjE,CACI,IAAM,EAAqB,KAAK,sBAChC,KAAK,sBAAwB,EAAO,KACpC,KAAK,UAAU,QAAQ,CACnB,KAAM,4BACN,QAAS,CAAE,WAAY,EAAO,KAAM,qBAAoB,CAC3D,CAAC,CAIN,GAAG,KAAK,cACR,CACI,KAAK,qBAAqB,EAAQ,EAAM,CACxC,OAGJ,IAAM,EAAW,KAAK,UAAU,IAAI,EAAO,GAAG,CAC3C,MAAC,GAAY,EAAS,SAAW,IAKjC,OAAK,gBAAgB,OAAS,GAAK,EAAS,KAAM,GAAY,EAAQ,QAAQ,EAKjF,IAAI,IAAM,KAAW,EAEb,KAAK,wBAAwB,EAAQ,EAKzC,EAAQ,QAAQ,EAAO,KAAK,UAAU,CAc9C,eAAsB,EACtB,CAGI,GAFA,KAAK,KAAK,MAAM,uBAAwB,EAAO,KAAM,GAAG,CAErD,KAAK,SAAS,IAAI,EAAO,KAAK,CACjC,CACI,IAAM,EAAW,WAAY,EAAO,KAAM,uBAE1C,MADA,KAAK,KAAK,MAAM,EAAS,CACf,MAAM,EAAS,CAG7B,KAAK,SAAS,IAAI,EAAO,KAAM,EAAO,CACtC,KAAK,KAAK,MAAM,WAAY,EAAO,KAAM,2BAA2B,CAQxE,UAAiB,EACjB,CACI,KAAK,KAAK,MAAM,mBAAoB,EAAY,GAAG,CAEnD,IAAM,EAAS,KAAK,SAAS,IAAI,EAAW,EAAI,KAMhD,OALI,GAEA,KAAK,KAAK,MAAM,WAAY,EAAY,aAAa,CAGlD,EAWX,IAAW,sBACX,CACI,OAAO,KAAK,sBAehB,aAAoB,EACpB,CACI,GAAG,KAAK,cAEJ,MAAU,MAAM,qCAAqC,CAIzD,IAAM,EAAW,CAAE,GAAG,KAAK,gBAAiB,CAM5C,OAHA,KAAK,gBAAgB,mBAAoB,GAAK,CAC9C,KAAK,gBAAgB,mBAAmB,CAEjC,IAAI,SAA0B,EAAS,IAC9C,CACI,IAAM,EAA8B,CAAE,UAAS,SAAQ,UAAS,WAAU,CAI1E,GAHA,KAAK,cAAgB,EAGlB,EAAQ,OACX,CACI,GAAG,EAAQ,OAAO,QAClB,CACI,KAAK,iBAAiB,CACtB,EAAO,EAAQ,OAAO,QAAc,MAAM,yBAAyB,CAAC,CACpE,OAGJ,IAAM,MACN,CACI,IAAM,EAAS,EAAQ,QAAQ,QAAc,MAAM,yBAAyB,CAC5E,KAAK,iBAAiB,CACtB,EAAO,EAAO,EAGlB,EAAQ,OAAO,iBAAiB,QAAS,EAAS,CAAE,KAAM,GAAM,CAAC,CAEjE,EAAa,iBACb,CACI,EAAQ,QAAQ,oBAAoB,QAAS,EAAQ,IAG/D,CAcN,gBAAuB,EAAsB,EAAY,GACzD,CACI,IAAM,EAAU,KAAK,oBAAoB,EAAa,EAAU,CAShE,OANG,EAAQ,YAAc,IAErB,EAAQ,UAAY,EACpB,KAAK,KAAK,KAAK,oBAAqB,EAAa,iBAAkB,IAAa,EAG7E,EASX,gBAAuB,EACvB,CAKI,IAAM,EAHU,KAAK,oBAAoB,EAAY,CAGzB,UAE5B,KAAK,KAAK,MAAM,uBAAwB,EAAa,gBAAiB,EAAa,GAAG,CAGtF,IAAM,EAAkB,KAAK,gBAAgB,IAAI,EAAY,CAE7D,GAAG,EACH,CAEI,IAAM,EAAc,KAAK,6BAA6B,EAAY,CAE/D,EAAY,OAAS,GAEpB,KAAK,KAAK,KAAK,mCAAoC,EAAY,KAAK,KAAK,GAAI,CAKrF,GAAG,CAAC,EACJ,CACI,KAAK,gBAAgB,IAAI,EAAY,CAKrC,IAAI,GAAM,CAAE,EAAU,KAAoB,KAAK,UAAU,SAAS,CAClE,CACI,IAAM,EAAY,KAAK,gBAAgB,IAAI,EAAS,CACpD,IAAI,IAAM,KAAW,EAEd,EAAQ,UAAY,GAEnB,EAAQ,eAAe,EAAU,CAK7C,KAAK,KAAK,KAAK,YAAa,EAAa,aAAc,EAAc,gBAAkB,KAAM,EASrG,kBAAyB,EACzB,CACI,KAAK,KAAK,MAAM,yBAA0B,EAAa,GAAG,CAEvD,KAAK,gBAAgB,IAAI,EAAY,EAEpC,KAAK,gBAAgB,OAAO,EAAY,CACxC,KAAK,KAAK,KAAK,YAAa,EAAa,eAAe,EAIxD,KAAK,KAAK,MAAM,YAAa,EAAa,kBAAkB,CAOpE,mBACA,CACI,MAAO,CAAE,GAAG,KAAK,gBAAiB,CAQtC,gBAAuB,EACvB,CACI,OAAO,KAAK,gBAAgB,IAAI,EAAY,CAQhD,WAAkB,EAClB,CACI,OAAO,KAAK,UAAU,IAAI,EAAY,EAAI,KAc9C,iBAAwB,EACxB,CACI,GAAG,CAAC,EAAa,SAAS,EAAQ,KAAK,CAEnC,MAAU,MAAM,yBAA0B,EAAQ,OAAQ,CAI3D,EAAQ,SAAW,CAAC,KAAK,UAAU,IAAI,EAAQ,QAAQ,EAEtD,KAAK,gBAAgB,EAAQ,QAAQ,CAIrC,KAAK,UAAU,IAAI,EAAQ,SAAS,EAEpC,KAAK,UAAU,IAAI,EAAQ,SAAU,EAAE,CAAC,CAI5C,KAAK,UAAU,IAAI,EAAQ,SAAS,EAAE,KAAK,EAAQ,CACnD,KAAK,KAAK,MAAM,cAAe,EAAQ,KAAM,gBAAiB,EAAQ,OAAO,KAAM,gBACxE,EAAQ,SAAW,KAAM,GAAG,CAS3C,gBAAuB,EACvB,CACI,IAAM,EAAU,KAAK,6BAA6B,EAAW,CAE1D,EAEC,KAAK,iBAAiB,EAAQ,CAI9B,KAAK,KAAK,MAAM,wCAAyC,EAAW,OAAQ,eACjE,EAAW,KAAM,GAAG,CAUvC,mBAA0B,EAAqB,EAA0B,KACzE,CACI,KAAK,KAAK,MAAM,0CAA2C,EAAY,gBAAiB,EAAS,GAAG,CAEpG,IAAI,EAAkB,EAGtB,IAAI,GAAM,CAAE,EAAU,KAAoB,KAAK,UAAU,SAAS,CAClE,CACI,IAAM,EAAc,EAAe,OAAQ,GAC3C,CACI,IAAM,EAAiB,EAAQ,SAAW,KACpC,EAAa,EAAQ,OAAO,OAAS,GAAc,IAAmB,EAK5E,OAJI,GAEA,IAEG,GACT,CAGC,EAAY,SAAW,EAAe,QAErC,KAAK,UAAU,IAAI,EAAU,EAAY,CAIjD,KAAK,KAAK,KAAK,WAAY,EAAiB,wBAAyB,EAAY,gBAAiB,EAAS,GAAG,CAUlH,qBAA4B,EAAqB,EAA0B,EAC3E,CACI,IAAM,EAAqB,EAAE,CAE7B,IAAI,IAAM,KAAkB,KAAK,UAAU,QAAQ,CAE/C,IAAI,IAAM,KAAW,EACrB,CAEI,GADG,EAAQ,OAAO,OAAS,GACxB,IAAe,IAAA,IAAa,EAAQ,aAAe,EAAc,SAEpE,IAAM,EAAiB,EAAQ,SAAW,KACvC,IAAY,IAAA,IAAa,IAAmB,GAE/C,EAAO,KAAK,EAAQ,CAI5B,OAAO,EAWX,qBACA,CACI,KAAK,KAAK,MAAM,kCAAkC,CAGlD,IAAM,EAAU,CAAE,GAAG,KAAK,SAAS,QAAQ,CAAE,CAAC,IAAK,GAG5C,EAAO,OAAS,SAER,CACH,KAAM,EAAO,KACb,KAAM,EAAO,KACb,MAAO,EAAO,MACd,SAAU,EAAO,UAAY,EAC7B,SAAU,EAAO,UAAY,EAChC,CAEE,EACT,CAGI,EAAiC,EAAE,CAGzC,IAAI,IAAM,KAAkB,KAAK,UAAU,QAAQ,CAE/C,IAAI,IAAM,KAAW,EAGjB,EAAS,KAAK,EAAQ,QAAQ,CAAC,CAKvC,IAAM,EAAW,CAAE,GAAG,KAAK,UAAU,QAAQ,CAAE,CAAC,IAAK,IAAa,CAC9D,KAAM,EAAQ,KACd,UAAW,EAAQ,UACnB,OAAQ,KAAK,gBAAgB,IAAI,EAAQ,KAAK,CACjD,EAAE,CAKH,OAHA,KAAK,KAAK,KAAK,2BAA4B,EAAQ,OAAQ,YAAa,EAAS,OAAQ,aAC/E,EAAS,OAAQ,WAAW,CAE/B,CACH,UACA,WACA,WACH,CAQL,oBAA2B,EAC3B,CACI,KAAK,KAAK,MAAM,kCAAkC,CAGlD,KAAK,UAAU,OAAO,CACtB,KAAK,SAAS,OAAO,CACrB,KAAK,UAAU,OAAO,CACtB,KAAK,gBAAgB,OAAO,CAG5B,IAAI,IAAM,KAAU,EAAO,QAEvB,KAAK,eAAe,EAAO,CAI/B,IAAI,IAAM,KAAe,EAAO,SAE5B,KAAK,gBAAgB,EAAY,KAAM,EAAY,UAAU,CAG1D,EAAY,QAEX,KAAK,gBAAgB,EAAY,KAAK,CAK9C,IAAI,IAAM,KAAc,EAAO,SAE3B,GACA,CACI,KAAK,gBAAgB,EAAW,OAE7B,EACP,CACO,aAAe,OAEd,KAAK,KAAK,MAAM,wCAAyC,EAAW,OAAQ,KAAM,EAAI,UAAW,CAK7G,KAAK,KAAK,KAAK,2BAA4B,EAAO,QAAQ,OAAQ,YACxD,EAAO,SAAS,OAAQ,aAAc,EAAO,SAAS,OAAQ,WAAW,CAMvF,MAAM,WACN,CAII,GAHA,KAAK,KAAK,KAAK,8BAA8B,CAG1C,KAAK,cACR,CACI,GAAM,CAAE,UAAW,KAAK,cACxB,KAAK,iBAAiB,CACtB,EAAW,MAAM,iDAAiD,CAAC,CAIvE,AAGI,KAAK,qBADL,KAAK,mBAAmB,CACC,MAI7B,KAAK,UAAU,OAAO,CACtB,KAAK,gBAAgB,OAAO,CAG5B,KAAK,SAAS,OAAO,CAGrB,KAAK,UAAU,OAAO,CACtB,KAAK,gBAAgB,OAAO,CAG5B,KAAK,sBAAwB,KAE7B,KAAK,KAAK,KAAK,wCAAwC,GCzjC/D,SAAgB,GAChB,CACI,OAAO,OAAO,OAAW,KAAsB,OAAO,WAAa,OAGvE,SAAgB,GAChB,CACI,OAAO,GAAW,EACX,CAAC,CAAC,OAAO,UAAU,IEZ9B,IAAI,IDyCS,GAAb,KACA,CACI,QACA,UACA,eACA,cACA,cACA,KACA,iBACA,oBAGA,gBAAsD,EAAE,CAGxD,gBAA0B,EAG1B,QAAkB,GAElB,QAAiB,GAIjB,YACI,EACA,EACA,EACA,EACA,EACA,EAEJ,CACI,KAAK,QAAU,EACf,KAAK,UAAY,EACjB,KAAK,eAAiB,EACtB,KAAK,cAAgB,EACrB,KAAK,cAAgB,EACrB,KAAK,KAAO,GAAQ,UAAU,cAAc,EAAI,IAAI,EAAW,cAAc,CAG7E,KAAK,iBAAmB,KAAK,YAAY,KAAK,KAAK,CACnD,KAAK,oBAAsB,KAAK,eAAe,KAAK,KAAK,CAEtD,GAAW,EAGV,OAAO,iBAAiB,SAAU,KAAK,oBAAoB,CAInE,IAAI,cACJ,CACI,OAAO,KAAK,cAAc,cAAc,OAAS,KAOrD,IAAI,UACJ,CACI,OAAO,KAAK,QA6BhB,sBAA6B,EAC7B,CACI,IAAM,EAAK,KAAK,kBAMhB,OALA,KAAK,gBAAgB,KAAK,CAAE,KAAI,WAAU,CAAC,CAE3C,KAAK,KAAK,MAAM,kCAAmC,EAAI,GAAG,KAI1D,CACI,IAAM,EAAQ,KAAK,gBAAgB,UAAW,GAAO,EAAG,KAAO,EAAG,CAC/D,IAAU,KAET,KAAK,gBAAgB,OAAO,EAAO,EAAE,CACrC,KAAK,KAAK,MAAM,oCAAqC,EAAI,GAAG,GAOxE,aACA,CAEI,IAAM,EADc,KAAK,QAAQ,cAAc,CACf,IAMhC,GAHA,KAAK,cAAc,cAAc,CAG9B,CAAC,KAAK,QACT,CAEI,KAAK,eAAe,aAAa,EAAU,CAG3C,IAAI,GAAM,CAAE,cAAc,KAAK,gBAE3B,GACA,CACI,EAAS,EAAU,OAEhB,EACP,CACI,KAAK,KAAK,MAAM,2BAA4B,EAAI,EAMzD,KAAK,cAEJ,KAAK,aAAa,QAAQ,CAIlC,gBACA,CACO,KAAK,SAEJ,KAAK,QAAQ,QAAQ,CAQ7B,MAAM,OACN,CAEI,KAAK,QAAQ,cAAc,KAAK,iBAAiB,CAEjD,KAAK,QAAU,GAEf,KAAK,KAAK,KAAK,gDAAgD,CAGnE,MAAM,MACN,CACI,KAAK,QAAU,GACf,KAAK,QAAQ,gBAAgB,CAE7B,KAAK,KAAK,KAAK,gDAAgD,CAYnE,MAAM,EACN,CACI,GAAG,KAAK,QACR,CACI,KAAK,KAAK,MAAM,yBAAyB,CACzC,OAGJ,KAAK,QAAU,GACf,KAAK,KAAK,KAAK,cAAe,EAAS,YAAa,EAAQ,GAAK,GAAG,CAEpE,KAAK,UAAU,QAAQ,CACnB,KAAM,cACN,QAAS,CAAE,SAAQ,CACtB,CAAC,CAWN,OAAO,EACP,CACI,GAAG,CAAC,KAAK,QACT,CACI,KAAK,KAAK,MAAM,qBAAqB,CACrC,OAGJ,KAAK,QAAU,GACf,KAAK,KAAK,KAAK,eAAgB,EAAS,YAAa,EAAQ,GAAK,GAAG,CAErE,KAAK,UAAU,QAAQ,CACnB,KAAM,eACN,QAAS,CAAE,SAAQ,CACtB,CAAC,CAON,MAAM,WACN,CACI,KAAK,KAAK,KAAK,2BAA2B,CAGvC,KAAK,SAEJ,MAAM,KAAK,MAAM,CAIlB,GAAW,EAEV,OAAO,oBAAoB,SAAU,KAAK,oBAAoB,CAIlE,KAAK,gBAAkB,EAAE,CAEzB,KAAK,KAAK,KAAK,qCAAqC,GCpSpD,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,SAAS,GAAG,CAAC,UAAU,EAAE,CAAC,SAAgB,GAAO,EAAE,CAAC,IAAK,GAAG,IAAI,EAAE,GAAG,EAAE,EAAE,OAAO,UAAU,CAAC,GAAG,CAAC,GAAS,IAAN,IAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,GAAG,EAAE,IAAI,KAAK,QAAQ,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE,MCkBtO,IAAa,EAAa,GAAO,GAAG,CCsEd,EAAtB,KACA,CAII,OAA4D,KAM5D,MAAM,EACN,CACI,GAAG,CAAC,KAAK,OAEL,MAAU,MAAM,uCAAuC,CAG3D,EAAM,SAAW,KAAK,OAAO,GAE5B,KAAK,OAAO,SAA8D,QAAQ,EAAM,CAU7F,kBAAkB,EAAiB,EACnC,CACI,GAAG,CAAC,KAAK,OAEL,MAAU,MAAM,uCAAuC,CAG3D,KAAK,MAAM,CACP,KAAM,uBACN,QAAS,CACL,SAAU,KAAK,OAAO,GACtB,WAAY,KAAK,OAAO,KACxB,MAAO,GAAS,KAAK,OAAO,MAC5B,UACH,CACJ,CAAC,CAON,WAAW,EACX,CACI,KAAK,OAAS,IA2DT,EAAb,KACA,CAEI,GAEA,KAGA,KAOA,KAGA,SAA0C,EAAE,CAG5C,OAA6C,KAE7C,MAGA,UAA0C,EAAE,CAE5C,SAEA,YAA0C,KAC1C,eAAkD,KAClD,MAA8B,IAAI,IAClC,cAA6D,IAAI,IAUjE,YACI,EACA,EACA,EACA,EACA,EAEJ,CACI,KAAK,GAAK,GAAY,CACtB,KAAK,KAAO,EACZ,KAAK,KAAO,EACZ,KAAK,MAAQ,EACb,KAAK,SAAW,EAGhB,IAAI,IAAM,KAAgB,EAEtB,KAAK,eAAe,IAAI,EAAe,CAW/C,IAAI,MACJ,CACI,OAAO,KAAK,MAOhB,OAAO,EACP,CACI,OAAO,KAAK,MAAM,IAAI,EAAI,CAO9B,QAAQ,EACR,CACI,KAAK,MAAM,IAAI,EAAI,CAOvB,WAAW,EACX,CACI,OAAO,KAAK,MAAM,OAAO,EAAI,CAWjC,aAAa,EACb,CACQ,QAAK,KAKT,OAAO,KAAK,KAAK,YACZ,GAAU,EAAM,OAAS,EAC1B,GACH,CAAC,GAON,SAAS,EACT,CACI,GAAG,CAAC,KAAK,KAEL,OAGJ,IAAI,EAAsC,KAAK,KAE/C,IAAI,IAAM,KAAQ,EAAK,MAAM,IAAI,CAE7B,EAAU,GAAS,YACd,GAAU,EAAM,OAAS,EAC1B,GACH,CAAC,GAGN,OAAO,EAUX,cAAc,EAAsB,EACpC,CACK,KAA6B,KAAO,EACrC,KAAK,YAAc,EAGnB,IAAI,IAAM,KAAY,KAAK,UAEvB,EAAS,iBAAiB,EAAM,EAAW,CAI/C,IAAI,IAAM,KAAS,KAAK,SAEjB,EAAM,OAEJ,EAAM,KAA8B,OAAS,GAU1D,iBACA,CAEI,KAAK,aAAa,CAGlB,IAAI,IAAM,KAAS,KAAK,SAEjB,EAAM,OAEJ,EAAM,KAA8B,OAAS,MAKtD,IAAI,IAAM,KAAY,KAAK,UAEvB,EAAS,kBAAkB,CAG9B,KAA6B,KAAO,IAAA,GACrC,KAAK,YAAc,KASvB,UAAU,EACV,CACI,IAAM,EAAW,KAAK,aAAa,UAAU,cAAc,cAAc,SACtE,GAEC,EAAS,gBAAgB,EAAW,KAAK,CASjD,YAAY,EACZ,CACI,IAAM,EAAW,KAAK,aAAa,UAAU,cAAc,cAAc,SACtE,IAEI,EAEC,EAAS,kBAAkB,EAAW,KAAK,CAI3C,EAAS,qBAAqB,KAAK,EAc/C,UAAU,EACV,CACI,KAAK,SAAS,KAAK,EAAM,CACxB,EAA8B,OAAS,KAGrC,KAAK,MAAQ,EAAM,OAEjB,EAAM,KAA8B,OAAS,KAAK,MAS3D,aAAa,EACb,CACI,IAAM,EAAQ,KAAK,SAAS,QAAQ,EAAM,CACvC,IAAU,KAET,KAAK,SAAS,OAAO,EAAO,EAAE,CAC7B,EAA8B,OAAS,KAGrC,EAAM,OAEJ,EAAM,KAA8B,OAAS,OAe1D,kBAAkB,EAClB,CACI,KAAK,eAAiB,EAW1B,MAAM,KAAK,EAAiB,EAAe,EAC3C,CACI,GAAG,IAAW,KAAK,GAEf,OAAO,KAAK,MAAM,EAAM,EAAS,KAAK,GAAG,CAG7C,GAAG,CAAC,KAAK,eAEL,MAAU,MAAM,kDAAkD,CAGtE,OAAO,KAAK,eAAe,KAAK,EAAQ,EAAM,EAAS,KAAK,GAAG,CAWnE,MAAM,QAAW,EAAiB,EAAe,EACjD,CACI,GAAG,IAAW,KAAK,GAEf,OAAO,KAAK,SAAY,EAAM,EAAS,KAAK,GAAG,CAGnD,GAAG,CAAC,KAAK,eAEL,MAAU,MAAM,kDAAkD,CAGtE,OAAO,KAAK,eAAe,QAAW,EAAQ,EAAM,EAAS,KAAK,GAAG,CAUzE,MAAM,MAAM,EAAe,EAAoB,EAC/C,CACI,MAAM,KAAK,cAAc,CAAE,OAAM,SAAU,KAAK,GAAI,WAAmB,UAAkB,CAAC,CAU9F,MAAM,SAAY,EAAe,EAAoB,EACrD,CACI,IAAM,EAAoB,CAAE,OAAM,SAAU,KAAK,GAAI,WAAmB,UAAkB,CACtF,EAEJ,IAAI,IAAM,KAAY,KAAK,UAEvB,GACA,CAEI,IAAM,EAAS,MAAM,EAAS,iBAAiB,EAAO,KAAK,MAAM,CACjE,GAAG,IAAW,IAAA,GAEV,MAAO,CAAE,QAAS,GAAM,MAAO,EAAa,OAG7C,EACP,CAEI,EAAY,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,CAIpE,MAAO,CACH,QAAS,GACT,MAAO,GAAa,2BAA4B,EAAM,eAAgB,KAAK,MAAQ,KAAK,GAAI,GAC/F,CAaL,OAAO,EAAuB,EAC9B,CAEI,KAAK,MAAQ,CAAE,GAAG,EAAc,CAGhC,KAAK,MAAM,OAAO,CAClB,IAAI,IAAM,KAAO,EAEb,KAAK,MAAM,IAAI,EAAI,CAIpB,KAAK,MAEJ,KAAK,iBAAiB,CAI1B,IAAI,IAAM,KAAY,KAAK,UAEvB,EAAS,UAAU,KAAK,MAAM,CAIlC,IAAI,IAAM,KAAS,KAAK,SAEpB,EAAM,OAAO,EAAE,CAAE,EAAE,CAAC,CAIvB,KAA6B,OAAS,KAO3C,MAAM,cAAc,EACpB,CACI,IAAI,IAAM,KAAY,KAAK,UAIvB,GADe,MAAM,EAAS,aAAa,EAAO,KAAK,MAAM,CAIzD,MASZ,QAAQ,EACR,CACI,IAAI,IAAM,KAAY,KAAK,UAEvB,EAAS,SAAS,EAAI,KAAK,MAAM,CAIrC,IAAI,IAAM,KAAS,KAAK,SAEpB,EAAM,QAAQ,EAAG,CAOzB,MAAM,UACN,CAEI,IAAI,IAAM,IAAS,CAAE,GAAG,KAAK,SAAU,CAGnC,MAAM,EAAM,UAAU,CAG1B,KAAK,SAAS,OAAS,EAEvB,IAAI,IAAM,KAAY,KAAK,UAGvB,MAAM,EAAS,WAAW,CAI9B,IAAI,IAAM,KAAgB,KAAK,cAAc,QAAQ,CAEjD,EAAa,aAAa,CAG9B,KAAK,UAAY,EAAE,CACnB,KAAK,cAAc,OAAO,CAO9B,kBAA0B,EAC1B,CACI,OAAO,KAAK,UAAU,UAAW,GAAO,EAAG,cAAgB,EAAc,CAO7E,YAAY,EACZ,CACI,OAAO,KAAK,kBAAkB,EAAc,GAAK,GAOrD,YAA0C,EAC1C,CACI,OAAO,KAAK,UAAU,KAAM,GAAO,EAAG,cAAgB,EAAc,CAkBxE,eAAe,EAA+B,EAC9C,CAEI,GAAG,KAAK,YAAY,EAAS,YAA6C,CAEtE,MAAU,MAAM,YAAa,EAAS,YAAY,KAAM,sCAAsC,CAIlG,IAAI,IAAM,KAAS,EAAS,mBAC5B,CAEI,IAAM,EAAuB,KAAK,cAAc,IAAI,EAAM,CAC1D,GAAG,EAEC,EAAqB,YAGzB,CAII,IAAM,EADM,KAAK,SACO,UAAU,EAAO,KAAK,cAAc,KAAK,KAAK,CAAC,CACvE,KAAK,cAAc,IAAI,EAAO,CAAE,MAAO,EAAG,cAAa,CAAC,EAKhE,GAAG,CAAC,EAGA,KAAK,UAAU,KAAK,EAAS,KAGjC,CAKI,GAH2B,CAAE,EAAQ,OAAQ,EAAQ,MAAO,EAAQ,GAAI,CACnE,OAAQ,GAAQ,IAAQ,IAAA,GAAU,CAEjB,OAAS,EAE3B,MAAU,MAAM,2DAA2D,CAG/E,GAAG,EAAQ,OACX,CACI,IAAM,EAAQ,KAAK,kBAAkB,EAAQ,OAAO,CACpD,GAAG,IAAU,GAET,MAAU,MAAM,uCAAwC,EAAQ,OAAO,KAAM,QAAQ,CAEzF,KAAK,UAAU,OAAO,EAAO,EAAG,EAAS,SAErC,EAAQ,MAChB,CACI,IAAM,EAAQ,KAAK,kBAAkB,EAAQ,MAAM,CACnD,GAAG,IAAU,GAET,MAAU,MAAM,uCAAwC,EAAQ,MAAM,KAAM,QAAQ,CAExF,KAAK,UAAU,OAAO,EAAQ,EAAG,EAAG,EAAS,SAEzC,EAAQ,KAAO,IAAA,GACvB,CAEI,IAAM,EAAe,KAAK,IAAI,EAAG,KAAK,IAAI,EAAQ,GAAI,KAAK,UAAU,OAAO,CAAC,CAC7E,KAAK,UAAU,OAAO,EAAc,EAAG,EAAS,MAKhD,KAAK,UAAU,KAAK,EAAS,CAIrC,EAAS,WAAW,KAAK,CAGtB,KAAK,MAAQ,KAAK,aAEjB,EAAS,iBAAiB,KAAK,KAAM,KAAK,YAAY,CAQ9D,eAAe,EACf,CACI,IAAM,EAAQ,KAAK,kBAAkB,EAAc,CACnD,GAAG,IAAU,GAET,MAAO,GAGX,IAAM,EAAW,KAAK,UAAU,GAG7B,KAAK,MAEJ,EAAS,kBAAkB,CAI/B,IAAI,IAAM,KAAS,EAAS,mBAC5B,CACI,IAAM,EAAe,KAAK,cAAc,IAAI,EAAM,CAC/C,IAEC,EAAa,QACV,EAAa,OAAS,IAErB,EAAa,aAAa,CAC1B,KAAK,cAAc,OAAO,EAAM,GAS5C,OAHA,KAAK,UAAU,OAAO,EAAO,EAAE,CAC/B,EAAS,WAAW,KAAK,CAElB,KC1zBF,GAAb,KACA,CAEI,SAGA,SAA6C,IAAI,IAGjD,kBAAgE,IAAI,IAGpE,YAA0C,KAG1C,eAGA,KAOA,gBAAyD,IAAI,IAG7D,gBAAyD,IAAI,IAG7D,eAAwD,IAAI,IAG5D,gBAA2D,IAAI,IAG/D,OAA6C,IAAI,IAQjD,YAAY,EAAyB,EAAqC,EAC1E,CACI,KAAK,SAAW,EAChB,KAAK,eAAiB,EACtB,KAAK,KAAO,GAAQ,UAAU,gBAAgB,EAAI,IAAI,EAAW,gBAAgB,CACjF,KAAK,KAAK,KAAK,4BAA4B,CAW/C,eAAe,EACf,CACI,KAAK,YAAc,EAGvB,IAAY,SACZ,CACI,GAAG,CAAC,KAAK,YAEL,MAAU,MAAM,kEAAkE,CAEtF,OAAO,KAAK,YAUhB,gBAA2B,EAA+B,EAC1D,CACI,IAAI,EAAM,EAAI,IAAI,EAAI,CAMtB,OALI,IAEA,EAAM,IAAI,IACV,EAAI,IAAI,EAAK,EAAI,EAEd,EAMX,aAAqB,EACrB,CAEI,KAAK,gBAAgB,KAAK,gBAAiB,EAAO,KAAK,CAAC,IAAI,EAAO,CAGhE,EAAO,MAEN,KAAK,gBAAgB,KAAK,gBAAiB,EAAO,KAAK,CAAC,IAAI,EAAO,CAIvE,IAAI,IAAM,KAAO,EAAO,KAEpB,KAAK,gBAAgB,KAAK,eAAgB,EAAI,CAAC,IAAI,EAAO,CAI3D,EAAO,MAEN,KAAK,gBAAgB,IAAI,EAAO,KAAM,EAAO,CAOrD,eAAuB,EACvB,CAEI,IAAM,EAAU,KAAK,gBAAgB,IAAI,EAAO,KAAK,CAWrD,GAVG,IAEC,EAAQ,OAAO,EAAO,CACnB,EAAQ,OAAS,GAEhB,KAAK,gBAAgB,OAAO,EAAO,KAAK,EAK7C,EAAO,KACV,CACI,IAAM,EAAU,KAAK,gBAAgB,IAAI,EAAO,KAAK,CAClD,IAEC,EAAQ,OAAO,EAAO,CACnB,EAAQ,OAAS,GAEhB,KAAK,gBAAgB,OAAO,EAAO,KAAK,EAMpD,IAAI,IAAM,KAAO,EAAO,KACxB,CACI,IAAM,EAAS,KAAK,eAAe,IAAI,EAAI,CACxC,IAEC,EAAO,OAAO,EAAO,CAClB,EAAO,OAAS,GAEf,KAAK,eAAe,OAAO,EAAI,EAMxC,EAAO,MAEN,KAAK,gBAAgB,OAAO,EAAO,KAAK,CAYhD,oBAA4B,EAC5B,CACI,IAAM,EAAW,EAAS,QAC1B,GAAG,CAAC,EAEA,OAAO,EAGX,IAAM,EAAU,KAAK,kBAAkB,IAAI,EAAS,CACpD,GAAG,CAAC,EACJ,CACI,IAAM,EAAW,kBAAmB,EAAU,8CAE9C,MADA,KAAK,KAAK,MAAM,EAAS,CACf,MAAM,EAAS,CAI7B,IAAI,EAqCJ,OApCG,EAAQ,MAAQ,EAAS,QAExB,EAAa,CAAE,GAAG,IAAI,IAAI,CAAE,GAAI,EAAQ,MAAQ,EAAE,CAAG,GAAI,EAAS,MAAQ,EAAE,CAAG,CAAC,CAAE,EAI9C,CAEpC,KAAM,EAAS,KACf,KAAM,EAAS,MAAQ,EAAQ,KAC/B,KAAM,EAAS,MAAQ,EAAQ,KAG/B,aAAc,CAAE,GAAG,EAAQ,aAAc,GAAG,EAAS,aAAc,CAGnE,UAAW,EAAS,WAAa,EAAQ,UACzC,QAAS,EAAS,SAAW,EAAQ,QAGrC,KAAM,EAGN,SAAU,EAAS,UAAY,EAAQ,SACvC,SAAU,EAAS,UAAY,EAAQ,SAGvC,SAAU,EAAS,UAAY,EAAQ,SAGvC,eAAgB,EAAS,gBAAkB,EAAQ,eACnD,SAAU,EAAS,UAAY,EAAQ,SACvC,gBAAiB,EAAS,iBAAmB,EAAQ,gBACrD,UAAW,EAAS,WAAa,EAAQ,UAC5C,CAcL,sBAA8B,EAAyB,EACvD,CAmBI,MAXA,EANG,EAAe,OAAS,EAAU,MAMlC,EAAe,OAAS,UAAY,EAAU,OAAS,WAGlD,EAAU,WAAa,IAAA,IAAa,EAAe,WAAa,EAAU,UACtE,EAAU,WAAa,IAAA,IAAa,EAAe,WAAa,EAAU,WAe1F,uBAA+B,EAC/B,CACQ,KAAU,QAKd,IAAI,IAAM,KAAU,EAAU,QAE1B,GACA,CAEI,IAAM,EAAiB,KAAK,eAAe,UAAU,EAAO,KAAK,CAE7D,EAMK,KAAK,sBAAsB,EAAgB,EAAO,CAUvD,KAAK,KAAK,MACN,WAAY,EAAO,KAAM,qEAC5B,CAVD,KAAK,KAAK,KACN,WAAY,EAAO,KAAM,uDACX,EAAU,KAAM,cAAe,KAAK,UAAU,EAAO,CAAE,eACpD,KAAK,UAAU,EAAe,GAClD,EATD,KAAK,KAAK,MAAM,uBAAwB,EAAO,KAAM,sBAAuB,EAAU,KAAM,GAAG,CAC/F,KAAK,eAAe,eAAe,EAAO,QAiB3C,EACP,CACI,KAAK,KAAK,MAAM,8BAA+B,EAAO,KAAM,KAClD,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,GAAI,EAK1E,aAAa,EACb,CACI,KAAK,KAAK,MAAM,YAAa,KAAK,SAAS,KAAM,oBAAqB,IAAM,CAC5E,IAAI,IAAM,KAAU,KAAK,SAAS,QAAQ,CAGlC,EAAO,QAEP,EAAO,QAAQ,EAAG,CAc9B,yBAAyB,EACzB,CACI,KAAK,KAAK,MAAM,kCAAmC,EAAU,OAAQ,CAGrE,IAAM,EAAW,EAAU,QACrB,KAAK,oBAAoB,EAAU,CACnC,EAGN,KAAK,uBAAuB,EAAS,CAGrC,KAAK,kBAAkB,IAAI,EAAS,KAAM,EAAS,CAGhD,EAAS,UAAY,EAAS,SAAW,GAInC,KAAK,QAAQ,EAAS,KAAM,EAAS,SAAS,CAQ3D,cAAc,EACd,CACI,OAAO,KAAK,kBAAkB,IAAI,EAAK,CAS3C,MAAM,aAIF,EACA,EAAuC,EAAE,CAE7C,CACI,KAAK,KAAK,MAAM,4BAA6B,IAAQ,CACrD,IAAM,EAAY,KAAK,kBAAkB,IAAI,EAAK,CAClD,GAAG,CAAC,EACJ,CACI,IAAM,EAAW,eAAgB,EAAM,qBAEvC,MADA,KAAK,KAAK,MAAM,EAAS,CACf,MAAM,EAAS,CAI7B,GAAG,EAAU,SACb,CACI,IAAM,EAAO,KAAK,OAAO,IAAI,EAAK,CAClC,GAAG,GAAQ,EAAK,OAAS,EACzB,CACI,KAAK,KAAK,MAAM,wCAAyC,IAAQ,CACjE,IAAM,EAAS,EAAK,KAAK,CAkBzB,GAfA,EAAO,OACH,EAAU,aACV,EAAU,MAAQ,EAAE,CACvB,CAGD,EAAO,kBAAkB,KAAK,CAG3B,EAAQ,eAEP,EAAO,MAAQ,CAAE,GAAG,EAAO,MAAO,GAAG,EAAQ,aAAc,EAI5D,EAAQ,KAEP,IAAI,IAAM,KAAO,EAAQ,KAErB,EAAO,QAAQ,EAAI,CAe3B,OAVG,EAAQ,MAEP,EAAO,cAAc,EAAQ,KAAM,KAAK,QAAQ,CAIpD,KAAK,SAAS,IAAI,EAAO,GAAI,EAAO,CACpC,KAAK,aAAa,EAAO,CAEzB,KAAK,KAAK,MAAM,mBAAoB,EAAO,GAAI,YAAY,CACpD,GAIf,KAAK,KAAK,MAAM,gCAAiC,EAAU,WAAW,QAAU,EAAG,YAAY,CAC/F,IAAI,EAAc,CAAE,GAAG,EAAU,aAAc,GAAG,EAAQ,aAAc,CAGxE,GAAG,EAAU,eACb,CACI,KAAK,KAAK,MAAM,gDAAiD,IAAQ,CACzE,IAAM,EAAS,MAAM,EAAU,eAAe,EAAY,CAEvD,IAAW,IAAA,KAEV,EAAc,GAKtB,IAAM,EAAa,EAAQ,MAAQ,EAAU,KAGvC,EAAS,IAAI,EACf,EAAU,KACV,KAAK,SACL,EACA,EAAU,UACV,EACH,CAYD,GATA,EAAO,kBAAkB,KAAK,CAG3B,EAAQ,MAEP,EAAO,cAAc,EAAQ,KAAM,KAAK,QAAQ,CAIjD,EAAU,KAET,IAAI,IAAM,KAAO,EAAU,KAEvB,EAAO,QAAQ,EAAI,CAK3B,GAAG,EAAQ,KAEP,IAAI,IAAM,KAAO,EAAQ,KAErB,EAAO,QAAQ,EAAI,CAU3B,GALA,KAAK,SAAS,IAAI,EAAO,GAAI,EAAO,CACpC,KAAK,aAAa,EAAO,CACzB,KAAK,KAAK,MAAM,2BAA4B,EAAO,KAAO,EAAa,WAAY,EAAY,GAAK,KAAM,CAGvG,EAAU,SACb,CACI,KAAK,KAAK,MAAM,0CAA2C,IAAQ,CACnE,IAAM,EAAS,MAAM,EAAU,SAAS,EAAO,MAAM,CAElD,IAAW,IAAA,KAEV,EAAO,MAAQ,GAKvB,GAAG,EAAU,SAET,IAAI,IAAM,KAAY,EAAU,SAChC,CACI,IAAM,EAA8C,CAAE,GAAG,EAAS,aAAc,CAG7E,EAAS,WAER,EAAkB,UAAY,EAAS,UAExC,EAAS,WAER,EAAkB,UAAY,EAAS,UAI3C,IAAM,EAAc,MAAM,KAAK,aAAa,EAAS,KAAM,CACvD,KAAM,EAAS,KACf,aAAc,EACjB,CAAC,CAEF,EAAO,UAAU,EAAY,CAIrC,OAAO,EAQX,MAAM,cAAc,EACpB,CACI,KAAK,KAAK,MAAM,sBAAuB,IAAY,CACnD,IAAM,EAAS,KAAK,SAAS,IAAI,EAAS,CAC1C,GAAG,EACH,CAEI,IAAM,EAAY,KAAK,kBAAkB,IAAI,EAAO,KAAK,CAGzD,GAAG,GAAW,SACd,CACI,KAAK,KAAK,MAAM,kBAAmB,EAAU,wBAAwB,CAGrE,IAAI,IAAM,IAAS,CAAE,GAAG,EAAO,SAAU,CAGrC,MAAM,KAAK,cAAc,EAAM,GAAG,CAInC,EAAO,QAEN,EAAO,OAAO,aAAa,EAAO,CAItC,KAAK,eAAe,EAAO,CAG3B,EAAO,OAAO,EAAU,aAAc,EAAU,MAAQ,EAAE,CAAC,CAG3D,IAAI,EAAO,KAAK,OAAO,IAAI,EAAO,KAAK,CACnC,IAEA,EAAO,EAAE,CACT,KAAK,OAAO,IAAI,EAAO,KAAM,EAAK,EAEtC,EAAK,KAAK,EAAO,CAGjB,KAAK,SAAS,OAAO,EAAS,CAE9B,KAAK,KAAK,MAAM,UAAW,EAAU,SAAS,CAC9C,OAMJ,IAAI,IAAM,IAAS,CAAE,GAAG,EAAO,SAAU,CAGrC,MAAM,KAAK,cAAc,EAAM,GAAG,CAUtC,GANG,EAAO,QAEN,EAAO,OAAO,aAAa,EAAO,CAInC,GAAW,gBACd,CACI,KAAK,KAAK,MAAM,4CAA6C,IAAY,CACzE,IAAM,EAAS,MAAM,EAAU,gBAAgB,EAAO,MAAM,CAEzD,IAAW,IAAA,KAEV,EAAO,MAAQ,GAKvB,KAAK,eAAe,EAAO,CAG3B,MAAM,EAAO,UAAU,CAGpB,GAAW,YAEV,KAAK,KAAK,MAAM,sCAAuC,IAAY,CACnE,MAAM,EAAU,UAAU,EAAO,MAAM,EAI3C,KAAK,SAAS,OAAO,EAAS,CAE9B,KAAK,KAAK,MAAM,UAAW,EAAU,YAAY,MAIjD,KAAK,KAAK,KAAK,6CAA8C,IAAY,CAQjF,UAAU,EACV,CAEI,OADA,KAAK,KAAK,MAAM,mBAAoB,IAAY,CACzC,KAAK,SAAS,IAAI,EAAS,EAAI,KAO1C,UAAU,EACV,CACI,KAAK,KAAK,MAAM,2BAA4B,EAAO,GAAI,UAAW,EAAO,KAAM,GAAG,CAClF,EAAO,kBAAkB,KAAK,CAC9B,KAAK,SAAS,IAAI,EAAO,GAAI,EAAO,CACpC,KAAK,aAAa,EAAO,CAO7B,aAAa,EACb,CACI,KAAK,KAAK,MAAM,oBAAqB,IAAY,CACjD,IAAM,EAAS,KAAK,SAAS,IAAI,EAAS,CACvC,GAEC,KAAK,eAAe,EAAO,CAC3B,KAAK,SAAS,OAAO,EAAS,CAC9B,KAAK,KAAK,MAAM,UAAW,EAAU,UAAU,EAI/C,KAAK,KAAK,KAAK,4CAA6C,IAAY,CAchF,SAAS,EAAmB,EAC5B,CACI,IAAM,EAAS,KAAK,SAAS,IAAI,EAAS,CACpC,EAAQ,KAAK,SAAS,IAAI,EAAQ,CAgBxC,OAdI,EAMA,GAMJ,EAAO,UAAU,EAAM,CACvB,KAAK,KAAK,MAAM,eAAgB,EAAS,aAAc,IAAY,CAC5D,KANH,KAAK,KAAK,KAAK,2BAA4B,EAAS,aAAa,CAC1D,KAPP,KAAK,KAAK,KAAK,4BAA6B,EAAU,aAAa,CAC5D,IAoBf,YAAY,EAAmB,EAC/B,CACI,IAAM,EAAS,KAAK,SAAS,IAAI,EAAS,CACpC,EAAQ,KAAK,SAAS,IAAI,EAAQ,CAgBxC,OAdI,EAMA,GAMJ,EAAO,aAAa,EAAM,CAC1B,KAAK,KAAK,MAAM,iBAAkB,EAAS,eAAgB,IAAY,CAChE,KANH,KAAK,KAAK,KAAK,8BAA+B,EAAS,aAAa,CAC7D,KAPP,KAAK,KAAK,KAAK,+BAAgC,EAAU,aAAa,CAC/D,IAuBf,UAAU,EACV,CACI,IAAM,EAAU,KAAK,gBAAgB,IAAI,EAAK,CAC9C,GAAG,GAAW,EAAQ,KAAO,EAEzB,OAAO,EAAQ,QAAQ,CAAC,MAAM,CAAC,MASvC,kBAAkB,EAClB,CACI,IAAM,EAAU,KAAK,gBAAgB,IAAI,EAAK,CAC9C,OAAO,EAAU,MAAM,KAAK,EAAQ,CAAG,EAAE,CAO7C,UAAU,EACV,CACI,IAAM,EAAU,KAAK,gBAAgB,IAAI,EAAK,CAC9C,OAAO,EAAU,MAAM,KAAK,EAAQ,CAAG,EAAE,CAO7C,SAAS,EACT,CACI,IAAM,EAAS,KAAK,eAAe,IAAI,EAAI,CAC3C,OAAO,EAAS,MAAM,KAAK,EAAO,CAAG,EAAE,CAQ3C,UAAU,EAAiB,EAAuB,MAClD,CACI,GAAG,EAAK,SAAW,EAEf,MAAO,EAAE,CAGb,GAAG,IAAS,MACZ,CAEI,IAAM,EAAS,IAAI,IACnB,IAAI,IAAM,KAAO,EACjB,CACI,IAAM,EAAS,KAAK,eAAe,IAAI,EAAI,CAC3C,GAAG,EAEC,IAAI,IAAM,KAAU,EAEhB,EAAO,IAAI,EAAO,CAI9B,OAAO,MAAM,KAAK,EAAO,KAG7B,CAGI,IAAM,EAAO,EACR,IAAK,GAAQ,KAAK,eAAe,IAAI,EAAI,CAAC,CAC1C,OAAQ,GAAiC,IAAQ,IAAA,GAAU,CAGhE,GAAG,EAAK,SAAW,EAAK,OAEpB,MAAO,EAAE,CAIb,EAAK,MAAM,EAAM,IAAS,EAAK,KAAO,EAAK,KAAK,CAChD,GAAM,CAAE,EAAU,GAAG,GAAS,EAE9B,OAAO,MAAM,KAAK,EAAS,CAAC,OAAQ,GAAW,EAAK,MAAO,GAAQ,EAAI,IAAI,EAAO,CAAC,CAAC,EAQ5F,gBACA,CACI,OAAO,KAAK,SAAS,QAAQ,CAMjC,IAAI,aACJ,CACI,OAAO,KAAK,SAAS,KAezB,MAAM,KAAK,EAAiB,EAAe,EAAoB,EAC/D,CACI,IAAM,EAAS,KAAK,SAAS,IAAI,EAAO,EAAI,KAAK,UAAU,EAAO,CAClE,GAAG,CAAC,EACJ,CACI,KAAK,KAAK,KAAK,iBAAkB,EAAQ,aAAa,CACtD,OAGJ,MAAM,EAAO,MAAM,EAAM,EAAS,EAAS,CAY/C,MAAM,QACF,EACA,EACA,EACA,EAEJ,CACI,IAAM,EAAS,KAAK,SAAS,IAAI,EAAO,EAAI,KAAK,UAAU,EAAO,CAMlE,OALI,EAKG,EAAO,SAAY,EAAM,EAAS,EAAS,CAHvC,CAAE,QAAS,GAAO,MAAO,WAAY,EAAQ,aAAc,CAgB1E,OAAO,EAA8B,EACrC,CACI,IAAM,EAAe,OAAO,GAAW,SAAW,KAAK,SAAS,IAAI,EAAO,CAAG,EAiB9E,OAhBI,EAMD,EAAa,OAAO,EAAI,CAEhB,IAIX,EAAa,QAAQ,EAAI,CACzB,KAAK,gBAAgB,KAAK,eAAgB,EAAI,CAAC,IAAI,EAAa,CAEhE,KAAK,KAAK,MAAM,cAAe,EAAK,cAAe,EAAa,KAAM,CAC/D,KAdH,KAAK,KAAK,KAAK,2BAA2B,CACnC,IAsBf,UAAU,EAA8B,EACxC,CACI,IAAM,EAAe,OAAO,GAAW,SAAW,KAAK,SAAS,IAAI,EAAO,CAAG,EAC9E,GAAG,CAAC,EAGA,OADA,KAAK,KAAK,KAAK,8BAA8B,CACtC,GAGX,GAAG,CAAC,EAAa,WAAW,EAAI,CAE5B,MAAO,GAIX,IAAM,EAAS,KAAK,eAAe,IAAI,EAAI,CAW3C,OAVG,IAEC,EAAO,OAAO,EAAa,CACxB,EAAO,OAAS,GAEf,KAAK,eAAe,OAAO,EAAI,EAIvC,KAAK,KAAK,MAAM,gBAAiB,EAAK,gBAAiB,EAAa,KAAM,CACnE,GAWX,UAAU,EACV,CACI,OAAO,KAAK,gBAAgB,IAAI,EAAK,CAWzC,aAAa,EAA8B,EAC3C,CACI,IAAM,EAAe,OAAO,GAAW,SAAW,KAAK,SAAS,IAAI,EAAO,CAAG,EAyB9E,OAxBI,EAOD,KAAK,gBAAgB,IAAI,EAAK,EAE7B,KAAK,KAAK,KAAK,oDAAoD,CAC5D,KAIR,EAAa,MAEZ,KAAK,gBAAgB,OAAO,EAAa,KAAK,CAIlD,EAAa,cAAc,EAAM,KAAK,QAAQ,CAC9C,KAAK,gBAAgB,IAAI,EAAM,EAAa,CAE5C,KAAK,KAAK,MAAM,mBAAoB,EAAa,GAAI,YAAa,EAAK,KAAM,GAAG,CACzE,KAtBH,KAAK,KAAK,KAAK,iCAAiC,CACzC,IA8Bf,eAAe,EACf,CACI,IAAM,EAAe,OAAO,GAAW,SAAW,KAAK,SAAS,IAAI,EAAO,CAAG,EAC9E,GAAG,CAAC,EAGA,OADA,KAAK,KAAK,KAAK,mCAAmC,CAC3C,GAGX,GAAG,CAAC,EAAa,KAEb,MAAO,GAIX,KAAK,gBAAgB,OAAO,EAAa,KAAK,CAG9C,IAAM,EAAW,EAAa,KAAK,KAInC,OAHA,EAAa,iBAAiB,CAE9B,KAAK,KAAK,MAAM,mBAAoB,EAAa,GAAI,cAAe,EAAU,GAAG,CAC1E,GAYX,MAAM,QAAQ,EAAe,EAC7B,CACI,KAAK,KAAK,MAAM,wBAAyB,EAAM,SAAU,EAAO,WAAW,CAE3E,IAAI,IAAI,EAAI,EAAG,EAAI,EAAO,IAC1B,CAEI,IAAM,EAAS,MAAM,KAAK,aAAa,EAAK,CAE5C,MAAM,KAAK,cAAc,EAAO,GAAG,EAQ3C,MAAM,UAAU,EAChB,CACI,IAAM,EAAO,KAAK,OAAO,IAAI,EAAK,CAC9B,KAKJ,MAAK,KAAK,MAAM,sBAAuB,EAAM,KAAM,EAAK,OAAQ,YAAY,CAE5E,IAAI,IAAM,KAAU,EAGhB,MAAM,EAAO,UAAU,CAG3B,EAAK,OAAS,EACd,KAAK,OAAO,OAAO,EAAK,EAW5B,MAAM,WACN,CACI,KAAK,KAAK,KAAK,mCAAoC,KAAK,SAAS,KAAM,WAAW,CAGlF,IAAM,EAAY,CAAE,GAAG,KAAK,SAAS,MAAM,CAAE,CAG7C,IAAI,IAAM,KAAY,EAElB,GACA,CAEI,MAAM,KAAK,cAAc,EAAS,OAE/B,EACP,CACI,KAAK,KAAK,MAAM,2BAA4B,EAAU,IAAK,IAAO,CAK1E,IAAI,IAAM,IAAQ,CAAE,GAAG,KAAK,OAAO,MAAM,CAAE,CAEvC,GACA,CAEI,MAAM,KAAK,UAAU,EAAK,OAEvB,EACP,CACI,KAAK,KAAK,MAAM,4BAA6B,EAAM,KAAM,IAAO,CAKxE,KAAK,kBAAkB,OAAO,CAG9B,KAAK,gBAAgB,OAAO,CAC5B,KAAK,gBAAgB,OAAO,CAC5B,KAAK,eAAe,OAAO,CAC3B,KAAK,gBAAgB,OAAO,CAE5B,KAAK,KAAK,KAAK,uCAAuC,GC/pCxD,GAA6C,CAC/C,MAAO,EAAA,6BAA6B,qBACpC,SAAU,EAAA,6BAA6B,qBACvC,WAAY,EAAA,6BAA6B,qBACzC,aAAc,EAAA,6BAA6B,qBAC3C,KAAM,EAAA,6BAA6B,iBACtC,CAUD,SAAgB,GAA4B,EAAe,EAC3D,CAEI,GAAG,CAAC,EAAM,aAEN,OAID,EAAO,YAEN,QAAQ,KAAK,wFAAwF,CAIzG,IAAM,EAAW,IAAI,EAAA,yBAAyB,eAAgB,GAAM,EAAO,EAAM,QAAQ,CAyGzF,GAtGG,EAAO,QAEN,EAAS,aAAe,GAErB,EAAO,MAAM,SAAW,IAAA,KAEvB,EAAS,YAAc,EAAO,MAAM,QAErC,EAAO,MAAM,YAAc,IAAA,KAE1B,EAAS,eAAiB,EAAO,MAAM,WAExC,EAAO,MAAM,QAAU,IAAA,KAEtB,EAAS,WAAa,EAAO,MAAM,OAEpC,EAAO,MAAM,SAAW,IAAA,KAEvB,EAAS,YAAc,EAAO,MAAM,SAKzC,EAAO,UAEN,EAAS,uBAAyB,GAClC,EAAS,gBAAgB,mBAAqB,GAE3C,EAAO,QAAQ,WAEd,EAAS,gBAAgB,gBACnB,GAAkB,EAAO,QAAQ,WAAa,EAAA,6BAA6B,uBAKtF,EAAO,QAEN,EAAS,aAAe,GAErB,EAAO,MAAM,YAAc,IAAA,KAE1B,EAAS,MAAM,UAAY,EAAO,MAAM,WAEzC,EAAO,MAAM,WAAa,IAAA,KAEzB,EAAS,MAAM,SAAW,EAAO,MAAM,WAK5C,EAAO,WAEN,EAAS,uBAAyB,GAClC,EAAS,gBAAgB,gBAAkB,GAExC,EAAO,SAAS,SAAW,IAAA,KAE1B,EAAS,gBAAgB,eAAiB,EAAO,SAAS,QAE3D,EAAO,SAAS,UAAY,IAAA,KAE3B,EAAS,gBAAgB,gBAAkB,EAAO,SAAS,SAE5D,EAAO,SAAS,QAEf,EAAS,gBAAgB,cAAgB,IAAI,EAAA,OACzC,EAAO,SAAS,MAAM,EACtB,EAAO,SAAS,MAAM,EACtB,EAAO,SAAS,MAAM,EACtB,EACH,GAKN,EAAO,UAEN,EAAS,eAAiB,GAEvB,EAAO,QAAQ,OAAS,IAAA,KAEvB,EAAS,QAAQ,WAAa,EAAO,QAAQ,MAE9C,EAAO,QAAQ,QAAU,IAAA,KAExB,EAAS,QAAQ,YAAc,EAAO,QAAQ,QAKnD,EAAO,sBAEN,EAAS,2BAA6B,GAEnC,EAAO,oBAAoB,SAAW,IAAA,KAErC,EAAS,oBAAoB,iBAAmB,EAAO,oBAAoB,SAKhF,EAAO,KACV,CACI,IAAM,EAAO,IAAI,EAAA,uBAAuB,YAAa,EAAO,CAAE,UAAW,GAAK,UAAW,EAAK,CAAC,CAE5F,EAAO,KAAK,SAAW,IAAA,KAEtB,EAAK,OAAS,EAAO,KAAK,QAE3B,EAAO,KAAK,UAAY,IAAA,KAEvB,EAAK,QAAU,EAAO,KAAK,SAE5B,EAAO,KAAK,gBAAkB,IAAA,KAE7B,EAAK,cAAgB,EAAO,KAAK,iECjI7C,eAAsB,GAA8B,EAAe,EACnE,CACI,IAAM,EAAS,EAAM,aACrB,GAAG,CAAC,EAEA,OAGJ,IAAM,EAAS,EAAM,WAAW,CAC1B,EAAK,IAAI,EAAA,WAAW,EAAM,CAG1B,EAAe,EAAG,eAAe,0BAA0B,gBAAiB,CAC9E,KAAM,CAAE,MAAO,IAAK,OAAQ,IAAK,CACjC,iBAAkB,GAClB,QAAS,CAAE,QAAS,CAAE,EAAA,UAAU,mBAAoB,CAAE,QAAS,EAAG,CACrE,CAAC,CAGI,EAAY,IAAI,EAAA,2BAA2B,aAAc,EAAG,CAClE,EAAU,cAAgB,EAC1B,EAAG,QAAQ,EAAU,CAGrB,IAAM,EAAa,IAAI,EAAA,6BAA6B,cAAe,EAAI,EAAM,CAC7E,EAAW,cAAgB,EAAU,cACrC,EAAW,aAAe,EAAU,mBACpC,EAAW,OAAS,EACpB,EAAW,WAAa,CAAE,OAAQ,EAAM,OAAQ,gBAAiB,EAAM,gBAAiB,CACxF,EAAW,qBAAuB,GAClC,EAAG,QAAQ,EAAW,CAGtB,IAAI,EAA2C,EAAW,cAG1D,GAAG,EAAO,KACV,CACI,IAAM,EAAU,IAAI,EAAA,+BAA+B,gBAAiB,EAAI,EAAM,CAC9E,EAAG,QAAQ,EAAQ,CAEnB,IAAM,EAAW,IAAI,EAAA,qCAAqC,YAAa,EAAI,GAAK,EAAI,CACpF,EAAS,cAAgB,EACzB,EAAS,aAAe,EAAQ,yBAChC,EAAS,cAAgB,EAAQ,0BACjC,EAAS,OAAS,EAClB,EAAG,QAAQ,EAAS,CAEjB,EAAO,KAAK,SAAW,IAAA,KAAa,EAAS,KAAK,OAAS,EAAO,KAAK,QACvE,EAAO,KAAK,UAAY,IAAA,KAAa,EAAS,KAAK,QAAU,EAAO,KAAK,SACzE,EAAO,KAAK,gBAAkB,IAAA,KAAa,EAAS,KAAK,cAAgB,EAAO,KAAK,eAExF,EAAiB,EAAS,cAI9B,GAAG,EAAO,MACV,CACI,IAAM,EAAY,IAAI,EAAA,oBAClB,aACA,EACA,EAAO,MAAM,QAAU,GACvB,EAAO,MAAM,QAAU,IACvB,EAAO,MAAM,WAAa,GAC1B,GACA,EAAO,MAAM,OAAS,GACzB,CACD,EAAU,cAAgB,EAC1B,EAAG,QAAQ,EAAU,CAErB,EAAiB,EAAU,cAI/B,GAAG,EAAO,MACV,CACI,IAAM,EAAY,IAAI,EAAA,oBAAoB,aAAc,EAAG,CAC3D,EAAU,cAAgB,EAC1B,EAAG,QAAQ,EAAU,CAElB,EAAO,MAAM,YAAc,IAAA,KAAa,EAAU,YAAY,UAAY,EAAO,MAAM,WACvF,EAAO,MAAM,WAAa,IAAA,KAAa,EAAU,YAAY,SAAW,EAAO,MAAM,UAExF,EAAiB,EAAU,cAI/B,GAAG,EAAO,QACV,CACI,IAAM,EAAc,IAAI,EAAA,sBAAsB,eAAgB,EAAG,CACjE,EAAY,cAAgB,EAC5B,EAAG,QAAQ,EAAY,CAEpB,EAAO,QAAQ,OAAS,IAAA,KAAa,EAAY,YAAY,WAAa,EAAO,QAAQ,MACzF,EAAO,QAAQ,QAAU,IAAA,KAAa,EAAY,YAAY,YAAc,EAAO,QAAQ,OAE9F,EAAiB,EAAY,cAIjC,GAAG,EAAO,oBACV,CACI,IAAM,EAAY,IAAI,EAAA,kCAAkC,eAAgB,EAAG,CAC3E,EAAU,cAAgB,EAC1B,EAAG,QAAQ,EAAU,CAElB,EAAO,oBAAoB,SAAW,IAAA,KAErC,EAAU,YAAY,iBAAmB,EAAO,oBAAoB,QAGxE,EAAiB,EAAU,cAI/B,GAAG,EAAO,SAAW,EAAO,SAC5B,CACI,IAAM,EAAU,IAAI,EAAA,8BAA8B,eAAgB,EAAG,CAgBrE,GAfA,EAAQ,cAAgB,EACxB,EAAG,QAAQ,EAAQ,CAEhB,EAAO,UAEN,EAAQ,YAAY,mBAAqB,GAEtC,EAAO,QAAQ,WAEd,EAAQ,YAAY,gBACd,EAAkB,EAAO,QAAQ,WAChC,EAAA,6BAA6B,uBAIzC,EAAO,WAEN,EAAQ,YAAY,gBAAkB,GAEnC,EAAO,SAAS,SAAW,IAAA,KAAa,EAAQ,YAAY,eAAiB,EAAO,SAAS,QAC7F,EAAO,SAAS,UAAY,IAAA,KAE3B,EAAQ,YAAY,gBAAkB,EAAO,SAAS,SAEvD,EAAO,SAAS,OACnB,CACI,IAAM,EAAK,EAAO,SAAS,MAC3B,EAAQ,YAAY,cAAc,IAAI,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,EAAE,CAIlE,EAAiB,EAAQ,cAI1B,EAAO,YAEN,QAAQ,KACJ,6KAEH,CAIL,IAAM,EAAW,IAAI,EAAA,oCAAoC,cAAe,EAAG,CAC3E,EAAS,cAAgB,EACzB,EAAG,QAAQ,EAAS,CAGpB,EAAM,uBAAyB,EAC/B,EAAM,WAAa,EAGnB,EAAO,mBAAmB,IAAI,SAC9B,CACI,MAAM,EAAG,YAAY,EACvB,CAGF,MAAM,EAAG,YAAY,kBA3LnB,EAA6C,CAC/C,MAAO,EAAA,6BAA6B,qBACpC,SAAU,EAAA,6BAA6B,qBACvC,WAAY,EAAA,6BAA6B,qBACzC,aAAc,EAAA,6BAA6B,qBAC3C,KAAM,EAAA,6BAA6B,iBACtC,ICxBD,eAAsB,EAAoB,EAAe,EACzD,CACI,GAAG,EAAO,WAAa,aACvB,CACI,GAAM,CAAE,iCAAkC,MAAA,QAAA,SAAA,CAAA,UAAA,IAAA,CAAA,IAAA,CAC1C,MAAM,EAA8B,EAAO,EAAO,MAIlD,GAA4B,EAAO,EAAO,CCelD,IAAsB,EAAtB,KACA,CACI,KACA,KACA,SACA,OAAkC,KAClC,gBAA0D,KAC1D,SAA0C,KAY1C,YAAY,EAAsB,EAClC,CACI,KAAK,KAAO,EAAO,KACnB,KAAK,SAAW,EAChB,KAAK,KAAO,EAAQ,QAAQ,UAAU,SAAU,EAAO,OAAQ,EACxD,IAAI,EAAW,SAAU,EAAO,OAAS,OAAO,CAO3D,IAAI,OACJ,CACI,OAAO,KAAK,OAGhB,IAAI,UACJ,CACI,OAAO,KAAK,SAAW,KAG3B,IAAI,YACJ,CACI,OAAO,KAAK,SAAS,WAGzB,IAAI,kBACJ,CACI,OAAO,KAAK,SAAS,iBAazB,cAAwB,EAAmB,EAC3C,CACI,KAAK,WAAW,SAAS,QAAQ,CAC7B,KAAM,iBACN,QAAS,CACL,UAAW,KAAK,KAChB,WACA,UACH,CACJ,CAAC,CAWN,MAAM,MACN,CACI,GAAG,KAAK,OAGJ,OADA,KAAK,KAAK,KAAK,SAAU,KAAK,KAAM,oBAAoB,CACjD,KAAK,OAGhB,KAAK,KAAK,KAAK,kBAAmB,KAAK,OAAQ,CAC/C,KAAK,KAAK,KAAK,SAAU,KAAK,KAAM,OAAO,CAE3C,GACA,CAmBI,OAlBA,KAAK,cAAc,EAAG,yBAAyB,CAG/C,KAAK,OAAS,MAAM,KAAK,YAAY,CAErC,KAAK,cAAc,IAAK,4BAA4B,CACpD,KAAK,WAAW,SAAS,QAAQ,CAC7B,KAAM,iBACN,QAAS,CACL,UAAW,KAAK,KAChB,QAAS,4BACT,MAAO,KACV,CACJ,CAAC,CAEF,KAAK,KAAK,QAAQ,SAAU,KAAK,KAAM,OAAO,CAC9C,KAAK,KAAK,KAAK,SAAU,KAAK,KAAM,sBAAsB,CAEnD,KAAK,aAET,EACP,CAUI,MATA,KAAK,KAAK,MAAM,wBAAyB,KAAK,KAAM,GAAI,EAAM,CAC9D,KAAK,WAAW,SAAS,QAAQ,CAC7B,KAAM,cACN,QAAS,CACL,UAAW,KAAK,KAChB,QAAS,uBACT,QACH,CACJ,CAAC,CACI,GAmCd,MAAM,UACN,CACI,GAAG,KAAK,OACR,CACI,KAAK,KAAK,KAAK,oBAAqB,KAAK,OAAQ,CAIjD,IAAM,EAAgB,KAAK,SAAS,YAAY,UAAU,cAC1D,GAAG,GAAe,mBAEV,IAAM,KAAU,EAAc,gBAAgB,CAE3C,EAAO,MAAQ,EAAO,KAAK,UAAU,GAAK,KAAK,QAE9C,EAAc,eAAe,EAAO,CAKhD,AAGI,KAAK,YADL,KAAK,SAAS,SAAS,CACP,MAGpB,AAGI,KAAK,mBADL,KAAK,gBAAgB,SAAS,CACP,MAG3B,KAAK,OAAO,SAAS,CACrB,KAAK,OAAS,QClNb,EAAb,KACA,CACI,OACA,QAAkB,IAAI,IAEtB,YAAY,EAAe,EAC3B,CAGI,GAFA,KAAK,OAAS,EAEX,EAEC,IAAI,GAAM,CAAE,EAAM,KAAY,OAAO,QAAQ,EAAQ,CAEjD,KAAK,OAAO,EAAM,EAAO,CASrC,IAAI,EACJ,CACI,OAAO,KAAK,QAAQ,IAAI,EAAK,EAAE,MAGnC,OAAO,EAAe,EACtB,CACI,IAAM,EAAW,KAAK,QAAQ,IAAI,EAAK,CACvC,GAAG,EAEC,OAAO,EAAS,MAGpB,IAAM,EAAQ,IAAI,EAAA,sBAAsB,gBAAiB,IAAS,KAAK,OAAO,CAW9E,OATG,GAAQ,QAEP,EAAM,aAAe,IAAI,EAAA,OAAO,EAAO,MAAM,EAAG,EAAO,MAAM,EAAG,EAAO,MAAM,EAAE,EAGhF,GAAQ,YAAc,IAAA,KAAa,EAAM,iBAAmB,EAAO,WACnE,GAAQ,oBAAsB,IAAA,KAAa,EAAM,kBAAoB,EAAO,mBAE/E,KAAK,QAAQ,IAAI,EAAM,CAAE,QAAO,SAAU,IAAI,IAAO,CAAC,CAC/C,EAGX,gBAAgB,EAAoB,EACpC,CACI,IAAM,EAAQ,KAAK,QAAQ,IAAI,EAAU,CACrC,IAKJ,EAAM,SAAS,IAAI,EAAO,CAC1B,KAAK,kBAAkB,EAAM,EAGjC,kBAAkB,EAAoB,EACtC,CACI,IAAM,EAAQ,KAAK,QAAQ,IAAI,EAAU,CACrC,IAKJ,EAAM,SAAS,OAAO,EAAO,CAC7B,KAAK,kBAAkB,EAAM,EAGjC,qBAAqB,EACrB,CACI,IAAI,IAAM,KAAS,KAAK,QAAQ,QAAQ,CAEjC,EAAM,SAAS,OAAO,EAAO,EAE5B,KAAK,kBAAkB,EAAM,CAKzC,MAAM,EACN,CACI,IAAM,EAAQ,KAAK,QAAQ,IAAI,EAAU,CACrC,IAKJ,EAAM,SAAS,OAAO,CACtB,EAAM,MAAM,gBAAgB,EAGhC,SACA,CACI,IAAI,IAAM,KAAS,KAAK,QAAQ,QAAQ,CAEpC,EAAM,SAAS,OAAO,CACtB,EAAM,MAAM,SAAS,CAGzB,KAAK,QAAQ,OAAO,CAOxB,kBAA0B,EAC1B,CACI,EAAM,MAAM,gBAAgB,CAE5B,IAAI,IAAM,KAAU,EAAM,SAEtB,GAAG,EAAO,KACV,CACI,IAAM,EAAS,EAAO,KAAK,gBAAgB,CACxC,EAAO,OAAS,GAEf,EAAM,MAAM,aAAa,EAAO,ICrEvC,EAAb,cAA+B,CAC/B,CAEI,QAGA,aAA4C,EAAE,CAG9C,aAA4C,EAAE,CAG9C,iBAA4C,EAAE,CAG9C,aAAyB,IAAI,IAG7B,+BAAyC,IAAI,IAY7C,YAAY,EAAsB,EAClC,CACI,MAAM,EAAQ,EAAQ,CACtB,KAAK,QAAU,EAOnB,IAAI,QACJ,CACI,OAAO,KAAK,QAGhB,IAAI,iBACJ,CACI,OAAO,KAAK,iBAUhB,MAAgB,YAChB,CACI,GAAM,CAAE,eAAgB,KAAK,WAAW,QAClC,CAAE,gBAAiB,KAAK,WAAW,SAGtC,KAAK,QAAQ,SAAW,KAAK,QAAQ,QAAQ,OAAS,IAErD,KAAK,cAAc,EAAG,uBAAuB,CAC7C,MAAM,EAAa,QAAQ,KAAK,QAAQ,QAAQ,EAIpD,IAAM,EAAQ,EAAY,aAAa,CAkCvC,GAjCA,KAAK,OAAS,EAGX,KAAK,QAAQ,UAEZ,KAAK,cAAc,EAAG,0BAA0B,CAChD,MAAM,KAAK,eAAe,EAAM,EAIjC,KAAK,QAAQ,QAEZ,KAAK,cAAc,GAAI,wBAAwB,CAC/C,MAAM,KAAK,eAAe,EAAM,EAIjC,KAAK,QAAQ,cAEZ,KAAK,cAAc,GAAI,4BAA4B,CACnD,KAAK,oBAAoB,EAAM,EAInC,KAAK,gBAAgB,EAAM,CAG3B,KAAK,eAAe,EAAM,CAG1B,KAAK,iBAAiB,EAAM,CAGzB,KAAK,QAAQ,eAChB,CACI,IAAM,EAAW,KAAK,QAAQ,eAAe,UAAY,WACzD,KAAK,KAAK,KAAK,iCAAkC,EAAU,WAAW,CACtE,MAAM,EAAoB,EAAO,KAAK,QAAQ,eAAe,CAyBjE,MArBA,MAAK,SAAW,IAAI,EAAe,EAAO,KAAK,QAAQ,SAAS,CAGhE,KAAK,cAAc,GAAI,iCAAiC,CACxD,MAAM,KAAK,uBAAuB,EAAM,CAGxC,KAAK,cAAc,GAAI,uBAAuB,CAC9C,MAAM,KAAK,qBAAqB,CAGhC,KAAK,cAAc,GAAI,0BAA0B,CACjD,MAAM,KAAK,qBAAqB,CAG7B,KAAK,QAAQ,SAEZ,KAAK,cAAc,GAAI,oBAAoB,CAC3C,MAAM,KAAK,qBAAqB,EAG7B,EAMX,MAAc,eAAe,EAC7B,CACI,IAAM,EAAY,KAAK,eAAe,CAClC,KAQJ,CAHA,KAAK,KAAK,MAAM,uBAAwB,IAAa,CAGlD,KAAK,gBAAgB,GAEpB,EAAM,qBAAuB,GAC7B,KAAK,KAAK,MAAM,8CAA8C,EAGlE,GACA,CACI,MAAM,KAAK,WAAW,QAAQ,YAAY,aAAa,EAAE,CAAE,EAAW,EAAM,CAC5E,KAAK,KAAK,MAAM,iCAAiC,OAE9C,EACP,CAEI,MADA,KAAK,KAAK,MAAM,8BAA+B,IAAc,EAAM,CAC7D,IAOd,eACA,CACQ,QAAK,QAAQ,MAKjB,OAAO,OAAO,KAAK,QAAQ,OAAU,SAC/B,KAAK,QAAQ,MACb,KAAK,QAAQ,MAAM,KAM7B,gBACA,CAMI,MALG,CAAC,KAAK,QAAQ,OAAS,OAAO,KAAK,QAAQ,OAAU,SAE7C,GAGJ,KAAK,QAAQ,MAAM,cAAgB,GAM9C,oBAA4B,EAC5B,CACI,IAAM,EAAM,KAAK,QAAQ,YACrB,KAUJ,IAPG,EAAI,MAEH,EAAM,mBAAqB,KAAK,0BAA0B,EAAI,IAAK,EAAI,eAAiB,IAAK,EAAM,CACnG,KAAK,KAAK,MAAM,iBAAkB,EAAI,MAAO,EAI9C,EAAI,OACP,CACI,IAAM,EAAM,KAAK,kBAAkB,EAAI,OAAO,CAE9C,GAAG,IAAQ,QAAU,IAAQ,OAC7B,CAEI,GAAG,CAAC,EAAI,IACR,CACI,IAAM,EAAM,EAAI,eAAiB,IACjC,EAAM,mBAAqB,KAAK,0BAA0B,EAAI,OAAQ,EAAK,EAAM,CAGlF,EAAM,oBAEL,EAAM,oBAAoB,EAAM,mBAAoB,GAAM,EAAI,YAAc,IAAM,EAAG,GAAM,KAInG,CAEI,IAAM,EAAO,EAAI,YAAc,IACzB,EAAS,EAAA,YAAY,aAAa,SAAU,CAAE,SAAU,EAAM,SAAU,GAAI,CAAE,EAAM,CAEpF,EAAM,IAAI,EAAA,iBAAiB,aAAc,EAAM,CACrD,EAAI,gBAAkB,GACtB,EAAI,gBAAkB,GACtB,EAAI,gBAAkB,IAAI,EAAA,QAAQ,EAAI,OAAQ,EAAM,CACpD,EAAO,SAAW,EAClB,EAAO,iBAAmB,GAG9B,KAAK,KAAK,MAAM,oBAAqB,EAAI,SAAU,CAIpD,EAAI,WAAa,IAAA,IAAa,EAAM,qBAElC,EAAM,mBAAsC,UAAY,EAAI,WAOrE,0BAAkC,EAAe,EAAqB,EACtE,CAQI,OAPY,KAAK,kBAAkB,EAAK,GAE7B,OAEA,EAAA,YAAY,0BAA0B,EAAM,EAAM,CAGtD,IAAI,EAAA,eAAe,EAAM,EAAO,EAAW,CAGtD,kBAA0B,EAC1B,CACI,IAAM,EAAW,EAAK,YAAY,IAAI,CACtC,OAAO,GAAY,EAAI,EAAK,MAAM,EAAS,CAAC,aAAa,CAAG,GAShE,gBAAwB,EACxB,CACI,IAAM,EAAgB,KAAK,QAAQ,QAC7B,EAAS,KAAK,WAAW,OAE/B,GAAG,CAAC,EACJ,CAEO,EAAM,QAAQ,OAAS,IAEtB,EAAM,aAAe,EAAM,QAAQ,GAEhC,GAEC,EAAM,aAAa,cAAc,EAAQ,GAAK,CAGlD,KAAK,KAAK,MAAM,+BAAgC,EAAM,aAAa,KAAM,GAAG,EAGhF,OAGJ,IAAI,EAA+B,KAEnC,IAAI,GAAM,CAAE,EAAM,KAAY,OAAO,QAAQ,EAAc,CAC3D,CACI,IAAM,EAAW,EAAM,gBAAgB,EAAK,CAE5C,GAAG,EAEC,KAAK,mBAAmB,EAAU,EAAO,CACzC,KAAK,KAAK,MAAM,iCAAkC,EAAM,GAAG,EAExD,CAAC,GAAgB,EAAO,UAEvB,EAAe,WAGf,EAAO,KACf,CACI,IAAM,EAAS,KAAK,cAAc,EAAM,EAAQ,EAAM,CACtD,KAAK,KAAK,MAAM,WAAY,EAAO,KAAM,YAAa,EAAM,GAAG,EAE5D,CAAC,GAAgB,EAAO,UAEvB,EAAe,QAKnB,KAAK,KAAK,KACN,WAAY,EAAM,uDACrB,CAIT,GAAG,EACH,CACI,EAAM,aAAe,EAErB,IAAM,EAAY,EAAc,EAAa,MAC1C,GAAU,GAAW,gBAAkB,IAEtC,EAAa,cAAc,EAAQ,GAAK,EAQpD,cAAsB,EAAe,EAA2B,EAChE,CACI,IAAM,EAAM,EAAO,SAAW,KAAK,WAAW,EAAO,SAAS,CAAG,EAAA,QAAQ,MAAM,CAE3E,EAEJ,OAAQ,EAAO,KAAf,CAEI,IAAK,YACL,CACI,IAAM,EAAS,EAAO,OAAS,KAAK,WAAW,EAAO,OAAO,CAAG,EAAA,QAAQ,MAAM,CAE9E,EAAS,IAAI,EAAA,gBACT,EACA,EAAO,OAAS,KAAK,GAAK,EAC1B,EAAO,MAAQ,KAAK,GAAK,EACzB,EAAO,QAAU,GACjB,EACA,EACH,CACD,MAGJ,IAAK,YAED,EAAS,IAAI,EAAA,gBAAgB,EAAM,EAAK,EAAM,CAC9C,MAGJ,IAAK,aACL,CACI,GAAG,CAAC,EAAO,aAGP,OADA,KAAK,KAAK,KAAK,sBAAuB,EAAM,qCAAqC,CAC1E,IAAI,EAAA,WAAW,EAAM,EAAA,QAAQ,MAAM,CAAE,EAAM,CAGtD,IAAM,EAAM,IAAI,EAAA,iBAAiB,EAAM,EAAO,CAC1C,aAAc,EAAO,aACxB,CAAC,CAEC,EAAO,SAAU,EAAI,OAAS,KAAK,WAAW,EAAO,OAAO,EAC5D,EAAO,MAAQ,IAAA,KAAa,EAAI,IAAM,EAAO,KAC7C,EAAO,QAAU,IAAA,KAAa,EAAI,MAAQ,EAAO,OACjD,EAAO,SAAW,IAAA,KAAa,EAAI,OAAS,EAAO,QACnD,EAAO,kBAAoB,IAAA,KAAa,EAAI,gBAAkB,EAAO,iBAErE,EAAO,YAAc,IAAA,KAAa,EAAI,OAAO,UAAY,EAAO,WAChE,EAAO,YAAc,IAAA,KAAa,EAAI,OAAO,UAAY,EAAO,WAChE,EAAO,WAAa,IAAA,KAAa,EAAI,OAAO,SAAW,EAAO,UAC9D,EAAO,WAAa,IAAA,KAAa,EAAI,OAAO,SAAW,EAAO,UAC9D,EAAO,SAAW,IAAA,KAAa,EAAI,OAAO,OAAS,EAAO,QAC1D,EAAO,SAAW,IAAA,KAAa,EAAI,OAAO,OAAS,EAAO,QAE7D,EAAS,EACT,MAIJ,QAEI,EAAS,IAAI,EAAA,WAAW,EAAM,EAAK,EAAM,CACzC,MAKR,OADA,KAAK,mBAAmB,EAAQ,EAAO,CAChC,EAMX,mBAA2B,EAAiB,EAC5C,CACO,EAAO,MAAQ,IAAA,KAAa,EAAO,IAAM,EAAO,KAChD,EAAO,WAAY,EAAO,SAAW,KAAK,WAAW,EAAO,SAAS,EACrE,EAAO,OAAS,IAAA,KAAa,EAAO,KAAO,EAAO,MAClD,EAAO,OAAS,IAAA,KAAa,EAAO,KAAO,EAAO,MAElD,aAAkB,EAAA,aAEd,EAAO,QAAU,IAAA,KAAa,EAAO,MAAQ,EAAO,OACpD,EAAO,WAAY,EAAO,SAAW,KAAK,WAAW,EAAO,SAAS,GAGzE,aAAkB,EAAA,kBAEd,EAAO,QAAU,EAAO,UAAU,KAAK,WAAW,EAAO,OAAO,CAAC,CACjE,EAAO,mBAAqB,IAAA,KAAa,EAAO,iBAAmB,EAAO,kBAC1E,EAAO,mBAAqB,IAAA,KAAa,EAAO,iBAAmB,EAAO,kBAC1E,EAAO,iBAAmB,IAAA,KAAa,EAAO,eAAiB,EAAO,gBACtE,EAAO,iBAAmB,IAAA,KAAa,EAAO,eAAiB,EAAO,gBACtE,EAAO,iBAAmB,IAAA,KAAa,EAAO,eAAiB,EAAO,iBAG1E,aAAkB,EAAA,mBAEd,EAAO,MAAQ,IAAA,KAAa,EAAO,IAAM,EAAO,KAChD,EAAO,QAAU,IAAA,KAAa,EAAO,MAAQ,EAAO,OACpD,EAAO,SAAW,IAAA,KAAa,EAAO,OAAS,EAAO,QACtD,EAAO,SAAU,EAAO,OAAS,KAAK,WAAW,EAAO,OAAO,EAC/D,EAAO,kBAAoB,IAAA,KAAa,EAAO,gBAAkB,EAAO,iBACxE,EAAO,YAAc,IAAA,KAAa,EAAO,OAAO,UAAY,EAAO,WACnE,EAAO,YAAc,IAAA,KAAa,EAAO,OAAO,UAAY,EAAO,WACnE,EAAO,WAAa,IAAA,KAAa,EAAO,OAAO,SAAW,EAAO,UACjE,EAAO,WAAa,IAAA,KAAa,EAAO,OAAO,SAAW,EAAO,WAU5E,eAAuB,EACvB,CACI,IAAM,EAAe,KAAK,QAAQ,OAC9B,KAEJ,IAAI,GAAM,CAAE,EAAM,KAAY,OAAO,QAAQ,EAAa,CAC1D,CACI,IAAM,EAAW,EAAM,eAAe,EAAK,CAExC,GAEC,KAAK,kBAAkB,EAAU,EAAO,CACxC,KAAK,KAAK,MAAM,gCAAiC,EAAM,GAAG,EAEtD,EAAO,MAEX,KAAK,aAAa,EAAM,EAAQ,EAAM,CACtC,KAAK,KAAK,MAAM,WAAY,EAAO,KAAM,WAAY,EAAM,GAAG,EAI9D,KAAK,KAAK,KACN,UAAW,EAAM,uDACpB,EAQb,aAAqB,EAAe,EAA0B,EAC9D,CACI,IAAM,EAAM,EAAO,UAAY,KAAK,WAAW,EAAO,UAAU,CAAG,IAAI,EAAA,QAAQ,EAAG,GAAI,EAAE,CAClF,EAAM,EAAO,SAAW,KAAK,WAAW,EAAO,SAAS,CAAG,EAAA,QAAQ,MAAM,CAE/E,OAAQ,EAAO,KAAf,CAEI,IAAK,cACL,CACI,IAAM,EAAQ,IAAI,EAAA,iBAAiB,EAAM,EAAK,EAAM,CAEjD,EAAO,cAEN,EAAM,YAAc,KAAK,UAAU,EAAO,YAAY,EAG1D,KAAK,kBAAkB,EAAO,EAAO,CACrC,MAGJ,IAAK,cACL,CACI,IAAM,EAAQ,IAAI,EAAA,iBAAiB,EAAM,EAAK,EAAM,CACpD,KAAK,kBAAkB,EAAO,EAAO,CACrC,MAGJ,IAAK,QACL,CACI,IAAM,EAAQ,IAAI,EAAA,WAAW,EAAM,EAAK,EAAM,CAC9C,KAAK,kBAAkB,EAAO,EAAO,CACrC,MAGJ,IAAK,OACL,CACI,IAAM,EAAQ,IAAI,EAAA,UACd,EACA,EACA,EACA,EAAO,OAAS,KAAK,GAAK,EAC1B,EAAO,UAAY,EACnB,EACH,CACD,KAAK,kBAAkB,EAAO,EAAO,CACrC,MAGJ,IAAK,WACL,CACI,IAAM,EAAQ,IAAI,EAAA,cACd,EACA,EACA,EAAO,OAAS,EAChB,EAAO,QAAU,EACjB,EACH,CAKD,GAJA,KAAK,kBAAkB,EAAO,EAAO,CAIlC,EAAO,UACV,CACI,IAAM,EAAQ,IAAI,EAAA,cAAc,GAAI,EAAM,QAAS,EAAM,CACzD,EAAM,SAAW,EAAM,SAAS,OAAO,CACvC,EAAM,SAAW,EAAA,QAAQ,MAAM,CAC/B,EAAM,OAAS,EAGf,EAAM,OAAO,EAAM,SAAS,SAAS,EAAI,CAAC,CAG9C,QAQZ,kBACI,EACA,EAEJ,CACO,EAAO,YAAc,IAAA,KAAa,EAAM,UAAY,EAAO,WAC3D,EAAO,UAAW,EAAM,QAAU,KAAK,UAAU,EAAO,QAAQ,EAChE,EAAO,WAAY,EAAM,SAAW,KAAK,UAAU,EAAO,SAAS,EAO1E,iBAAyB,EACzB,CACI,IAAM,EAAgB,KAAK,QAAQ,WACnC,GAAG,CAAC,GAAe,QAEf,OAIJ,IAAM,EAAiB,EAAM,OAAO,OAC/B,GAAU,EAAA,wBAAwB,iBAAiB,EAAM,CAC7D,CAED,GAAG,EAAe,SAAW,EAC7B,CACI,KAAK,KAAK,MAAM,kDAAkD,CAClE,OAIJ,IAAI,IAAM,KAAS,EAEf,EAAM,YAAY,EAAM,CAI5B,IAAM,EAAY,IAAI,EAAA,wBAAwB,iBAAkB,EAAgB,EAAM,CAGnF,EAAc,kBAAoB,IAAA,KAAa,EAAU,gBAAkB,EAAc,iBACzF,EAAc,gBAAkB,IAAA,KAAa,EAAU,cAAgB,EAAc,eACrF,EAAc,cAAgB,IAAA,KAAa,EAAU,YAAc,EAAc,aACjF,EAAc,WAAa,IAAA,KAAa,EAAU,SAAW,EAAc,UAE9E,KAAK,gBAAkB,EAEvB,KAAK,KAAK,MACN,+BAAgC,EAAe,OAAQ,WACjD,EAAU,gBAAiB,GAAI,EAAU,cAAe,UACxD,EAAU,YAAa,eAChC,CAGD,IAAI,IAAM,KAAY,EAAM,UAErB,aAAoB,EAAA,cAEnB,EAAS,oBAAsB,IAO3C,WAAmB,EACnB,CACI,OAAO,IAAI,EAAA,QAAQ,EAAI,EAAG,EAAI,EAAG,EAAI,EAAE,CAG3C,UAAkB,EAClB,CACI,OAAO,IAAI,EAAA,OAAO,EAAM,EAAG,EAAM,EAAG,EAAM,EAAE,CAMhD,MAAc,eAAe,EAC7B,CACI,IAAM,EAAgB,KAAK,QAAQ,QAG/B,EAAU,IAAI,EAAA,QAAQ,EAAG,MAAO,EAAE,CAEnC,OAAO,GAAkB,UAAY,EAAc,UAElD,EAAU,IAAI,EAAA,QACV,EAAc,QAAQ,EACtB,EAAc,QAAQ,EACtB,EAAc,QAAQ,EACzB,EAIL,IAAM,EAAuB,KAAK,WAAW,oBACvC,IACA,IAAA,GAEN,KAAK,KAAK,MAAM,kCAAmC,EAAQ,UAAU,GAAI,CACzE,MAAM,KAAK,WAAW,QAAQ,YAAY,cAAc,EAAO,EAAS,EAAqB,CAMjG,MAAc,uBAAuB,EACrC,CACI,IAAM,EAAQ,EAAM,eAAe,OAAO,EAAM,OAA0B,CAE1E,IAAI,IAAM,KAAQ,EAClB,CAEI,IAAM,EAAW,KAAK,uBAAuB,EAAK,CAE/C,GAAY,OAAO,KAAK,EAAS,CAAC,OAAS,IAG1C,EAAK,SAAW,CAAE,GAAG,EAAK,SAAU,GAAG,EAAU,CAGjD,MAAM,KAAK,qBAAqB,EAAK,EAI7C,KAAK,KAAK,MACN,aAAc,EAAM,OAAQ,gBAAiB,KAAK,aAAa,OAAQ,oBAC7D,KAAK,aAAa,OAAQ,eACvC,CASL,uBAA+B,EAC/B,CACI,GAAG,CAAC,EAAK,SAEL,OAAO,KAIX,IAAM,EAAa,EAAK,UAAU,MAAM,OASxC,OAPG,EAGQ,CAAE,GAAG,EAAY,CAIrB,EAAK,SAMhB,MAAc,qBAAqB,EACnC,CAEO,UAAW,EAAK,UAEf,KAAK,aAAa,KAAK,CACnB,KAAM,EAAK,SAAS,MACpB,SAAU,EAAK,SAAS,OAAO,CAC/B,SAAU,EAAK,SAAS,OAAO,CAC/B,QAAS,EAAK,QAAQ,OAAO,CAC7B,OACH,CAAC,CAIH,WAAY,EAAK,UAEhB,KAAK,aAAa,KAAK,CACnB,KAAM,EAAK,SAAS,OACpB,OACH,CAAC,CAIN,IAAI,GAAM,CAAE,EAAU,KAAa,KAAK,iBAEpC,GAAG,KAAY,EAAK,SAEhB,GACA,CAEI,MAAM,EAAQ,EAAM,EAAK,SAAS,GAAW,KAAM,KAAK,WAAW,OAEhE,EACP,CACI,KAAK,KAAK,MACN,8BAA+B,EAAU,cAAe,EAAK,KAAM,IACnE,EACH,EASjB,MAAc,qBACd,CACI,IAAI,IAAM,KAAc,KAAK,aAC7B,CACI,IAAM,EAAW,KAAK,QAAQ,SAAS,EAAW,MAE9C,EAOA,MAAM,KAAK,mBAAmB,EAAY,EAAS,CALnD,KAAK,KAAK,KAAK,kCAAmC,EAAW,KAAM,aAAa,EAa5F,MAAc,mBAAmB,EAA6B,EAC9D,CACI,GACA,CACI,IAAM,EAAS,MAAM,KAAK,aAAa,EAAU,EAAW,CAC5D,KAAK,iBAAiB,KAAK,EAAO,CAGlC,EAAW,KAAK,SAAS,CAEzB,KAAK,KAAK,MAAM,mBAAoB,EAAS,OAAQ,oBAAqB,EAAW,KAAM,GAAG,OAE3F,EACP,CACI,KAAK,KAAK,MAAM,0CAA2C,EAAW,KAAM,IAAK,EAAM,EAO/F,MAAc,aAAa,EAA4B,EACvD,CACI,IAAM,EAAe,CACjB,GAAG,EAAS,OACZ,SAAU,KAAK,gBAAgB,EAAW,SAAS,CACnD,SAAU,KAAK,gBAAgB,EAAW,SAAS,CACnD,QAAS,KAAK,gBAAgB,EAAW,QAAQ,CACpD,CAEK,EAAS,MAAM,KAAK,WAAW,SAAS,cAAc,aAAa,EAAS,OAAQ,CACtF,KAAM,EAAS,KACf,KAAM,EAAS,KACf,eACH,CAAC,CAKF,OAFA,MAAM,KAAK,kBAAkB,EAAQ,EAAW,CAEzC,EAMX,gBAAwB,EACxB,CACI,MAAO,CAAE,EAAG,EAAO,EAAG,EAAG,EAAO,EAAG,EAAG,EAAO,EAAG,CAOpD,MAAc,kBAAkB,EAAqB,EACrD,CACI,GAAG,CAAC,KAAK,OAEL,OAIJ,IAAM,EAAa,KAAK,WAAW,SAAS,cAAc,cAAc,EAAO,KAAK,CACpF,GAAG,CAAC,GAAY,KAEZ,OAGJ,IAAM,EAAa,EAAW,KACxB,EAAQ,KAAK,OAGb,EAAO,IAAI,EAAA,cAAc,UAAW,EAAO,KAAO,EAAM,CAC9D,EAAK,SAAS,SAAS,EAAW,SAAS,CAC3C,EAAK,SAAS,SAAS,EAAW,SAAS,CAG3C,IAAI,EAEJ,OAAQ,EAAW,OAAnB,CAEI,IAAK,MACD,EAAS,CAAE,EAAA,YAAY,UAAU,GAAI,EAAO,KAAM,OAAQ,CACtD,KAAM,EAAW,QAAQ,MAAQ,EACjC,MAAO,EAAW,QAAQ,MAC1B,OAAQ,EAAW,QAAQ,OAC3B,MAAO,EAAW,QAAQ,MAC7B,CAAE,EAAM,CAAE,CACX,MAEJ,IAAK,SACD,EAAS,CAAE,EAAA,YAAY,aAAa,GAAI,EAAO,KAAM,OAAQ,CACzD,SAAU,EAAW,QAAQ,UAAY,EACzC,SAAU,EAAW,QAAQ,UAAY,GAC5C,CAAE,EAAM,CAAE,CACX,MAEJ,IAAK,UACD,EAAS,CAAE,EAAA,YAAY,cAAc,GAAI,EAAO,KAAM,OAAQ,CAC1D,OAAQ,EAAW,QAAQ,QAAU,IACrC,OAAQ,EAAW,QAAQ,QAAU,GACxC,CAAE,EAAM,CAAE,CACX,MAEJ,IAAK,WACD,EAAS,CAAE,EAAA,YAAY,eAAe,GAAI,EAAO,KAAM,OAAQ,CAC3D,OAAQ,EAAW,QAAQ,QAAU,EACrC,SAAU,EAAW,QAAQ,UAAY,EAC5C,CAAE,EAAM,CAAE,CACX,MAEJ,QACA,CAEI,IAAM,EAAS,MAAM,KAAK,WAAW,QAAQ,YAAY,aAAa,EAAE,CAAE,EAAW,OAAQ,EAAM,CAEnG,GAAG,EAAO,OAAO,SAAW,EAC5B,CACI,KAAK,KAAK,KAAK,0BAA2B,EAAW,SAAU,CAC/D,EAAK,SAAS,CACd,OAGJ,EAAS,EAAO,OAIhB,IAAI,IAAM,KAAM,EAAO,eAEnB,AAEI,EAAG,SAAS,EAIpB,OAMR,IAAI,IAAM,KAAQ,EAEd,AAEI,EAAK,SAAS,EAKnB,EAAW,QAEP,OAAO,EAAW,OAAU,SAE3B,EAAK,QAAQ,OAAO,EAAW,MAAM,CAIrC,EAAK,QAAQ,IAAI,EAAW,MAAM,EAAG,EAAW,MAAM,EAAG,EAAW,MAAM,EAAE,EAKpF,IAAM,EAAY,EAAW,SAC7B,GAAG,GAAW,MACd,CACI,IAAM,EAAQ,EAAU,MAExB,GAAG,EAAU,OAAS,MACtB,CACI,IAAM,EAAW,IAAI,EAAA,YAAY,GAAI,EAAO,KAAM,WAAY,EAAM,CACpE,EAAS,YAAc,KAAK,UAAU,EAAM,CAEzC,EAAU,WAET,EAAS,cAAgB,KAAK,UAAU,EAAU,SAAS,EAG/D,EAAS,SAAW,EAAU,UAAY,EAC1C,EAAS,UAAY,EAAU,WAAa,EAE5C,IAAI,IAAM,KAAQ,EAEd,EAAK,SAAW,MAIxB,CACI,IAAM,EAAW,IAAI,EAAA,iBAAiB,GAAI,EAAO,KAAM,WAAY,EAAM,CACzE,EAAS,aAAe,KAAK,UAAU,EAAM,CAE1C,EAAU,WAET,EAAS,cAAgB,KAAK,UAAU,EAAU,SAAS,EAG/D,IAAI,IAAM,KAAQ,EAEd,EAAK,SAAW,GAM5B,KAAK,WAAW,SAAS,cAAc,aAAa,EAAQ,EAAK,CAMrE,MAAc,qBACd,CACI,IAAI,IAAM,KAAc,KAAK,aAGzB,MAAM,KAAK,mBAAmB,EAAW,CAOjD,MAAc,mBAAmB,EACjC,CACI,IAAM,EAAY,KAAK,QAAQ,WAAW,EAAW,MAErD,GACA,CACI,IAAM,EAAe,CACjB,GAAG,GAAW,OACd,SAAU,KAAK,gBAAgB,EAAW,KAAK,SAAS,CAC3D,CAEK,EAAS,MAAM,KAAK,WAAW,SAAS,cAAc,aAAa,EAAW,KAAM,CACtF,KAAM,GAAW,MAAQ,EAAW,KAAK,KACzC,KAAM,GAAW,KACjB,eACA,KAAM,EAAW,KACpB,CAAC,CACF,KAAK,iBAAiB,KAAK,EAAO,CAElC,KAAK,KAAK,MAAM,mBAAoB,EAAW,KAAM,cAAe,EAAW,KAAK,KAAM,GAAG,OAE1F,EACP,CACI,KAAK,KAAK,MAAM,qCAAsC,EAAW,KAAK,KAAM,IAAK,EAAM,EAY/F,MAAc,qBACd,CACI,IAAM,EAAe,KAAK,QAAQ,OAClC,GAAG,CAAC,EAAgB,OAEpB,GAAM,CAAE,gBAAiB,KAAK,WAAW,SACzC,GAAG,CAAC,EACJ,CACI,KAAK,KAAK,KAAK,gEAAgE,CAC/E,OAGJ,IAAI,GAAM,CAAE,EAAM,KAAY,OAAO,QAAQ,EAAa,CAEtD,GACA,CAEI,IAAM,EAAQ,MAAM,EAAa,YAAY,EAAM,EAAO,IAAK,EAAO,QAAS,CAC3E,KAAM,EAAO,MAAQ,GACrB,OAAQ,EAAO,QAAU,EAC5B,CAAC,CAEF,KAAK,aAAa,IAAI,EAAM,EAAM,CAE/B,EAAO,UAEN,EAAM,MAAM,CAGhB,KAAK,KAAK,MAAM,yBAA0B,EAAM,GAAG,OAEhD,EACP,CACI,KAAK,KAAK,MAAM,iCAAkC,EAAM,IAAK,EAAM,EAa/E,MAAM,cACN,CACI,IAAI,GAAM,CAAE,EAAM,KAAW,KAAK,aAE3B,EAAM,QAAU,EAAA,WAAW,UAE1B,KAAK,+BAA+B,IAAI,EAAK,CAC7C,EAAM,OAAO,EASzB,MAAM,YACN,CACI,IAAI,IAAM,KAAQ,KAAK,+BACvB,CACI,IAAM,EAAQ,KAAK,aAAa,IAAI,EAAK,CACtC,GAEC,EAAM,MAAM,CAIpB,KAAK,+BAA+B,OAAO,CAU/C,MAAe,UACf,CAEI,GAAM,CAAE,iBAAkB,KAAK,WAAW,SACpC,EAAkB,KAAK,iBAAiB,IAAI,KAAO,IACzD,CACI,GACA,CACI,MAAM,EAAc,cAAc,EAAO,GAAG,OAEzC,EACP,CACI,KAAK,KAAK,MAAM,4BAA6B,EAAO,GAAI,GAAI,EAAM,GAExE,CAEF,MAAM,QAAQ,IAAI,EAAgB,CAElC,KAAK,iBAAmB,EAAE,CAC1B,KAAK,aAAe,EAAE,CACtB,KAAK,aAAe,EAAE,CAGtB,IAAI,IAAM,KAAS,KAAK,aAAa,QAAQ,CAEzC,EAAM,SAAS,CAEnB,KAAK,aAAa,OAAO,CACzB,KAAK,+BAA+B,OAAO,CAE3C,MAAM,MAAM,UAAU,GCzqCjB,GAAb,KACA,CACI,UACA,YAA0C,KAC1C,KACA,QAGA,cAAwB,IAAI,IAG5B,cAAwB,IAAI,IAG5B,cAAwB,IAAI,IAG5B,kBAA4B,IAAI,IAGhC,cAAuC,KAGvC,oBAA+C,EAAE,CAOjD,IAAW,cACX,CACI,OAAO,KAAK,cAMhB,IAAW,kBACX,CACI,OAAO,KAAK,kBAKhB,YACI,EACA,EAEJ,CACI,KAAK,UAAY,EACjB,KAAK,QAAU,EACf,KAAK,KAAO,GAAQ,UAAU,eAAe,EAAI,IAAI,EAAW,eAAe,CAG/E,KAAK,oBAAoB,KACrB,KAAK,UAAU,UAAU,iBAAmB,GAC5C,CACI,KAAK,gBAAgB,EAAM,QAAQ,EACrC,CACL,CAED,KAAK,oBAAoB,KACrB,KAAK,UAAU,UAAU,iBAAmB,GAC5C,CACI,KAAK,gBAAgB,EAAM,QAAQ,EACrC,CACL,CAED,KAAK,oBAAoB,KACrB,KAAK,UAAU,UAAU,cAAgB,GACzC,CACI,KAAK,aAAa,EAAM,QAAQ,EAClC,CACL,CAED,KAAK,KAAK,KAAK,2BAA2B,CAW9C,eAAe,EACf,CACI,KAAK,YAAc,EAOvB,gBAAwB,EACxB,CACI,KAAK,KAAK,MAAM,SAAU,EAAQ,UAAW,aAAc,EAAQ,SAAU,GAAG,CAGpF,gBAAwB,EACxB,CACI,KAAK,KAAK,KAAK,SAAU,EAAQ,UAAW,sBAAsB,CAGtE,aAAqB,EACrB,CACI,KAAK,KAAK,MAAM,SAAU,EAAQ,UAAW,UAAW,EAAQ,UAAW,CAM/E,gBACA,CACI,GAAG,CAAC,KAAK,YAEL,MAAU,MAAM,iEAAiE,CAGrF,MAAO,CACH,WAAY,KAAK,YACjB,iBAAkB,KAAK,kBACvB,OAAQ,KAAK,QAChB,CAML,qBAA6B,EAC7B,CACI,IAAM,EAAU,KAAK,gBAAgB,CAErC,GAAG,EAAO,MACV,CAEI,IAAM,EAAa,KAAK,cAAc,IAAI,EAAO,MAAM,CACvD,GAAG,CAAC,EAEA,MAAU,MAAM,gBAAiB,EAAO,MAAO,sBAAsB,CAIzE,OADA,KAAK,KAAK,MAAM,mBAAoB,EAAO,KAAM,iBAAkB,EAAO,MAAO,GAAG,CAC7E,IAAI,EAAW,EAAQ,EAAQ,CAK1C,OADA,KAAK,KAAK,MAAM,mBAAoB,EAAO,KAAM,mBAAmB,CAC7D,IAAI,EAAU,EAAQ,EAAQ,CAazC,oBAA2B,EAC3B,CACO,KAAK,cAAc,IAAI,EAAO,KAAK,EAElC,KAAK,KAAK,KAAK,iBAAkB,EAAO,KAAM,uCAAuC,CAGzF,KAAK,cAAc,IAAI,EAAO,KAAM,EAAO,CAC3C,KAAK,KAAK,KAAK,4BAA6B,EAAO,OAAQ,CAU/D,mBAA0B,EAAe,EACzC,CACO,KAAK,cAAc,IAAI,EAAK,EAE3B,KAAK,KAAK,KAAK,gBAAiB,EAAM,uCAAuC,CAGjF,KAAK,cAAc,IAAI,EAAM,EAAW,CACxC,KAAK,KAAK,KAAK,2BAA4B,IAAQ,CAcvD,wBAA+B,EAAmB,EAClD,CACO,KAAK,kBAAkB,IAAI,EAAS,EAEnC,KAAK,KAAK,KAAK,qBAAsB,EAAU,uCAAuC,CAG1F,KAAK,kBAAkB,IAAI,EAAU,EAAQ,CAC7C,KAAK,KAAK,MAAM,gCAAiC,IAAY,CAQjE,0BAAiC,EACjC,CACI,KAAK,kBAAkB,OAAO,EAAS,CAO3C,0BACA,CACI,KAAK,kBAAkB,OAAO,CAalC,SAAgB,EAChB,CACI,OAAO,KAAK,cAAc,IAAI,EAAK,EAAI,KAQ3C,eAAsB,EACtB,CACI,OAAO,KAAK,cAAc,IAAI,EAAK,EAAI,KAwB3C,MAAa,UAAU,EACvB,CAEI,IAAI,EACJ,GAAG,OAAO,GAAiB,SAC3B,CACI,IAAM,EAAa,KAAK,cAAc,IAAI,EAAa,CACvD,GAAG,CAAC,EAEA,MAAU,MAAM,iBAAkB,EAAc,sBAAsB,CAE1E,EAAS,OAIT,EAAS,EAET,KAAK,oBAAoB,EAAO,CAIpC,IAAM,EAAW,KAAK,cAAc,IAAI,EAAO,KAAK,CACpD,GAAG,EAGC,OADA,KAAK,KAAK,KAAK,UAAW,EAAO,KAAM,qBAAqB,CACrD,EAIX,KAAK,KAAK,MAAM,kBAAmB,EAAO,OAAQ,CAClD,IAAM,EAAQ,KAAK,qBAAqB,EAAO,CAM/C,OALA,MAAM,EAAM,MAAM,CAGlB,KAAK,cAAc,IAAI,EAAO,KAAM,EAAM,CAEnC,EAuBX,MAAa,cAAc,EAC3B,CACI,IAAM,EACA,MAAM,KAAK,UAAU,EAAa,CAIxC,MAFA,MAAK,cAAgB,EACrB,KAAK,KAAK,KAAK,oBAAqB,EAAM,OAAQ,CAC3C,EASX,MAAa,YAAY,EACzB,CACI,IAAM,EAAQ,KAAK,cAAc,IAAI,EAAK,CACvC,IAGI,KAAK,gBAAkB,IAEtB,KAAK,cAAgB,MAIzB,MAAM,EAAM,UAAU,CAGtB,KAAK,cAAc,OAAO,EAAK,CAE/B,KAAK,KAAK,KAAK,mBAAoB,IAAQ,EAOnD,MAAa,oBACb,CACO,KAAK,cAEJ,MAAM,KAAK,YAAY,KAAK,cAAc,KAAK,CAI/C,KAAK,KAAK,KAAK,6BAA6B,CAiBpD,MAAa,WAAW,EAAoB,EAC5C,CACI,IAAM,EAAW,KAAK,cAEtB,GACA,CAEI,KAAK,UAAU,QAAQ,CACnB,KAAM,yBACN,QAAS,CACL,KAAM,GAAU,KAChB,GAAI,EACP,CACJ,CAAC,CAGC,GAAU,cAET,MAAM,EAAS,cAAc,CAIjC,KAAK,UAAU,QAAQ,CACnB,KAAM,4BACN,QAAS,CACL,MAAO,UACP,YACH,CACJ,CAAC,CAGF,IAAM,EAAW,MAAM,KAAK,UAAU,EAAU,CAGhD,GAAG,GAAS,YAER,OAID,GAAY,CAAC,GAAS,WAErB,MAAM,KAAK,YAAY,EAAS,KAAK,CAIzC,KAAK,cAAgB,EAGlB,EAAS,YAER,MAAM,EAAS,YAAY,CAI/B,KAAK,UAAU,QAAQ,CACnB,KAAM,4BACN,QAAS,CAAE,YAAW,CACzB,CAAC,OAEC,EACP,CAWI,MATA,KAAK,UAAU,QAAQ,CACnB,KAAM,yBACN,QAAS,CACL,KAAM,GAAU,KAChB,GAAI,EACJ,QACH,CACJ,CAAC,CAEI,GAWd,MAAM,WACN,CAEI,IAAI,IAAM,KAAe,KAAK,oBAE1B,GAAa,CAEjB,KAAK,oBAAsB,EAAE,CAG7B,IAAI,IAAM,KAAS,KAAK,cAAc,QAAQ,CAG1C,MAAM,EAAM,UAAU,CAG1B,KAAK,cAAc,OAAO,CAC1B,KAAK,cAAc,OAAO,CAC1B,KAAK,cAAgB,OClhBhB,EAAb,KACA,CACI,aACA,UAAoB,IAAI,IACxB,aAAuB,GACvB,wBAAkC,EAClC,KAEA,YAAY,EAA2B,EACvC,CACI,KAAK,aAAe,EACpB,KAAK,KAAO,EAAO,UAAU,eAAe,CAOhD,MAAM,WAAW,EACjB,CACI,KAAK,KAAK,KAAK,gCAAiC,EAAS,KAAK,KAAK,GAAI,CAEvE,IAAI,IAAM,KAAQ,EAClB,CAEI,IAAM,EAAM,MAAM,KAAK,aAAa,UAAU,EAAK,CACnD,KAAK,UAAU,IAAI,EAAM,CAAE,MAAK,OAAQ,EAAG,MAAO,GAAO,CAAC,CAG9D,KAAK,KAAK,KAAK,8BAA8B,CAGjD,MAAM,WACN,CACI,KAAK,KAAK,KAAK,gCAAgC,CAE/C,IAAI,IAAM,KAAS,KAAK,UAAU,QAAQ,CAEtC,EAAM,IAAI,SAAS,CAGvB,KAAK,UAAU,OAAO,CACtB,KAAK,KAAK,KAAK,2BAA2B,CAO9C,MAAM,YACF,EACA,EACA,EACA,EAEJ,CACI,IAAI,EAEJ,GAAG,EACH,CACI,IAAM,EAAQ,KAAK,UAAU,IAAI,EAAQ,CACrC,EAMA,EAAM,EAAM,IAJZ,KAAK,KAAK,KAAK,0BAA2B,EAAS,YAAa,EAAM,yBAAyB,CAQvG,OAAO,KAAK,aAAa,YAAY,EAAM,EAAK,EAAK,EAAQ,CAOjE,gBAAgB,EAChB,CACQ,KAAK,aAOL,KAAK,wBAA0B,EAL/B,KAAK,aAAa,gBAAgB,EAAO,CASjD,iBACA,CAMI,OALG,KAAK,aAEG,KAAK,wBAGT,KAAK,aAAa,iBAAiB,CAG9C,eAAe,EACf,CACO,IAAU,KAAK,eAElB,KAAK,aAAe,EAEjB,GAEC,KAAK,wBAA0B,KAAK,aAAa,iBAAiB,CAClE,KAAK,aAAa,gBAAgB,EAAE,EAIpC,KAAK,aAAa,gBAAgB,KAAK,wBAAwB,EAIvE,eACA,CACI,OAAO,KAAK,aAOhB,iBAAiB,EAAkB,EACnC,CACI,IAAM,EAAQ,KAAK,UAAU,IAAI,EAAQ,CACzC,GAAG,CAAC,EACJ,CACI,KAAK,KAAK,KAAK,0BAA2B,IAAW,CACrD,OAGJ,EAAM,OAAS,EAEX,EAAM,OAEN,KAAK,aAAa,aAAa,EAAM,IAAK,EAAO,CAIzD,iBAAiB,EACjB,CAEI,OADc,KAAK,UAAU,IAAI,EAAQ,EAC3B,QAAU,EAG5B,gBAAgB,EAAkB,EAClC,CACI,IAAM,EAAQ,KAAK,UAAU,IAAI,EAAQ,CACzC,GAAG,CAAC,EACJ,CACI,KAAK,KAAK,KAAK,0BAA2B,IAAW,CACrD,OAGD,IAAU,EAAM,QAEnB,EAAM,MAAQ,EACd,KAAK,aAAa,aAAa,EAAM,IAAK,EAAQ,EAAI,EAAM,OAAO,EAGvE,eAAe,EACf,CAEI,OADc,KAAK,UAAU,IAAI,EAAQ,EAC3B,OAAS,GAG3B,aACA,CACI,OAAO,MAAM,KAAK,KAAK,UAAU,MAAM,CAAC,GCpKnC,EAAb,KACA,CACI,eACA,cACA,KAEA,sBAAwD,EAAE,CAC1D,uBAA0D,EAAE,CAE5D,YACI,EACA,EACA,EAEJ,CACI,KAAK,eAAiB,EACtB,KAAK,cAAgB,EACrB,KAAK,KAAO,GAAQ,UAAU,cAAc,EAAI,IAAI,EAAW,cAAc,CAC7E,KAAK,KAAK,KAAK,0BAA0B,CAW7C,kBAAkB,EAClB,CACI,KAAK,sBAAsB,KAAK,EAAK,CAOzC,mBAAmB,EACnB,CACI,KAAK,uBAAuB,KAAK,EAAK,CAY1C,WACA,CACI,KAAK,KAAK,MAAM,4BAA4B,CAE5C,IAAM,EAAY,KAAK,cAAc,cAAc,MAAQ,GACrD,EAAgC,EAAE,CAExC,IAAI,IAAM,KAAU,KAAK,eAAe,gBAAgB,CACxD,CACI,IAAM,EAAgC,CAClC,GAAI,EAAO,GACX,KAAM,EAAO,KACb,KAAM,MAAM,KAAK,EAAO,KAAK,CAC7B,MAAO,gBAAgB,EAAO,MAAM,CACvC,CAYD,GAVG,EAAO,OAEN,EAAW,KAAO,EAAO,MAG1B,EAAO,SAEN,EAAW,SAAW,EAAO,OAAO,IAGrC,EAAO,KACV,CACI,GAAM,CAAE,WAAU,WAAU,WAAY,EAAO,KAC/C,EAAW,UAAY,CACnB,SAAU,CAAE,EAAG,EAAS,EAAG,EAAG,EAAS,EAAG,EAAG,EAAS,EAAG,CACzD,SAAU,CAAE,EAAG,EAAS,EAAG,EAAG,EAAS,EAAG,EAAG,EAAS,EAAG,CACzD,QAAS,CAAE,EAAG,EAAQ,EAAG,EAAG,EAAQ,EAAG,EAAG,EAAQ,EAAG,CACxD,CAGL,EAAS,KAAK,EAAW,CAI7B,IAAI,EAAmC,EAAE,CACzC,IAAI,IAAM,KAAQ,KAAK,sBACvB,CACI,IAAM,EAAW,GAAM,CACvB,EAAS,CAAE,GAAG,EAAQ,GAAG,EAAU,CAGvC,IAAM,EAAsB,CACxB,QAAS,EACT,YACA,WACA,SACH,CAGD,OADA,KAAK,KAAK,KAAK,cAAe,EAAS,OAAQ,wBAAyB,EAAW,GAAG,CAC/E,EAkBX,MAAM,YAAY,EAClB,CACI,KAAK,KAAK,MAAM,oCAAqC,EAAK,QAAS,WAAY,EAAK,UAAW,OAAO,CAGtG,MAAM,KAAK,cAAc,WAAW,EAAK,UAAU,CAGnD,IAAM,EAAyB,EAAE,CACjC,IAAI,IAAM,KAAU,KAAK,eAAe,gBAAgB,CAEpD,EAAY,KAAK,EAAO,GAAG,CAG/B,IAAI,IAAM,KAAM,EAGZ,MAAM,KAAK,eAAe,cAAc,EAAG,CAI/C,IAAM,EAAQ,IAAI,IAElB,IAAI,IAAM,KAAc,EAAK,SAC7B,CAEI,IAAM,EAAS,MAAM,KAAK,eAAe,aAAa,EAAW,KAAM,CACnE,KAAM,EAAW,KACjB,aAAc,EAAW,MACzB,KAAM,EAAW,KACpB,CAAC,CAKF,GAHA,EAAM,IAAI,EAAW,GAAI,EAAO,GAAG,CAGhC,EAAW,WAAa,EAAO,KAClC,CACI,GAAM,CAAE,WAAU,WAAU,WAAY,EAAW,UACnD,EAAO,KAAK,SAAS,EAAI,EAAS,EAClC,EAAO,KAAK,SAAS,EAAI,EAAS,EAClC,EAAO,KAAK,SAAS,EAAI,EAAS,EAClC,EAAO,KAAK,SAAS,EAAI,EAAS,EAClC,EAAO,KAAK,SAAS,EAAI,EAAS,EAClC,EAAO,KAAK,SAAS,EAAI,EAAS,EAClC,EAAO,KAAK,QAAQ,EAAI,EAAQ,EAChC,EAAO,KAAK,QAAQ,EAAI,EAAQ,EAChC,EAAO,KAAK,QAAQ,EAAI,EAAQ,GAKxC,IAAI,IAAM,KAAc,EAAK,SAEzB,GAAG,EAAW,SACd,CACI,IAAM,EAAc,EAAM,IAAI,EAAW,SAAS,CAC5C,EAAa,EAAM,IAAI,EAAW,GAAG,CAExC,GAAe,EAEd,KAAK,eAAe,SAAS,EAAa,EAAW,CAIrD,KAAK,KAAK,KACN,mCAAoC,EAAW,GAAI,MAAO,EAAW,SAAU,YAChE,EAAY,MAAO,EAAa,GAClD,CAMb,IAAI,IAAM,KAAQ,KAAK,uBAEnB,EAAK,EAAK,OAAO,CAGrB,KAAK,KAAK,KAAK,gBAAiB,EAAK,SAAS,OAAQ,uBAAwB,EAAK,UAAW,GAAG,CAOrG,MAAM,WACN,CACI,KAAK,sBAAwB,EAAE,CAC/B,KAAK,uBAAyB,EAAE,CAChC,KAAK,KAAK,KAAK,wBAAwB,GC9OlC,GAAb,KACA,CACI,gBACA,WAA+C,EAAE,CAGjD,mBACA,gBAOA,aACA,CAEI,KAAK,gBAAkB,CACnB,GAAI,aACJ,KAAM,WACN,KAAM,WACN,UAAW,GACd,CAGD,KAAK,sBAAsB,CAG3B,eAAiB,KAAK,wBAAwB,CAAE,EAAE,CAYtD,kBAAyB,EACzB,CACI,KAAK,mBAAqB,EAQ9B,eAAsB,EACtB,CACI,KAAK,gBAAkB,EAM3B,UACA,CACI,MAAO,CACH,KAAM,WACN,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,MAAO,EAAE,CACZ,CAML,WACA,CACI,MAAO,CAAE,GAAG,KAAK,gBAAiB,CAMtC,WACA,CAKI,OAHA,OAAO,oBAAoB,UAAW,KAAK,eAAe,CAC1D,OAAO,oBAAoB,QAAS,KAAK,aAAa,CAE/C,QAAQ,SAAS,CAU5B,sBACA,CAEI,KAAK,eAAiB,KAAK,eAAe,KAAK,KAAK,CACpD,KAAK,aAAe,KAAK,aAAa,KAAK,KAAK,CAGhD,OAAO,iBAAiB,UAAW,KAAK,eAAe,CACvD,OAAO,iBAAiB,QAAS,KAAK,aAAa,CAMvD,eAAuB,EACvB,CACI,KAAK,WAAW,EAAM,MAAQ,GAE9B,IAAM,EAAkC,EAAE,CAC1C,EAAM,EAAM,MAAQ,GAEpB,IAAM,EAA6B,CAC/B,KAAM,WACN,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,QACA,QACH,CAED,KAAK,oBAAoB,EAAM,CAMnC,aAAqB,EACrB,CACI,KAAK,WAAW,EAAM,MAAQ,GAE9B,IAAM,EAAkC,EAAE,CAC1C,EAAM,EAAM,MAAQ,GAEpB,IAAM,EAA6B,CAC/B,KAAM,WACN,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,QACA,QACH,CAED,KAAK,oBAAoB,EAAM,CAMnC,wBACA,CACO,KAAK,oBAEJ,KAAK,mBAAmB,KAAK,gBAAgB,CAOrD,oBAA4B,EAC5B,CACO,KAAK,iBAEJ,KAAK,gBAAgB,KAAK,gBAAiB,EAAM,GCnKhD,GAAb,KACA,CACI,eACA,aACA,aAAqD,EAAE,CACvD,WAA8C,EAAE,CAChD,UAA+B,CAC3B,SAAU,CAAE,EAAG,EAAG,EAAG,EAAG,CACxB,SAAU,CAAE,EAAG,EAAG,EAAG,EAAG,CAC3B,CAED,YAAsB,CAClB,OAAQ,EACR,OAAQ,EACR,OAAQ,EACR,UAAW,EACd,CAGD,mBACA,gBASA,YAAY,EAA8B,SAAS,KACnD,CACI,KAAK,eAAiB,EAGtB,KAAK,aAAe,CAChB,GAAI,UACJ,KAAM,QACN,KAAM,QACN,UAAW,GACd,CAGD,KAAK,mBAAmB,CAGxB,KAAK,WAAW,UAAY,EAC5B,KAAK,WAAW,UAAY,EAG5B,eAAiB,KAAK,wBAAwB,CAAE,EAAE,CAYtD,kBAAyB,EACzB,CACI,KAAK,mBAAqB,EAQ9B,eAAsB,EACtB,CACI,KAAK,gBAAkB,EAM3B,UACA,CACI,MAAO,CACH,KAAM,QACN,QAAS,CAAE,GAAG,KAAK,aAAc,CACjC,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,SAAU,CACN,SAAU,CAAE,GAAG,KAAK,UAAU,SAAU,CACxC,SAAU,CAAE,GAAG,KAAK,UAAU,SAAU,CAC3C,CACD,MAAO,CAAE,GAAG,KAAK,YAAa,CACjC,CAML,WACA,CACI,MAAO,CAAE,GAAG,KAAK,aAAc,CAMnC,WACA,CAMI,OALA,KAAK,eAAe,oBAAoB,cAAe,KAAK,mBAAmB,CAC/E,KAAK,eAAe,oBAAoB,YAAa,KAAK,iBAAiB,CAC3E,KAAK,eAAe,oBAAoB,cAAe,KAAK,mBAAmB,CAC/E,KAAK,eAAe,oBAAoB,QAAS,KAAK,kBAAkB,CAEjE,QAAQ,SAAS,CAY5B,mBACA,CAEI,KAAK,mBAAqB,KAAK,mBAAmB,KAAK,KAAK,CAC5D,KAAK,iBAAmB,KAAK,iBAAiB,KAAK,KAAK,CACxD,KAAK,mBAAqB,KAAK,mBAAmB,KAAK,KAAK,CAC5D,KAAK,kBAAoB,KAAK,kBAAkB,KAAK,KAAK,CAE1D,KAAK,eAAe,iBAAiB,cAAe,KAAK,mBAAmB,CAC5E,KAAK,eAAe,iBAAiB,YAAa,KAAK,iBAAiB,CACxE,KAAK,eAAe,iBAAiB,cAAe,KAAK,mBAAmB,CAC5E,KAAK,eAAe,iBAAiB,QAAS,KAAK,kBAAkB,CAMzE,mBAA2B,EAC3B,CACI,GAAG,EAAM,cAAgB,QAAW,OAEpC,IAAM,EAAY,UAAW,EAAM,SACnC,KAAK,aAAa,GAAa,CAAE,QAAS,GAAM,CAEhD,KAAK,oBAAoB,CACrB,KAAM,QACN,QACA,QAAS,CAAE,GAAG,KAAK,aAAc,CACjC,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,SAAU,CAAE,GAAG,KAAK,UAAW,CAClC,CAAC,CAMN,iBAAyB,EACzB,CACI,GAAG,EAAM,cAAgB,QAAW,OAEpC,IAAM,EAAY,UAAW,EAAM,SACnC,KAAK,aAAa,GAAa,CAAE,QAAS,GAAO,CAEjD,KAAK,oBAAoB,CACrB,KAAM,QACN,QACA,QAAS,CAAE,GAAG,KAAK,aAAc,CACjC,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,SAAU,CAAE,GAAG,KAAK,UAAW,CAClC,CAAC,CAMN,mBAA2B,EAC3B,CACO,EAAM,cAAgB,UAEzB,KAAK,UAAY,CACb,SAAU,CACN,EAAG,EAAM,QACT,EAAG,EAAM,QACZ,CACD,SAAU,CACN,EAAG,EAAM,UACT,EAAG,EAAM,UACZ,CACJ,CAED,KAAK,WAAW,UAAY,EAAM,QAClC,KAAK,WAAW,UAAY,EAAM,QAElC,KAAK,oBAAoB,CACrB,KAAM,QACN,QACA,QAAS,CAAE,GAAG,KAAK,aAAc,CACjC,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,SAAU,CAAE,GAAG,KAAK,UAAW,CAClC,CAAC,EAMN,kBAA0B,EAC1B,CACI,KAAK,YAAc,CACf,OAAQ,EAAM,OACd,OAAQ,EAAM,OACd,OAAQ,EAAM,OACd,UAAW,EAAM,UACpB,CAED,KAAK,oBAAoB,CACrB,KAAM,QACN,QACA,QAAS,CAAE,GAAG,KAAK,aAAc,CACjC,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,SAAU,CAAE,GAAG,KAAK,UAAW,CAC/B,MAAO,CAAE,GAAG,KAAK,YAAa,CACjC,CAAC,CAMN,wBACA,CACO,KAAK,oBAEJ,KAAK,mBAAmB,KAAK,aAAa,CAOlD,oBAA4B,EAC5B,CACO,KAAK,iBAEJ,KAAK,gBAAgB,KAAK,aAAc,EAAM,GCrP7C,GAAb,KACA,CACI,gBAAsE,EAAE,CACxE,cAAkF,EAAE,CACpF,YAA2E,EAAE,CAG7E,mBACA,sBACA,gBAOA,aACA,CAEI,KAAK,qBAAqB,CAY9B,kBAAyB,EACzB,CACI,KAAK,mBAAqB,EAQ9B,qBAA4B,EAC5B,CACI,KAAK,sBAAwB,EAQjC,eAAsB,EACtB,CACI,KAAK,gBAAkB,EAM3B,YACA,CACI,OAAO,OAAO,OAAO,KAAK,gBAAgB,CAAC,IAAK,IAAY,CAAE,GAAG,EAAQ,EAAmB,CAMhG,WACA,CACI,IAAM,EAA6C,EAAE,CAerD,OAbA,OAAO,KAAK,KAAK,cAAc,CAAC,QAAS,GACzC,CACI,IAAM,EAAQ,OAAO,EAAS,CACxB,EAAU,KAAK,cAAc,IAAU,EAAE,CACzC,EAAO,KAAK,YAAY,IAAU,EAAE,CAE1C,EAAO,GAAS,CACZ,KAAM,UACN,QAAS,CAAE,GAAG,EAAS,CACvB,KAAM,CAAE,GAAG,EAAM,CACpB,EACH,CAEK,EAQX,UAAiB,EACjB,CACI,IAAM,EAAS,KAAK,gBAAgB,GACpC,OAAO,EAAS,CAAE,GAAG,EAAQ,CAAG,KAQpC,SAAgB,EAChB,CACI,IAAM,EAAU,KAAK,cAAc,GAC7B,EAAO,KAAK,YAAY,GAI9B,MAFG,CAAC,GAAW,CAAC,EAAe,KAExB,CACH,KAAM,UACN,QAAS,EAAU,CAAE,GAAG,EAAS,CAAG,EAAE,CACtC,KAAM,EAAO,CAAE,GAAG,EAAM,CAAG,EAAE,CAChC,CAML,cACA,CAGI,GAAG,CAAC,UAAU,YAAe,OAE7B,IAAM,EAAW,UAAU,aAAa,CACxC,IAAI,IAAM,KAAW,EACrB,CACI,GAAG,CAAC,EAAW,SAEf,IAAM,EAAQ,EAAQ,MAEtB,GAAG,CAAC,KAAK,gBAAgB,GACzB,CACI,KAAK,wBAAwB,EAAQ,CACrC,SAIJ,IAAM,EAAS,KAAK,gBAAgB,GACpC,GAAG,CAAC,EAAU,SAGd,IAAM,EAAiB,KAAK,cAAc,IAAU,EAAE,CAChD,EAAc,KAAK,YAAY,IAAU,EAAE,CAG3C,EAA2C,EAAE,CAC/C,EAAiB,GAErB,EAAQ,QAAQ,SAAS,EAAK,IAC9B,CACI,IAAM,EAAY,UAAW,IACvB,EAA4B,CAC9B,QAAS,EAAI,QACb,QAAS,EAAI,QACb,MAAO,EAAI,MACd,CAED,EAAW,GAAa,EAExB,IAAM,EAAa,EAAe,IAC/B,CAAC,GACG,EAAW,UAAY,EAAY,SACnC,EAAW,UAAY,EAAY,SACnC,EAAW,QAAU,EAAY,SAEpC,EAAiB,KAEvB,CAGF,IAAM,EAAmC,EAAE,CACvC,EAAc,GAmBlB,GAjBA,EAAQ,KAAK,SAAS,EAAW,IACjC,CACI,IAAM,EAAU,QAAS,IACzB,EAAQ,GAAW,EAEF,EAAY,KACb,IAEZ,EAAc,KAEpB,CAGF,KAAK,cAAc,GAAS,EAC5B,KAAK,YAAY,GAAS,EAGvB,GAAkB,EACrB,CACI,IAAM,EAA4B,CAC9B,KAAM,UACN,QAAS,CAAE,GAAG,EAAY,CAC1B,KAAM,CAAE,GAAG,EAAS,CACvB,CAED,KAAK,oBAAoB,EAAQ,EAAM,GAUnD,WACA,CAKI,OAHA,OAAO,oBAAoB,mBAAoB,KAAK,wBAAwB,CAC5E,OAAO,oBAAoB,sBAAuB,KAAK,2BAA2B,CAE3E,QAAQ,SAAS,CAU5B,qBACA,CAUI,GARA,KAAK,wBAA0B,KAAK,wBAAwB,KAAK,KAAK,CACtE,KAAK,2BAA6B,KAAK,2BAA2B,KAAK,KAAK,CAG5E,OAAO,iBAAiB,mBAAoB,KAAK,wBAAwB,CACzE,OAAO,iBAAiB,sBAAuB,KAAK,2BAA2B,CAG5E,UAAU,YACb,CACI,IAAM,EAAW,UAAU,aAAa,CACxC,IAAI,IAAM,KAAW,EAEd,GAEC,KAAK,wBAAwB,EAAQ,EASrD,wBAAgC,EAChC,CACI,IAAM,EAAU,aAAiB,aAAe,EAAM,QAAU,EAC1D,EAAQ,EAAQ,MAGhB,EAAgC,CAClC,GAAI,WAAY,IAChB,KAAM,EAAQ,GACd,KAAM,UACN,UAAW,GACX,QACA,QAAS,EAAQ,QACjB,KAAM,MAAM,KAAK,EAAQ,KAAK,CAC9B,QAAS,MAAM,KAAK,EAAQ,QAAQ,CACvC,CAGD,KAAK,gBAAgB,GAAS,EAG9B,IAAM,EAA+C,EAAE,CACvD,MAAM,KAAK,EAAQ,QAAQ,CAAC,SAAS,EAAK,IAC1C,CACI,EAAe,UAAW,KAAQ,CAC9B,QAAS,EAAI,QACb,QAAS,EAAI,QACb,MAAO,EAAI,MACd,EACH,CACF,KAAK,cAAc,GAAS,EAG5B,IAAM,EAAwC,EAAE,CAChD,MAAM,KAAK,EAAQ,KAAK,CAAC,SAAS,EAAW,IAC7C,CACI,EAAa,QAAS,KAAQ,GAChC,CACF,KAAK,YAAY,GAAS,EAG1B,KAAK,uBAAuB,EAAc,CAG1C,KAAK,oBAAoB,EAAe,CACpC,KAAM,UACN,QAAS,CAAE,GAAG,EAAgB,CAC9B,KAAM,CAAE,GAAG,EAAc,CACzB,MAAO,aAAiB,aAAe,EAAQ,IAAA,GAClD,CAAC,CAMN,2BAAmC,EACnC,CAEI,IAAM,EADU,EAAM,QACA,MAGhB,EAAgB,KAAK,gBAAgB,GAExC,IAGC,EAAc,UAAY,GAG1B,KAAK,0BAA0B,EAAc,CAG7C,KAAK,gBAAgB,GAAS,IAAA,GAC9B,KAAK,cAAc,GAAS,IAAA,GAC5B,KAAK,YAAY,GAAS,IAAA,IAOlC,uBAA+B,EAC/B,CACO,KAAK,oBAEJ,KAAK,mBAAmB,EAAO,CAOvC,0BAAkC,EAClC,CACO,KAAK,uBAEJ,KAAK,sBAAsB,EAAO,CAO1C,oBAA4B,EAAwB,EACpD,CACO,KAAK,iBAEJ,KAAK,gBAAgB,EAAQ,EAAM,GCtWlC,GAAb,KACA,CACI,UAEA,YACA,SACA,WAGA,KAWA,YACI,EACA,EACA,EAEJ,CACI,KAAK,UAAY,EAGjB,KAAK,KAAO,GAAQ,UAAU,mBAAmB,EAAI,IAAI,EAAW,mBAAmB,CAEvF,KAAK,KAAK,KAAK,gCAAgC,CAG/C,KAAK,KAAK,MAAM,6CAA6C,CAC7D,KAAK,YAAc,IAAI,GACvB,KAAK,SAAW,IAAI,GAAiB,EAAO,CAC5C,KAAK,WAAa,IAAI,GAGtB,KAAK,KAAK,MAAM,oCAAoC,CACpD,KAAK,YAAY,kBAAkB,KAAK,wBAAwB,KAAK,KAAK,CAAC,CAC3E,KAAK,YAAY,eAAe,KAAK,qBAAqB,KAAK,KAAK,CAAC,CACrE,KAAK,SAAS,kBAAkB,KAAK,wBAAwB,KAAK,KAAK,CAAC,CACxE,KAAK,SAAS,eAAe,KAAK,qBAAqB,KAAK,KAAK,CAAC,CAClE,KAAK,WAAW,kBAAkB,KAAK,wBAAwB,KAAK,KAAK,CAAC,CAC1E,KAAK,WAAW,qBAAqB,KAAK,2BAA2B,KAAK,KAAK,CAAC,CAChF,KAAK,WAAW,eAAe,KAAK,qBAAqB,KAAK,KAAK,CAAC,CAEpE,KAAK,KAAK,KAAK,4CAA4C,CAU/D,wBAAgC,EAChC,CACI,KAAK,KAAK,MAAM,qBAAsB,EAAO,GAAI,IAAK,EAAO,KAAM,GAAG,CACtE,KAAK,UAAU,QAAQ,CAAE,KAAM,yBAA0B,QAAS,CAAE,SAAQ,CAAE,CAAC,CAMnF,2BAAmC,EACnC,CACI,KAAK,KAAK,MAAM,wBAAyB,EAAO,GAAI,IAAK,EAAO,KAAM,GAAG,CACzE,KAAK,UAAU,QAAQ,CAAE,KAAM,4BAA6B,QAAS,CAAE,SAAQ,CAAE,CAAC,CAMtF,qBACI,EACA,EAEJ,CACI,KAAK,KAAK,MAAM,kBAAmB,EAAO,KAAO,EAAM,CACvD,KAAK,UAAU,QAAQ,CACnB,KAAM,gBACN,QAAS,CACL,SAAU,EAAO,GACjB,SACA,QACH,CACJ,CAAC,CAUN,WACA,CAOI,OANA,KAAK,KAAK,KAAK,gCAAgC,CAG/C,KAAK,KAAK,MAAM,8CAA8C,CAGvD,QAAQ,IAAI,CACf,KAAK,YAAY,WAAW,CAC5B,KAAK,SAAS,WAAW,CACzB,KAAK,WAAW,WAAW,CAC9B,CAAC,CAAC,SACH,CACI,KAAK,KAAK,KAAK,0CAA0C,EAC3D,CAUN,aACA,CACI,KAAK,KAAK,MAAM,sCAAsC,CAEtD,IAAM,EAA0B,EAAE,CAUlC,OAPA,EAAQ,KAAK,KAAK,YAAY,WAAW,CAAC,CAC1C,EAAQ,KAAK,KAAK,SAAS,WAAW,CAAC,CAGvC,EAAQ,KAAK,GAAG,KAAK,WAAW,YAAY,CAAC,CAE7C,KAAK,KAAK,MAAM,SAAU,EAAQ,OAAQ,oBAAoB,CACvD,EASX,UAAiB,EACjB,CAII,GAHA,KAAK,KAAK,MAAM,mBAAoB,IAAY,CAG7C,EAAS,WAAW,YAAY,CAE/B,OAAO,KAAK,YAAY,WAAW,CAGvC,GAAG,EAAS,WAAW,SAAS,CAE5B,OAAO,KAAK,SAAS,WAAW,CAGpC,GAAG,EAAS,WAAW,WAAW,CAClC,CACI,IAAM,EAAQ,SAAS,EAAS,MAAM,IAAI,CAAC,GAAI,GAAG,CAClD,OAAO,KAAK,WAAW,UAAU,EAAM,CAW3C,OAPe,KAAK,aAAa,CAAC,KAAM,GAAQ,EAAI,KAAO,EAAS,GAMpE,KAAK,KAAK,KAAK,qBAAsB,IAAY,CAC1C,MAMX,cACA,CACI,KAAK,WAAW,cAAc,GCtLtC,eAAe,EACX,EACA,EAEJ,CACI,IAAM,EAAS,IAAI,EAAA,aAAa,EAAQ,EAAQ,CAEhD,OADA,MAAM,EAAO,WAAW,CACjB,EASX,SAAS,EAAkB,EAAqB,EAChD,CACI,OAAO,IAAI,EAAA,OAAO,EAAQ,EAAQ,UAAW,EAAQ,QAAS,EAAQ,mBAAmB,CAQ7F,SAAS,GAAiB,EAC1B,CACI,OAAO,IAAI,EAAA,WAAW,EAA6B,CAavD,eAAsB,GAClB,EACA,EACA,EAAsB,GAE1B,CAEI,GAAG,IAAW,KAGV,OADA,QAAQ,MAAM,oBAAoB,CAC3B,GAAiB,EAA6B,CAKzD,GAAG,EAEC,GAAG,YAAa,EAChB,CACI,IAAM,EAAc,EACpB,EAAU,CACN,GAAG,EACH,QAAS,CAAE,GAAG,EAAY,QAAS,uBAAwB,GAAM,CACpE,MAKD,EAAU,CAAE,GAAG,EAAS,uBAAwB,GAAM,CAK9D,IAAM,EAAc,EAAQ,QAAU,OAGtC,GAAG,IAAgB,SAEf,GAAG,GAAW,CAEV,GACA,CAEI,OADA,QAAQ,MAAM,6BAA6B,CACpC,MAAM,EAAmB,EAAQ,EAAQ,OAE7C,EACP,CAEI,MADA,QAAQ,MAAM,uCAAwC,EAAM,CAClD,MACN,0FACA,CAAE,MAAO,EAAO,CACnB,MAKL,MAAU,MAAM,0DAA0D,CAKlF,GAAG,IAAgB,QAGf,OADA,QAAQ,MAAM,4BAA4B,CACnC,EAAkB,EAAQ,EAAgC,CAIrE,GAAG,GAAW,CAEV,GACA,CAGI,OAFA,QAAQ,MAAM,eAAe,CAEtB,EAAmB,EAAQ,EAAQ,CACrC,MAAO,IAEJ,QAAQ,KAAK,uDAAwD,EAAM,CACpE,EAAkB,EAAQ,EAAyB,EAC5D,OAEH,EACP,CACI,QAAQ,KAAK,uDAAwD,EAAM,MAK/E,QAAQ,KAAK,+CAA+C,CAKhE,OAFA,QAAQ,MAAM,cAAc,CAErB,EAAkB,EAAQ,EAAyB,CCnJ9D,eAAsB,IACtB,CACI,OAAO,MAAA,EAAA,EAAA,UAAoB,CAM/B,SAAgB,GAAkB,EAClC,CACI,OAAO,IAAI,EAAA,YAAY,GAAM,EAAM,CCKvC,IAAa,EAAb,KACA,CACI,eAEA,YAAY,EACZ,CACI,KAAK,eAAiB,EAO1B,WAAW,EAAe,EAAY,EAAY,EAClD,CACI,IAAM,EAAO,EAAM,KAAK,EAAG,EAAE,CAC7B,OAAO,KAAK,gBAAgB,EAAM,EAAO,CAG7C,aAAa,EAAe,EAAY,EAAY,EACpD,CACI,IAAM,EAAQ,EAAM,UAAU,EAAG,EAAE,CACnC,GAAG,CAAC,EAAS,MAAO,EAAE,CAEtB,IAAM,EAA+B,EAAE,CACvC,IAAI,IAAM,KAAQ,EAClB,CACI,IAAM,EAAS,KAAK,gBAAgB,EAAM,EAAO,CAC9C,GAEC,EAAQ,KAAK,EAAO,CAG5B,OAAO,EAGX,kBAAkB,EAAe,EAAW,EAC5C,CACI,IAAM,EAAO,EAAM,YAAY,EAAI,CAEnC,OADI,EACG,KAAK,gBAAgB,EAAM,EAAO,CADtB,KAIvB,oBAAoB,EAAe,EAAW,EAC9C,CACI,IAAM,EAAQ,EAAM,iBAAiB,EAAI,CACzC,GAAG,CAAC,EAAS,MAAO,EAAE,CAEtB,IAAM,EAA+B,EAAE,CACvC,IAAI,IAAM,KAAQ,EAClB,CACI,IAAM,EAAS,KAAK,gBAAgB,EAAM,EAAO,CAC9C,GAEC,EAAQ,KAAK,EAAO,CAG5B,OAAO,EAGX,kBACI,EACA,EACA,EAAc,IACd,EAEJ,CACI,IAAM,EAAM,EAAO,cAAc,EAAY,CAC7C,OAAO,KAAK,kBAAkB,EAAO,EAAK,EAAO,CAOrD,gBAAwB,EAAoB,EAC5C,CACI,GAAG,CAAC,EAAK,KAAO,CAAC,EAAK,YAAc,CAAC,EAAK,YAEtC,OAAO,KAGX,IAAM,EAAO,EAAK,WAGd,EAAS,KAAK,eAAe,UAAU,EAAiC,CAC5E,GAAG,CAAC,EACJ,CACI,IAAI,EAAkC,EAAkC,OACxE,KAAM,IAEF,EAAS,KAAK,eAAe,UAAU,EAAQ,CAC5C,KACH,EAAU,EAAQ,OAI1B,GAAG,CAAC,EAEA,OAAO,KAIX,GAAG,EACH,CACI,GAAG,EAAO,MAAQ,EAAO,OAAS,EAAO,KAErC,OAAO,KAGX,GAAG,EAAO,UAEF,IAAM,KAAO,EAAO,KAEpB,GAAG,CAAC,EAAO,OAAO,EAAI,CAElB,OAAO,KAKnB,GAAG,EAAO,WAAa,CAAC,EAAO,UAAU,EAAO,CAE5C,OAAO,KAIf,IAAM,EAAS,EAAK,UAAU,GAAM,GAAK,CAEzC,MAAO,CACH,SACA,MAAO,EAAK,YACZ,SAAU,EAAK,SACf,OAAQ,GAAU,CAAE,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,CACtC,OACA,YAAa,EAChB,GCzHI,EAAb,KACA,CACI,QAAkB,EAClB,QAAiC,EAAE,CACnC,WAAuC,EAAE,CACzC,WAAuC,EAAE,CAWzC,MAAM,EAAa,EACnB,CACI,GAAG,EAAK,EAEJ,MAAU,WAAW,iCAAkC,IAAM,CAGjE,IAAM,EAAK,KAAK,UAGhB,OAFA,KAAK,QAAQ,KAAK,CAAE,KAAI,UAAW,EAAI,WAAU,CAAC,KAGlD,CACI,KAAK,QAAU,KAAK,QAAQ,OAAQ,GAAU,EAAM,KAAO,EAAG,EAStE,SAAS,EAAa,EACtB,CACI,GAAG,GAAM,EAEL,MAAU,WAAW,mCAAoC,IAAM,CAGnE,IAAM,EAAK,KAAK,UAGhB,OAFA,KAAK,WAAW,KAAK,CAAE,KAAI,SAAU,EAAI,QAAS,EAAG,WAAU,CAAC,KAGhE,CACI,KAAK,WAAa,KAAK,WAAW,OAAQ,GAAU,EAAM,KAAO,EAAG,EAO5E,SAAS,EACT,CAEI,IAAM,EAAwB,CAAE,GADrB,KAAK,UACoB,SAAU,EAAI,UAAW,EAAG,CAGhE,OAFA,KAAK,WAAW,KAAK,EAAM,CAEpB,CACH,IAAI,OACJ,CACI,OAAO,EAAM,WAAa,GAE9B,OACA,CACI,EAAM,UAAY,EAAM,UAE/B,CAML,WACA,CACI,KAAK,QAAU,EAAE,CACjB,KAAK,WAAa,EAAE,CAGpB,IAAI,IAAM,KAAS,KAAK,WAEpB,EAAM,UAAY,EAGtB,KAAK,WAAa,EAAE,CAMxB,KAAK,EACL,CAEI,IAAM,EAAyB,EAAE,CACjC,IAAI,IAAM,KAAS,KAAK,QAEpB,EAAM,WAAa,EAChB,EAAM,WAAa,IAElB,EAAM,UAAU,CAChB,EAAY,KAAK,EAAM,GAAG,EAI/B,EAAY,OAAS,IAEpB,KAAK,QAAU,KAAK,QAAQ,OAAQ,GAAU,CAAC,EAAY,SAAS,EAAM,GAAG,CAAC,EAIlF,IAAI,IAAM,KAAS,KAAK,WAGpB,IADA,EAAM,SAAW,EACX,EAAM,SAAW,EAAM,UAEzB,EAAM,UAAU,CAChB,EAAM,SAAW,EAAM,SAK/B,IAAI,IAAM,KAAS,KAAK,WAEjB,EAAM,UAAY,IAEjB,EAAM,UAAY,KAAK,IAAI,EAAG,EAAM,UAAY,EAAK,ICvKxD,GAAY,CACrB,QACA,QACA,OACA,OACA,QACA,OACH,CCoEY,GAAb,cAAmC,CACnC,CACI,KAAO,QACP,mBAAgC,EAAE,CAGlC,QAAkB,IAAI,IAGtB,aAAuB,GAGvB,cAA8C,KAG9C,MAAuC,KAEvC,KAAiC,IAAI,EAAW,gBAAgB,CAMhE,eAAe,EAAsB,EACrC,CACI,GAAG,KAAK,aAEJ,OAQJ,GALA,KAAK,MAAQ,EACb,KAAK,aAAe,GAEpB,KAAK,KAAO,EAAW,OAAO,UAAU,gBAAgB,CAErD,EAAW,SAAS,aAEnB,KAAK,cAAgB,EAAW,SAAS,iBAG7C,CACI,KAAK,KAAK,KAAK,0DAA0D,CACzE,OAGJ,GAAG,CAAC,KAAK,OAEL,OAIJ,IAAM,EADQ,KAAK,OAAO,MACC,OACvB,KAKJ,IAAI,GAAM,CAAE,EAAM,KAAY,OAAO,QAAQ,EAAa,CAEtD,KAAK,aAAa,EAAM,EAAsB,CAItD,aAAa,EAAoB,EACjC,CACI,MAAO,GAGX,QAAQ,EACR,CACI,KAAK,gBAAgB,CAGzB,MAAM,SACN,CACI,KAAK,gBAAgB,CAOzB,aAAqB,EAAe,EACpC,CACI,GAAG,CAAC,KAAK,eAAiB,CAAC,KAAK,OAE5B,OAGJ,IAAM,EAAW,KAAK,OAAO,GACvB,EAAO,KAAK,MAElB,KAAK,cAAc,YACf,GAAI,EAAU,GAAI,IAClB,EAAO,IACP,EAAO,QACP,CACI,KAAM,EAAO,MAAQ,GACrB,SAAU,EAAO,UAAY,GAC7B,OAAQ,EAAO,QAAU,EACzB,eAAgB,EAAO,SAAW,GAClC,mBAAoB,EAAO,aAAe,IAC1C,qBAAsB,SACzB,CACJ,CACI,KAAM,GACP,CACI,KAAK,QAAQ,IAAI,EAAM,EAAM,EAEzB,EAAO,SAAW,KAAU,GAE5B,EAAM,QAAQ,OAAO,EAAK,EAEhC,CACD,MAAO,GACR,CACI,KAAK,KAAK,MAAM,2BAA4B,EAAM,KAAM,IAAO,EACjE,CAGV,gBACA,CACI,IAAI,IAAM,KAAS,KAAK,QAAQ,QAAQ,CAEpC,EAAM,SAAS,CAEnB,KAAK,QAAQ,OAAO,CACpB,KAAK,aAAe,GACpB,KAAK,MAAQ,KACb,KAAK,cAAgB,KAazB,KAAK,EAAgB,EACrB,CACI,IAAM,EAAQ,KAAK,UAAU,EAAK,CAC/B,IAEI,IAAS,IAAA,GAMR,EAAM,MAAM,CAJZ,EAAM,KAAK,CAAE,YAAa,EAAM,CAAC,EAc7C,KAAK,EACL,CACI,GAAG,EACH,CACI,IAAM,EAAQ,KAAK,QAAQ,IAAI,EAAK,CACjC,GAEC,EAAM,MAAM,MAKhB,IAAI,IAAM,KAAS,KAAK,QAAQ,QAAQ,CAEpC,EAAM,MAAM,CAUxB,MAAM,EACN,CACI,IAAM,EAAQ,KAAK,UAAU,EAAK,CAC/B,GAEC,EAAM,OAAO,CAUrB,UAAU,EAAiB,EAC3B,CACI,GAAG,EACH,CACI,IAAM,EAAQ,KAAK,QAAQ,IAAI,EAAK,CACjC,IAEC,EAAM,OAAS,QAKnB,IAAI,IAAM,KAAS,KAAK,QAAQ,QAAQ,CAEpC,EAAM,OAAS,EAU3B,UAAU,EACV,CAEI,OADc,KAAK,UAAU,EAAK,EACpB,QAAU,EAAA,WAAW,QAMvC,eACA,CACI,OAAO,MAAM,KAAK,KAAK,QAAQ,MAAM,CAAC,CAQ1C,SAAS,EACT,CACI,OAAO,KAAK,QAAQ,IAAI,EAAK,CASjC,cAAc,EAAe,EAC7B,CACI,GAAG,CAAC,KAAK,cAAgB,CAAC,KAAK,cAE3B,MAAU,MAAM,8DAA8D,CAGlF,IAAM,EAAW,KAAK,QAAQ,IAAI,EAAK,CACpC,GAEC,EAAS,SAAS,CAGtB,KAAK,aAAa,EAAM,EAAO,CAQnC,gBAAgB,EAChB,CACI,IAAM,EAAQ,KAAK,QAAQ,IAAI,EAAK,CACjC,IAEC,EAAM,SAAS,CACf,KAAK,QAAQ,OAAO,EAAK,EAWjC,UAAkB,EAClB,CACI,GAAG,EAEC,OAAO,KAAK,QAAQ,IAAI,EAAK,CAGjC,GAAG,KAAK,QAAQ,OAAS,EAErB,OAAO,KAAK,QAAQ,QAAQ,CAAC,MAAM,CAAC,QC7WnC,EAAb,KACA,CACI,cACA,YAA+B,IAAI,IACnC,oBAAuC,IAAI,IAC3C,eAAkC,IAAI,IACtC,cAAiC,IAAI,IACrC,SAEA,YAAY,EAAuB,EACnC,CACI,KAAK,cAAgB,EACrB,KAAK,SAAW,EAOpB,IAAI,cACJ,CACI,OAAO,KAAK,cAUhB,cAAc,EAAe,EAAa,EAC1C,CACI,IAAM,EAAM,GAAI,EAAM,IAAK,IAC3B,KAAK,YAAY,IAAI,EAAK,CAAE,QAAO,CAAC,CAMxC,qBAAqB,EAAa,EAClC,CACI,KAAK,oBAAoB,IAAI,EAAI,CAAE,QAAO,CAAC,CAM/C,QAAQ,EAAgB,EACxB,CACI,IAAM,EAAY,KAAK,eAAe,IAAI,EAAM,EAAI,EAAE,CACtD,EAAU,KAAK,EAAS,CACxB,KAAK,eAAe,IAAI,EAAO,EAAU,CAM7C,OAAO,EAAgB,EACvB,CACI,IAAM,EAAY,KAAK,cAAc,IAAI,EAAM,EAAI,EAAE,CACrD,EAAU,KAAK,EAAS,CACxB,KAAK,cAAc,IAAI,EAAO,EAAU,CAU5C,cAAc,EACd,CACI,IAAM,EAAM,GAAI,KAAK,cAAe,IAAK,IACzC,OAAO,KAAK,YAAY,IAAI,EAAI,EAAI,KAAK,oBAAoB,IAAI,EAAG,CAaxE,WAAW,EACX,CACI,IAAM,EAAM,GAAI,KAAK,cAAe,IAAK,IACrC,EAAgB,KAAK,YAAY,IAAI,EAAI,CAQ7C,GALA,AAEI,IAAgB,KAAK,oBAAoB,IAAI,EAAG,CAGjD,CAAC,EAEA,MAAU,MACN,+BAAgC,KAAK,cAAe,QAAS,EAAI,IACpE,CAIL,GAAG,EAAc,OAAS,CAAC,EAAc,OAAO,CAE5C,MAAU,MACN,mCAAoC,KAAK,cAAe,QAAS,EAAI,IACxE,CAGL,IAAM,EAAgB,KAAK,cAGrB,EAAU,KAAK,cAAc,IAAI,EAAc,CACrD,GAAG,EAEC,IAAI,IAAM,KAAM,EAEZ,GAAI,CAKT,KAAK,UAEJ,KAAK,SAAS,QAAQ,CAClB,KAAM,cAAe,IACrB,QAAS,CAAE,KAAM,EAAe,KAAI,CACvC,CAAyC,CAI9C,KAAK,cAAgB,EAGrB,IAAM,EAAW,KAAK,eAAe,IAAI,EAAG,CAC5C,GAAG,EAEC,IAAI,IAAM,KAAM,EAEZ,GAAI,CAKT,KAAK,UAEJ,KAAK,SAAS,QAAQ,CAClB,KAAM,eAAgB,IACtB,QAAS,CAAE,KAAM,EAAe,KAAI,CACvC,CAAyC,GCvGzC,EAAb,cAGU,CACV,CACI,KAAO,eACP,mBAAgC,EAAE,CAElC,aACA,SAEA,aACA,CACI,OAAO,CAIP,KAAK,aAAe,IAAI,EAAqB,GAAa,CAC1D,KAAK,SAAW,GAUpB,IAAI,cACJ,CACI,OAAO,KAAK,aAAa,aAQ7B,WAAW,EACX,CACI,KAAK,aAAa,WAAW,EAAG,CAG7B,KAAK,SAEH,KAAK,OAAO,MAAkC,KAAK,UAAY,EAChE,KAAK,mBAAmB,EAQhC,aAAa,EAAoB,EACjC,CACI,MAAO,GAWX,OAAO,OACH,EAEJ,CAEI,GAAM,CAAE,eAAc,WAAU,cAAa,uBAAwB,EAErE,MAAM,UAAuC,CAC7C,CACI,aACA,CACI,OAAO,CAEP,KAAK,SAAW,EAChB,KAAK,aAAe,IAAI,EAAqB,EAAa,CAE1D,IAAI,IAAM,KAAM,EAEZ,KAAK,aAAa,cAAc,EAAG,KAAM,EAAG,GAAI,EAAG,MAAM,CAG7D,GAAG,EAEC,IAAI,IAAM,KAAM,EAEZ,KAAK,aAAa,qBAAqB,EAAG,GAAI,EAAG,MAAM,EAMvE,OAAO,IC9JT,EAAS,IAAI,EAAW,kBAAkB,CAKhD,SAAS,EAAuB,EAAa,EAA8B,EAC3E,CACI,IAAM,EAAQ,EAAK,UAAU,CAC7B,IAAI,EAAA,iBAAiB,EAAM,EAAW,CAAE,OAAM,CAAE,EAAM,CAM1D,SAAS,GAAmB,EAAa,EACzC,CACI,IAAM,EAAgB,EAAK,gBAAgB,CAAC,KACvC,GAAU,EAAM,UAAU,gBAAkB,GAChD,CAEE,aAAyB,EAAA,MAExB,EAAc,UAAY,GAC1B,EAAuB,EAAe,EAAA,iBAAiB,KAAM,EAAK,EAIlE,EAAuB,EAAM,EAAA,iBAAiB,KAAM,EAAK,CAmBjE,SAAgB,EAAwB,EACxC,CACI,EAAa,wBAAwB,YAAa,EAAM,EAAO,EAAO,IACtE,CACI,IAAM,EAAe,EAGrB,GAFA,EAAO,MAAM,wBAAyB,EAAK,KAAM,MAAO,EAAc,YAAa,aAAgB,EAAA,KAAM,GAAG,CAEzG,CAAC,EAAM,MAEN,MAAU,MAAM,2CAA2C,CAG/D,GAAG,EAAE,aAAgB,EAAA,MACrB,CACI,EAAO,KAAK,yBAA0B,EAAK,KAAM,uBAAuB,CACxE,OAGJ,IAAM,EAAQ,EAAK,UAAU,eAAwC,EAErE,OAAQ,EAAR,CAEI,IAAK,MACD,EAAuB,EAAM,EAAA,iBAAiB,IAAK,EAAK,CACxD,MAEJ,IAAK,SACD,EAAuB,EAAM,EAAA,iBAAiB,OAAQ,EAAK,CAC3D,MAEJ,IAAK,OACD,GAAmB,EAAM,EAAK,CAC9B,MAEJ,IAAK,OACD,EAAK,gBAAkB,GACvB,MAEJ,QACI,EAAO,KAAK,0BAA2B,IAAgB,CAI/D,GAAG,EAAK,UAAU,mBAClB,CACI,IAAM,EAAO,EACV,EAAK,cAEJ,EAAK,YAAY,eAAiB,MAG5C,CCjGN,IAAM,GAAS,IAAI,EAAW,aAAa,CAM3C,SAAS,GAAe,EACxB,CACI,OAAO,EACF,MAAM,IAAI,CACV,IAAK,GAAQ,WAAW,EAAI,MAAM,CAAC,CAAC,CACpC,OAAQ,GAAQ,CAAC,MAAM,EAAI,CAAC,CAkBrC,SAAgB,GAAmB,EACnC,CACI,EAAa,wBAAwB,iBAAkB,EAAM,EAAO,EAAQ,IAC5E,CACI,IAAM,EAAe,EAErB,GAAG,EAAE,aAAgB,EAAA,MAEjB,OAGJ,IAAM,EAAY,GAAe,EAAa,CAE9C,GAAG,EAAU,SAAW,EACxB,CACI,GAAO,KAAK,gCAAiC,IAAgB,CAC7D,OAGJ,IAAM,EAAW,EAAK,eAAe,GAAK,CACpC,EAAa,KAAK,IAAI,EAAU,OAAQ,EAAS,OAAO,CAG9D,IAAI,IAAI,EAAI,EAAG,EAAI,EAAY,IAE3B,EAAK,YAAY,EAAU,GAAI,EAAS,GAAG,CAI/C,IAAM,EAAe,EAAU,EAAU,OAAS,GAClD,EAAK,YAAY,EAAc,KAAK,EACtC,CClDN,SAAgB,GAAwB,EACxC,CACI,EAAa,wBAAwB,YAAa,EAAM,EAAO,EAAQ,IACvE,CACQ,GAMC,aAAgB,EAAA,OAOpB,EAAyC,WAAa,GACvD,EAAK,UAAY,KACnB,CCpBN,SAAgB,GACZ,EACA,EAEJ,CACI,IAAM,EAAmC,EAAE,CACrC,EAAuB,GAAI,EAAQ,GAEzC,IAAI,GAAM,CAAE,EAAK,KAAW,OAAO,QAAQ,EAAS,CAEhD,GAAG,EAAI,WAAW,EAAqB,CACvC,CACI,IAAM,EAAS,EAAI,MAAM,EAAqB,OAAO,CACrD,EAAO,GAAU,EAIzB,OAAO,EC3BX,IAAM,GAAS,IAAI,EAAW,eAAe,CAkB7C,SAAgB,GAAqB,EACrC,CACI,EAAa,wBAAwB,SAAU,EAAM,EAAO,EAAO,IACnE,CACI,IAAM,EAAY,EAGlB,GAAG,CAFW,EAAM,MAIhB,MAAU,MAAM,wCAAwC,CAI5D,IAAM,EAAS,GACX,EAAK,SACL,QACH,CAEK,EAAU,EAAO,QAAqB,EACtC,EAAQ,EAAO,MAAoB,GACnC,EAAW,EAAO,SAAuB,GACzC,EAAe,EAAO,UAAuB,IAC7C,EAAY,EAAO,UAAwB,GAC3C,EAAW,EAAO,SAAsB,UAExC,EAAe,EAAW,SAAS,aAEzC,GAAG,CAAC,EACJ,CACI,GAAO,KAAK,yCAA0C,EAAK,KAAM,YAAY,CAC7E,OAGJ,EAAa,YACT,GAAI,EAAK,KAAM,QACf,EACA,EACA,CACI,OACA,WACA,SACA,eAAgB,EAChB,mBAAoB,EACpB,qBAAsB,SACzB,CACJ,CACI,KAAM,GACP,CACO,GAEC,EAAM,QAAQ,OAAO,EAAK,EAEhC,CACD,MAAO,GACR,CACI,GAAO,MAAM,oCAAqC,EAAK,KAAM,KAAM,IAAO,EAC5E,EACR,CCxEN,SAAS,GACL,EACA,EACA,EACA,EACA,EAEJ,CACI,IAAM,EAAc,IAAe,QAC7B,EAAA,cAAc,2BACd,EAAA,cAAc,0BAEd,EAAY,WAAY,IAE9B,EAAc,eACV,IAAI,EAAA,kBACA,CACI,QAAS,EACT,UAAW,CAAE,KAAM,EAAY,CAClC,CACA,GACD,CACI,EAAS,QAAQ,CACb,KAAM,EACN,QAAS,CACL,QAAS,EACT,MAAO,EAAI,OACd,CACJ,CAAC,EAET,CACJ,CAML,SAAS,GACL,EACA,EACA,EACA,EAEJ,CACI,GAA2B,EAAe,EAAa,EAAU,EAAY,QAAQ,CACrF,GAA2B,EAAe,EAAa,EAAU,EAAY,OAAO,CAmBxF,SAAgB,GAAuB,EACvC,CACI,EAAa,wBAAwB,WAAY,EAAM,EAAO,EAAO,IACrE,CACI,IAAM,EAAc,EACd,EAAQ,EAAM,MAEpB,GAAG,CAAC,EAEA,MAAU,MAAM,0CAA0C,CAG9D,GAAG,EAAE,aAAgB,EAAA,cAEjB,OAGJ,IAAM,EAAW,EAAW,SAG5B,EAAK,UAAY,GACjB,EAAK,gBAAkB,GAGvB,IAAM,EAAiB,EAAK,eAAmC,IAAI,EAAA,cAAc,EAAM,CACvF,EAAK,cAAgB,EAGrB,IAAI,IAAM,KAAQ,EAAM,OAEjB,IAAS,GAAQ,CAAC,EAAK,UAAU,SAEhC,GAAwB,EAAe,EAAa,EAAU,EAAK,CAK3E,IAAM,EAAW,EAAM,yBAAyB,IAAK,GACrD,CACO,IAAS,GAAQ,CAAC,EAAK,UAAU,SAEhC,GAAwB,EAAe,EAAa,EAAU,EAAK,EAEzE,CAGF,EAAK,oBAAoB,QACzB,CACI,EAAM,yBAAyB,OAAO,EAAS,EACjD,EACJ,CC7GN,SAAgB,EAAuB,EACvC,CACI,EAAa,wBAAwB,WAAY,EAAsB,EAAiB,EAAQ,IAChG,CAEI,GAAG,EAAE,aAAgB,EAAA,cAEjB,OAKJ,IAAI,EAAU,GAEX,OAAO,GAAU,UAEhB,EAAU,EAEN,OAAO,GAAU,SAErB,EAAU,EAAM,aAAa,GAAK,QAAU,IAAU,IAElD,OAAO,GAAU,WAErB,EAAU,IAAU,GAGxB,EAAK,UAAY,GACnB,CCZN,SAAgB,GAA4B,EAC5C,CACI,EAAwB,EAAa,CACrC,GAAmB,EAAa,CAChC,GAAwB,EAAa,CACrC,GAAqB,EAAa,CAClC,GAAuB,EAAa,CACpC,EAAuB,EAAa,CCExC,eAAsB,GAClB,EACA,EACA,EAAwB,EAAE,CAE9B,CAEI,IAAM,EAAS,IAAI,EAAe,EAAQ,UAAY,QAAQ,CACxD,EAAe,EAAO,UAAU,OAAO,CAE7C,EAAa,KAAK,kCAAmC,EAAS,KAAK,CAGnE,EAAa,MAAM,+BAA+B,CAQlD,IAAM,EAAS,MAAM,GAAa,EANU,CACxC,UAAW,GACX,mBAAoB,GACpB,GAAG,EAAQ,cACd,CAEwD,EAAQ,qBAAuB,GAAM,CAE9F,EAAa,MAAM,6BAA6B,CAChD,IAAM,EAAQ,MAAM,IAAW,CACzB,EAAU,GAAkB,EAAM,CAGxC,EAAa,MAAM,wBAAwB,CAC3C,IAAM,EAAW,IAAI,EAAa,EAAO,CAGzC,EAAa,MAAM,2BAA2B,CAC9C,IAAM,EAAc,IAAI,EAAY,EAAQ,EAAQ,EAAO,EAAO,CAGlE,EAAa,MAAM,uBAAuB,CAC1C,IAAM,EAAe,IAAI,EAAa,EAAU,EAAa,EAAO,CAC9D,EAAe,IAAI,GAAiB,EAAU,EAAuB,EAAO,CAC5E,EAAiB,IAAI,GAAe,EAAU,EAAO,CACrD,EAAgB,IAAI,GAAkB,EAAU,EAAQ,EAAe,CACvE,EAAe,IAAI,GAAa,EAAU,EAAO,CACjD,EAAc,IAAI,EAAY,EAAe,EAAc,EAAO,CAClE,EAAc,IAAI,GAAY,EAAQ,EAAU,EAAe,EAAc,EAAc,EAAO,CAGxG,EAAa,MAAM,6BAA6B,CAChD,IAAM,EAAU,IAAI,EAAc,EAAc,CAGhD,EAAa,MAAM,yBAAyB,CAC5C,IAAM,EAAQ,IAAI,EAClB,EAAY,sBAAuB,GAAO,EAAM,KAAK,EAAK,IAAK,CAAC,CAGhE,IAAI,EACA,EAED,EAAQ,eAAiB,EAAQ,cAAc,OAAS,IAEvD,EAAa,MAAM,2BAA2B,CAC9C,EAAc,IAAI,EAAY,EAAO,CACrC,MAAM,EAAY,YAAY,CAE9B,EAAa,MAAM,4BAA4B,CAC/C,EAAe,IAAI,EAAa,EAAa,EAAO,CACpD,MAAM,EAAa,WAAW,EAAQ,cAAc,EAGxD,EAAa,KAAK,qBAAsB,EAAS,4BAA4B,CAG7E,EAAa,MAAM,sBAAsB,CACzC,IAAI,IAAM,KAAU,EAEhB,EAAc,yBAAyB,EAAO,CAKlD,GADA,EAAa,MAAM,wCAAwC,CACxD,EAAQ,SAEP,IAAI,IAAM,KAAW,EAAQ,SAEzB,EAAe,gBAAgB,EAAQ,CAI/C,IAAM,EAAK,IAAI,EACX,EACA,EACA,EACA,EACA,EACA,EACA,EAGA,CACI,cACA,cACH,CAGD,CACI,eACA,iBACA,gBACA,cACA,eACA,eACA,cACA,eACH,CAED,EAAQ,qBAAuB,GAClC,CAQD,OAHA,EAAc,eAAe,EAAG,CAChC,EAAa,eAAe,EAAG,CAExB"}
|
|
1
|
+
{"version":3,"file":"sage.umd.js","names":[],"sources":["../src/utils/version.ts","../src/classes/gameEngine.ts","../src/classes/loggers/consoleBackend.ts","../src/classes/loggers/nullBackend.ts","../src/utils/logger.ts","../src/classes/eventBus.ts","../src/engines/audio.ts","../src/engines/scene.ts","../src/managers/asset.ts","../src/interfaces/binding.ts","../src/interfaces/input.ts","../src/classes/bindings/trigger.ts","../src/classes/bindings/toggle.ts","../src/classes/bindings/value.ts","../src/classes/input/readers/keyboard.ts","../src/classes/input/readers/mouse.ts","../src/classes/input/readers/gamepad.ts","../src/managers/binding.ts","../src/utils/capabilities.ts","../src/managers/game.ts","../node_modules/hexoid/dist/index.mjs","../src/utils/id.ts","../src/classes/entity.ts","../src/managers/entity.ts","../src/handlers/postProcessingPipeline.ts","../src/handlers/postProcessingFrameGraph.ts","../src/handlers/postProcessing.ts","../src/utils/vectors.ts","../src/classes/level.ts","../src/managers/outline.ts","../src/classes/gameLevel.ts","../src/managers/level.ts","../src/managers/audio.ts","../src/managers/save.ts","../src/classes/input/keyboard.ts","../src/classes/input/mouse.ts","../src/classes/input/gamepad.ts","../src/managers/input.ts","../src/utils/graphics.ts","../src/utils/physics.ts","../src/utils/raycast.ts","../src/utils/timer.ts","../src/interfaces/logger.ts","../src/behaviors/sound.ts","../src/utils/stateMachine.ts","../src/behaviors/stateMachine.ts","../src/handlers/collider.ts","../src/handlers/lod.ts","../src/handlers/occluder.ts","../src/utils/metadata.ts","../src/handlers/sound.ts","../src/handlers/trigger.ts","../src/handlers/visible.ts","../src/handlers/index.ts","../src/sage.ts"],"sourcesContent":["//----------------------------------------------------------------------------------------------------------------------\n// Version utility for SAGE\n//----------------------------------------------------------------------------------------------------------------------\n\ndeclare const __APP_VERSION__ : string;\n\nexport const VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0-dev';\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Game Engine\n//\n// WARNING: This module must be imported as `import type` everywhere except sage.ts. A regular import\n// would create a circular runtime dependency (managers import GameEngine, GameEngine imports managers).\n// The `import type` is erased at compile time, keeping the runtime graph acyclic.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { AbstractEngine, HavokPlugin } from '@babylonjs/core';\n\n// Interfaces\nimport type { GameCanvas } from '../interfaces/game.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\n\n// Engines\nimport { SceneEngine } from '../engines/scene.ts';\nimport type { AudioEngine } from '../engines/audio.ts';\n\n// Managers\nimport { AssetManager } from '../managers/asset.ts';\nimport { BindingManager } from '../managers/binding.ts';\nimport { GameManager } from '../managers/game.ts';\nimport { GameEntityManager } from '../managers/entity.ts';\nimport { LevelManager } from '../managers/level.ts';\nimport { SaveManager } from '../managers/save.ts';\nimport { UserInputManager } from '../managers/input.ts';\nimport type { AudioManager } from '../managers/audio.ts';\n\n// Utils\nimport { type GameEvent, GameEventBus, type GameEventCallback, type Unsubscribe } from './eventBus.ts';\nimport type { ActionPayload, LibraryEventPayloadMap } from '../events/payloads.ts';\nimport type { WildcardPattern } from '../events/types.ts';\nimport { LoggingUtility } from '../utils/logger.ts';\nimport { RaycastHelper } from '../utils/raycast.ts';\nimport { GameTimer } from '../utils/timer.ts';\nimport { VERSION } from '../utils/version.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Type definition for game lifecycle hook functions\n */\n \nexport type GameHook = (gameEngine : GameEngine) => Promise<void>;\n\n/**\n * Interface representing the engines used in the game.\n * All engines must implement Disposable for proper cleanup.\n */\ninterface Engines extends Record<string, Disposable | undefined>\n{\n sceneEngine : SceneEngine;\n audioEngine ?: AudioEngine;\n}\n\n/**\n * Interface representing the managers used in the game.\n * All managers must implement Disposable for proper cleanup.\n */\ninterface Managers extends Record<string, Disposable | undefined>\n{\n assetManager : AssetManager;\n bindingManager : BindingManager;\n gameManager : GameManager;\n entityManager : GameEntityManager;\n inputManager : UserInputManager;\n levelManager : LevelManager;\n saveManager : SaveManager;\n audioManager ?: AudioManager;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Central hub that owns the render loop, physics, event bus, and all managers.\n * Created via `createGameEngine()` in sage.ts.\n */\nexport class GameEngine\n{\n public canvas : GameCanvas;\n public renderEngine : AbstractEngine;\n public physics : HavokPlugin;\n public managers : Managers;\n public engines : Engines;\n public eventBus : GameEventBus;\n public logger : LoggingUtility;\n public raycast : RaycastHelper;\n public timer : GameTimer;\n public largeWorldRendering : boolean;\n public started = false;\n\n private _log : LoggerInterface;\n\n // Lifecycle hooks\n private _beforeStartHook : GameHook | null = null;\n private _onStartHook : GameHook | null = null;\n private _onTeardownHook : GameHook | null = null;\n\n /**\n * Creates an instance of GameEngine.\n * @param canvas\n * @param renderEngine\n * @param physics\n * @param eventBus\n * @param logger\n * @param engines\n * @param managers\n */\n constructor(\n canvas : GameCanvas,\n renderEngine : AbstractEngine,\n physics : HavokPlugin,\n eventBus : GameEventBus,\n logger : LoggingUtility,\n raycast : RaycastHelper,\n timer : GameTimer,\n engines : Engines,\n managers : Managers,\n largeWorldRendering = false\n )\n {\n this.canvas = canvas;\n this.renderEngine = renderEngine;\n this.physics = physics;\n this.eventBus = eventBus;\n this.logger = logger;\n this.raycast = raycast;\n this.timer = timer;\n this.engines = engines;\n this.managers = managers;\n this.largeWorldRendering = largeWorldRendering;\n\n this._log = logger.getLogger('GameEngine');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a function to be called before the game engine starts\n * @param hook\n * @throws Error if a hook is already registered\n */\n onBeforeStart(hook : GameHook) : void\n {\n if(this._beforeStartHook !== null)\n {\n throw new Error('A beforeStart hook is already registered. Only one hook is allowed per lifecycle event.');\n }\n this._beforeStartHook = hook;\n }\n\n /**\n * Register a function to be called after the game engine starts\n * @param hook\n * @throws Error if a hook is already registered\n */\n onStart(hook : GameHook) : void\n {\n if(this._onStartHook !== null)\n {\n throw new Error('An onStart hook is already registered. Only one hook is allowed per lifecycle event.');\n }\n this._onStartHook = hook;\n }\n\n /**\n * Register a function to be called when the game engine stops\n * @param hook\n * @throws Error if a hook is already registered\n */\n onTeardown(hook : GameHook) : void\n {\n if(this._onTeardownHook !== null)\n {\n throw new Error('An onTeardown hook is already registered. Only one hook is allowed per lifecycle event.');\n }\n this._onTeardownHook = hook;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Event Subscriptions\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Subscribe to an event on the event bus.\n *\n * This is a convenience method that forwards to `eventBus.subscribe()`.\n *\n * @param eventType - Exact event type string, wildcard pattern (e.g. 'input:*'), or RegExp\n * @param callback\n * @returns A function that removes this subscription when called\n *\n * @example\n * // Subscribe to a specific event\n * const unsub = engine.subscribe('level:complete', (event) => {\n * console.log('Level complete:', event.payload.levelName);\n * });\n *\n * // Subscribe to all input events\n * engine.subscribe('input:*', (event) => {\n * console.log('Input event:', event.type);\n * });\n */\n subscribe<T extends keyof LibraryEventPayloadMap & string>(\n eventType : T | WildcardPattern | RegExp,\n callback : GameEventCallback<T, LibraryEventPayloadMap>\n ) : Unsubscribe\n {\n return this.eventBus.subscribe(eventType, callback);\n }\n\n /**\n * Subscribe to an action event with full type safety.\n *\n * Action events are fired by the binding system when inputs trigger registered actions.\n * All action events have the same payload structure (`ActionPayload`), containing:\n * - `value`: The action value (number for analog, boolean for digital)\n * - `deviceId`: The ID of the input device that triggered the action\n * - `context`: The binding context that was active (optional)\n *\n * @param actionName - The action name (without the 'action:' prefix)\n * @param callback\n * @returns A function that removes this subscription when called\n *\n * @example\n * // Subscribe to a specific action\n * engine.subscribeAction('jump', (event) => {\n * console.log('Jump triggered with value:', event.payload.value);\n * console.log('From device:', event.payload.deviceId);\n * });\n *\n * // Subscribe to movement action\n * engine.subscribeAction('moveForward', (event) => {\n * const velocity = Number(event.payload.value);\n * player.move(velocity);\n * });\n */\n subscribeAction(\n actionName : string,\n callback : (event : GameEvent<`action:${ string }`, Record<`action:${ string }`, ActionPayload>>) => void\n ) : Unsubscribe\n {\n return this.eventBus.subscribe(\n `action:${ actionName }` as keyof LibraryEventPayloadMap & string,\n callback as unknown as GameEventCallback<keyof LibraryEventPayloadMap & string, LibraryEventPayloadMap>\n );\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Start the engine: runs beforeStart hook, starts the game manager, then runs onStart hook.\n */\n async start() : Promise<void>\n {\n if(!this.started)\n {\n this._log.info(`Starting SkewedAspect Game Engine (Version: ${ VERSION })...`);\n\n // Execute beforeStart hook if registered\n if(this._beforeStartHook)\n {\n this._log.debug('Executing beforeStart hook...');\n await this._beforeStartHook(this);\n }\n\n // Start the game manager\n await this.managers.gameManager.start();\n\n this._log.info('Game engine started successfully');\n\n // Execute onStart hook if registered\n if(this._onStartHook)\n {\n this._log.debug('Executing onStart hook...');\n await this._onStartHook(this);\n }\n\n // Mark game as started\n this.started = true;\n }\n else\n {\n this._log.warn('Game engine is already started. Skipping start.');\n }\n }\n\n /**\n * Stop the engine: runs the teardown hook, then tears down all managers and engines in order.\n */\n async stop() : Promise<void>\n {\n if(this.started)\n {\n this._log.info(`Stopping SkewedAspect Game Engine (Version: ${ VERSION })...`);\n\n let teardownError : Error | null = null;\n\n // Execute onTeardown hook if registered\n if(this._onTeardownHook)\n {\n try\n {\n this._log.debug('Executing onTeardown hook...');\n await this._onTeardownHook(this);\n }\n catch (err)\n {\n this._log.error(`Error in onTeardown hook: ${ err }`);\n teardownError = teardownError || (err as Error);\n }\n }\n\n // Teardown all managers\n for(const managerKey of Object.keys(this.managers) as (keyof Managers)[])\n {\n const manager = this.managers[managerKey];\n if(manager)\n {\n try\n {\n this._log.debug(`Tearing down manager: ${ managerKey }`);\n // eslint-disable-next-line no-await-in-loop\n await manager.$teardown();\n }\n catch (err)\n {\n this._log.error(`Error tearing down manager ${ managerKey }: ${ err }`);\n teardownError = teardownError || (err as Error);\n }\n }\n }\n\n // Teardown all engines\n for(const engineKey of Object.keys(this.engines) as (keyof Engines)[])\n {\n const engine = this.engines[engineKey];\n if(engine)\n {\n try\n {\n this._log.debug(`Tearing down engine: ${ engineKey }`);\n // eslint-disable-next-line no-await-in-loop\n await engine.$teardown();\n }\n catch (err)\n {\n this._log.error(`Error tearing down engine ${ engineKey }: ${ err }`);\n teardownError = teardownError || (err as Error);\n }\n }\n }\n\n this.started = false;\n this._log.info('Game engine stopped successfully');\n\n // If we encountered any errors during teardown, throw the first one\n if(teardownError)\n {\n throw teardownError;\n }\n }\n else\n {\n this._log.warn('Game engine is not started. Skipping stop.');\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Console Logger Backend\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { LogLevel, LoggingBackend } from '../../interfaces/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ntype ColorLevel = LogLevel | 'timer';\n\n/**\n * A logging backend that logs to the console.\n */\nexport class ConsoleBackend implements LoggingBackend\n{\n private timers : Map<string, number>;\n\n constructor()\n {\n this.timers = new Map();\n }\n\n /**\n * Get the CSS style for a specific log level\n */\n private getStyleForLevel(level : ColorLevel) : string\n {\n // CSS styles for browser console\n const styles : Record<ColorLevel, string> = {\n trace: 'color: #999999', // Dim gray\n debug: 'color: #00AAAA', // Cyan\n info: 'color: #00AA00', // Green\n warn: 'color: #AAAA00', // Yellow\n error: 'color: #AA0000', // Red\n timer: 'color: #AA00AA', // Magenta\n none: 'color: inherit', // Default\n };\n\n return styles[level] || styles.none;\n }\n\n /**\n * Format a log message with category, timestamp and log level\n */\n private formatMessage(category : string, level : ColorLevel) : [string, string, string, string]\n {\n // Format time as HH:MM:SS AM/PM\n const now = new Date();\n const hours = now.getHours() % 12 || 12; // Convert 0 to 12 for 12 AM\n const minutes = now.getMinutes().toString()\n .padStart(2, '0');\n const seconds = now.getSeconds().toString()\n .padStart(2, '0');\n const ampm = now.getHours() >= 12 ? 'PM' : 'AM';\n const timestamp = `[${ hours }:${ minutes }:${ seconds } ${ ampm }]`;\n\n const upperLevel = level.toUpperCase();\n\n // Return format strings and styling that will be used with console methods\n return [\n `${ timestamp } %c${ upperLevel }%c (${ category }): `, // Format string with placeholders\n this.getStyleForLevel(level), // Level style\n '', // Default style\n '', // Reset style\n ];\n }\n\n trace(category : string, message : string, ...args : unknown[]) : void\n {\n const [ format, defaultStyle, levelStyle, resetStyle ] = this.formatMessage(category, 'trace');\n console.debug(format, defaultStyle, levelStyle, resetStyle, message, ...args);\n }\n\n debug(category : string, message : string, ...args : unknown[]) : void\n {\n const [ format, defaultStyle, levelStyle, resetStyle ] = this.formatMessage(category, 'debug');\n console.debug(format, defaultStyle, levelStyle, resetStyle, message, ...args);\n }\n\n info(category : string, message : string, ...args : unknown[]) : void\n {\n const [ format, defaultStyle, levelStyle, resetStyle ] = this.formatMessage(category, 'info');\n console.info(format, defaultStyle, levelStyle, resetStyle, message, ...args);\n }\n\n warn(category : string, message : string, ...args : unknown[]) : void\n {\n const [ format, defaultStyle, levelStyle, resetStyle ] = this.formatMessage(category, 'warn');\n console.warn(format, defaultStyle, levelStyle, resetStyle, message, ...args);\n }\n\n error(category : string, message : string, ...args : unknown[]) : void\n {\n const [ format, defaultStyle, levelStyle, resetStyle ] = this.formatMessage(category, 'error');\n console.error(format, defaultStyle, levelStyle, resetStyle, message, ...args);\n }\n\n /**\n * Start a timer with the specified label.\n */\n time(category : string, label : string) : void\n {\n const timerKey = `${ category }:${ label }`;\n this.timers.set(timerKey, performance.now());\n }\n\n /**\n * End a timer and log the elapsed time.\n */\n timeEnd(category : string, label : string) : void\n {\n const timerKey = `${ category }:${ label }`;\n const startTime = this.timers.get(timerKey);\n\n if(startTime !== undefined)\n {\n const duration = performance.now() - startTime;\n this.timers.delete(timerKey);\n\n const [ format, defaultStyle, levelStyle, resetStyle ] = this.formatMessage(category, 'timer');\n console.info(\n `${ format } Timer '${ label }' completed in ${ duration.toFixed(2) }ms`,\n defaultStyle,\n levelStyle,\n resetStyle\n );\n }\n else\n {\n console.warn(`[${ category }]: Timer '${ label }' does not exist`);\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Null Logger Backend\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { LoggingBackend } from '../../interfaces/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A logging backend that does nothing (discards all logs).\n * Useful for production environments or when you want to completely disable logging.\n */\nexport class NullBackend implements LoggingBackend\n{\n trace(_category : string, _message : string, ..._args : unknown[]) : void\n {\n // Do nothing\n }\n\n debug(_category : string, _message : string, ..._args : unknown[]) : void\n {\n // Do nothing\n }\n\n info(_category : string, _message : string, ..._args : unknown[]) : void\n {\n // Do nothing\n }\n\n warn(_category : string, _message : string, ..._args : unknown[]) : void\n {\n // Do nothing\n }\n\n error(_category : string, _message : string, ..._args : unknown[]) : void\n {\n // Do nothing\n }\n\n time(_category : string, _label : string) : void\n {\n // Do nothing\n }\n\n timeEnd(_category : string, _label : string) : void\n {\n // Do nothing\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Logger Utility\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type {\n LogLevel,\n LoggerInterface,\n LoggingBackend,\n} from '../interfaces/logger.ts';\n\nimport { ConsoleBackend } from '../classes/loggers/consoleBackend.ts';\nimport { NullBackend } from '../classes/loggers/nullBackend.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Implementation of Logger interface.\n * Each instance is bound to a specific category and backend.\n */\nexport class SAGELogger implements LoggerInterface\n{\n private category : string;\n private backend : LoggingBackend;\n private minLevel : LogLevel;\n\n constructor(category : string, minLevel : LogLevel = 'none', backend : LoggingBackend = new NullBackend())\n {\n this.category = category;\n this.backend = backend;\n this.minLevel = minLevel;\n }\n\n /**\n * Update the logger's backend and minimum level\n */\n public updateSettings(backend : LoggingBackend, minLevel : LogLevel) : void\n {\n this.backend = backend;\n this.minLevel = minLevel;\n }\n\n trace(message : string, ...args : unknown[]) : void\n {\n if(this.shouldLog('trace'))\n {\n this.backend.trace(this.category, message, ...args);\n }\n }\n\n debug(message : string, ...args : unknown[]) : void\n {\n if(this.shouldLog('debug'))\n {\n this.backend.debug(this.category, message, ...args);\n }\n }\n\n info(message : string, ...args : unknown[]) : void\n {\n if(this.shouldLog('info'))\n {\n this.backend.info(this.category, message, ...args);\n }\n }\n\n warn(message : string, ...args : unknown[]) : void\n {\n if(this.shouldLog('warn'))\n {\n this.backend.warn(this.category, message, ...args);\n }\n }\n\n error(message : string, ...args : unknown[]) : void\n {\n if(this.shouldLog('error'))\n {\n this.backend.error(this.category, message, ...args);\n }\n }\n\n time(label : string) : void\n {\n if(this.minLevel !== 'none')\n {\n this.backend.time(this.category, label);\n }\n }\n\n timeEnd(label : string) : void\n {\n if(this.minLevel !== 'none')\n {\n this.backend.timeEnd(this.category, label);\n }\n }\n\n /**\n * Determine if a message at the given level should be logged based on the current minimum level.\n */\n private shouldLog(level : LogLevel) : boolean\n {\n if(this.minLevel === 'none')\n {\n return false;\n }\n\n switch (this.minLevel)\n {\n case 'trace':\n return true;\n case 'debug':\n return level !== 'trace';\n case 'info':\n return level === 'info' || level === 'warn' || level === 'error';\n case 'warn':\n return level === 'warn' || level === 'error';\n case 'error':\n return level === 'error';\n default:\n return false;\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A utility for creating loggers and managing logging configuration.\n */\nexport class LoggingUtility\n{\n private backend : LoggingBackend;\n private level : LogLevel;\n private loggers : Map<string, LoggerInterface>;\n\n /**\n * Create a new LoggingUtility.\n *\n * @param level - The minimum logging level (defaults to INFO)\n * @param backend - The logging backend to use (defaults to ConsoleBackend)\n */\n constructor(level : LogLevel = 'debug', backend : LoggingBackend = new ConsoleBackend())\n {\n this.backend = backend;\n this.level = level;\n this.loggers = new Map();\n }\n\n /**\n * Set the logging backend. Updates all existing loggers to use the new backend.\n *\n * @param backend\n */\n public setBackend(backend : LoggingBackend) : void\n {\n this.backend = backend;\n\n // Update all existing loggers to use the new backend\n this.loggers.forEach((logger) =>\n {\n if(logger instanceof SAGELogger)\n {\n logger.updateSettings(this.backend, this.level);\n }\n });\n }\n\n /**\n * Set the minimum logging level. Updates all existing loggers.\n *\n * @param level\n */\n public setLevel(level : LogLevel) : void\n {\n this.level = level;\n\n // Update all existing loggers to use the new level\n this.loggers.forEach((logger) =>\n {\n if(logger instanceof SAGELogger)\n {\n logger.updateSettings(this.backend, this.level);\n }\n });\n }\n\n /**\n * Get the current minimum logging level.\n */\n public getLevel() : LogLevel\n {\n return this.level;\n }\n\n /**\n * Get or create a logger for the specified category. Returns an existing logger if one was already created.\n *\n * @param category - Typically the class or module name\n */\n public getLogger(category : string) : LoggerInterface\n {\n let logger = this.loggers.get(category);\n if(!logger)\n {\n logger = new SAGELogger(category, this.level, this.backend);\n this.loggers.set(category, logger);\n }\n\n return logger;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport { ConsoleBackend } from '../classes/loggers/consoleBackend.ts';\nexport { NullBackend } from '../classes/loggers/nullBackend.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Event Bus\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LibraryEventPayloadMap } from '../events/payloads.ts';\nimport type { WildcardPattern } from '../events/types.ts';\nimport { LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A typed game event with compile-time payload checking.\n *\n * @template T - The event type string (e.g., 'input:changed')\n * @template TPayloadMap - The payload map to resolve types from\n */\nexport interface GameEvent<\n T extends string = string,\n TPayloadMap extends Record<string, unknown> = Record<string, unknown>,\n>\n{\n /** The string identifier/category of the event (e.g. \"input:keydown\"). */\n type : T;\n /** The optional ID of whoever sent this event. */\n senderID ?: string;\n /** The optional ID of who should receive this event. */\n targetID ?: string;\n /** The payload, typed based on the payload map. */\n payload : T extends keyof TPayloadMap ? TPayloadMap[T] : unknown;\n}\n\n/**\n * A callback function that receives a {@link GameEvent} with a given payload type.\n *\n * @template T - The event type string\n * @template TPayloadMap - The payload map to resolve types from\n */\nexport type GameEventCallback<\n T extends string = string,\n TPayloadMap extends Record<string, unknown> = Record<string, unknown>,\n> = (event : GameEvent<T, TPayloadMap>) => void | Promise<void>;\n\n/**\n * Represents a subscription record internally within the EventBus.\n *\n * We store the callback as if it handles `GameEvent<any>`\n * because we can't unify multiple different payload types in the same structure.\n */\ninterface Subscription\n{\n /** Precompiled RegExp if this subscription is pattern-based. */\n pattern ?: RegExp;\n /** The callback, but stored to accept any payload type (due to the union nature). */\n callback : (event : GameEvent<any, any>) => void | Promise<void>;\n}\n\n/**\n * An unsubscribe function, returned by any subscribe method.\n */\nexport type Unsubscribe = () => void;\n\n/**\n * Callback for wildcard/pattern subscriptions.\n * Payload is unknown since multiple event types may match.\n */\nexport type WildcardEventCallback\n = (event : GameEvent<string, Record<string, unknown>>) => void | Promise<void>;\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A high-performance, type-safe event bus that supports exact or pattern-based subscriptions.\n *\n * The generic parameter `TPayloadMap` maps event type strings to their payload types,\n * enabling compile-time checking of event/payload combinations.\n *\n * @template TPayloadMap - Map of event type strings to their payload types.\n * Defaults to LibraryEventPayloadMap for internal library usage.\n *\n * @example\n * // Using library events (default)\n * const bus = new GameEventBus();\n * bus.publish({ type: 'level:progress', payload: { levelName: 'Level1', progress: 50 } });\n *\n * @example\n * // User-defined events with full type safety\n * interface MyEvents {\n * 'game:score-updated': { score: number; player: string };\n * 'game:paused': { reason: string };\n * [key: string]: unknown; // Allow other events\n * }\n *\n * const bus = new GameEventBus<MyEvents>();\n * bus.publish({ type: 'game:score-updated', payload: { score: 100, player: 'alice' } });\n *\n * @example\n * // Merging user events with library events\n * const bus = new GameEventBus<MyEvents & LibraryEventPayloadMap>();\n */\nexport class GameEventBus<TPayloadMap extends Record<string, unknown> = LibraryEventPayloadMap> implements Disposable\n{\n /**\n * For exact event-type matches:\n * - Key = event type string (e.g. \"input:keydown\")\n * - Value = set of subscriptions for that exact type\n */\n private directMap = new Map<string, Set<Subscription>>();\n\n /**\n * For pattern-based (wildcard or RegExp) subscriptions.\n */\n private patternSubs = new Set<Subscription>();\n\n /**\n * Logger instance\n */\n private _log : LoggerInterface;\n\n /**\n * Creates a new GameEventBus instance\n *\n * @param logger\n */\n constructor(logger ?: LoggingUtility)\n {\n this._log = logger?.getLogger('EventBus') || new SAGELogger('EventBus');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Subscribe\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Subscribe with automatic detection:\n * - If `eventTypeOrPattern` is a `RegExp` or a string containing `*`, treat it as a pattern subscription.\n * - Otherwise, treat it as an exact subscription.\n *\n * @template T - The event type string (must be a key in TPayloadMap for type safety)\n * @param eventTypeOrPattern - Exact event type string, wildcard pattern, or RegExp\n * @param callback\n * @returns A function that removes this subscription when called\n */\n public subscribe<T extends keyof TPayloadMap & string>(\n eventTypeOrPattern : T | WildcardPattern | RegExp,\n callback : GameEventCallback<T, TPayloadMap>\n ) : Unsubscribe\n {\n this._log.trace(`Subscribe request for: ${ eventTypeOrPattern.toString() }`);\n\n // If it's explicitly a RegExp or includes '*', treat as pattern:\n if(\n eventTypeOrPattern instanceof RegExp\n || (typeof eventTypeOrPattern === 'string' && eventTypeOrPattern.includes('*'))\n )\n {\n return this.subscribePattern(\n eventTypeOrPattern as WildcardPattern | RegExp,\n callback as WildcardEventCallback\n );\n }\n else\n {\n return this.subscribeExact(eventTypeOrPattern as T, callback);\n }\n }\n\n /**\n * Subscribes to an exact event type (e.g. \"input:keydown\").\n *\n * @template T - The event type string (must be a key in TPayloadMap for type safety)\n * @param eventType\n * @param callback\n * @returns A function that removes this subscription when called\n */\n public subscribeExact<T extends keyof TPayloadMap & string>(\n eventType : T,\n callback : GameEventCallback<T, TPayloadMap>\n ) : Unsubscribe\n {\n this._log.debug(`Adding exact subscription for: ${ eventType }`);\n\n let subs = this.directMap.get(eventType);\n if(!subs)\n {\n subs = new Set();\n this.directMap.set(eventType, subs);\n }\n\n // Store as a Subscription that accepts `GameEvent<any>`\n const subscription : Subscription = {\n callback: (event) => callback(event as GameEvent<T, TPayloadMap>),\n };\n subs.add(subscription);\n\n return () =>\n {\n this._log.debug(`Removing exact subscription for: ${ eventType }`);\n subs.delete(subscription);\n if(subs.size === 0)\n {\n this.directMap.delete(eventType);\n }\n };\n }\n\n /**\n * Subscribes with either:\n * - a wildcard string (contains `*`) that gets converted to a RegExp\n * - a RegExp directly\n *\n * Note: Payload typing is not available for wildcards since multiple event types may match.\n *\n * @param patternOrRegExp - Wildcard string (e.g. `\"input:*\"`) or a RegExp\n * @param callback\n * @returns A function that removes this subscription when called\n */\n public subscribePattern(\n patternOrRegExp : WildcardPattern | RegExp,\n callback : WildcardEventCallback\n ) : Unsubscribe\n {\n let regex : RegExp;\n\n if(typeof patternOrRegExp === 'string')\n {\n // We replace '*' with '.*' for wildcard matching\n const escaped = patternOrRegExp\n .replace(/[-/\\\\^$+?.()|[\\]{}]/g, '\\\\$&') // escape special chars\n .replace(/\\*/g, '.*'); // replace '*' with '.*'\n\n // e.g. \"input:*\" => \"input:.*\" => new RegExp(\"^input:.*$\")\n regex = new RegExp(`^${ escaped }$`);\n this._log.debug(\n `Adding pattern subscription for string: ${ patternOrRegExp }, regex: ${ regex.toString() }`\n );\n }\n else\n {\n // If already a RegExp, use as-is\n regex = patternOrRegExp;\n this._log.debug(`Adding pattern subscription for regex: ${ regex.toString() }`);\n }\n\n const subscription : Subscription = {\n pattern: regex,\n callback: (event) => callback(event as GameEvent<string, Record<string, unknown>>),\n };\n this.patternSubs.add(subscription);\n\n return () =>\n {\n this._log.debug(`Removing pattern subscription: ${ regex.toString() }`);\n this.patternSubs.delete(subscription);\n };\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Publish\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Publishes an event, invoking all matching callbacks.\n *\n * **Async Execution Model:**\n * Callbacks are invoked asynchronously via the microtask queue using `Promise.resolve().then()`.\n * This means `publish()` returns immediately, before any callbacks execute. This design:\n * - Prevents stack overflow from recursive event publishing\n * - Allows multiple events to be queued before processing\n * - Is ideal for fire-and-forget events (notifications, state changes)\n *\n * **Not suitable for request-response patterns.** If you need to wait for a callback's\n * result, consider using a different communication mechanism.\n *\n * @template T - The event type string (must be a key in TPayloadMap for type safety)\n * @param event\n */\n public publish<T extends keyof TPayloadMap & string>(\n event : GameEvent<T, TPayloadMap>\n ) : void\n {\n this._log.trace(`Publishing event: ${ event.type }`, event);\n\n let callbackCount = 0;\n\n // 1) Exact match\n const directSubs = this.directMap.get(event.type);\n if(directSubs)\n {\n callbackCount += directSubs.size;\n for(const sub of directSubs)\n {\n Promise.resolve()\n .then(() => sub.callback(event))\n .catch((err) =>\n {\n this._log.error(`Error in event handler for '${ event.type }':`, err);\n });\n }\n }\n\n // 2) Pattern subscriptions\n for(const sub of this.patternSubs)\n {\n if(sub.pattern && sub.pattern.test(event.type))\n {\n callbackCount++;\n Promise.resolve()\n .then(() => sub.callback(event))\n .catch((err) =>\n {\n this._log.error(`Error in pattern event handler for '${ event.type }':`, err);\n });\n }\n }\n\n if(callbackCount === 0)\n {\n this._log.debug(`No subscribers found for event: ${ event.type }`);\n }\n else\n {\n this._log.trace(`Event ${ event.type } dispatched to ${ callbackCount } subscribers`);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Tears down the event bus and clears all subscriptions.\n * After calling this method, no events will be delivered.\n */\n public async $teardown() : Promise<void>\n {\n this._log.debug('Tearing down EventBus');\n this.directMap.clear();\n this.patternSubs.clear();\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Audio Engine\n//----------------------------------------------------------------------------------------------------------------------\n\nimport {\n type AudioBus,\n type AudioEngineV2,\n CreateAudioEngineAsync,\n type IStaticSoundOptions,\n type MainAudioBus,\n type PrimaryAudioBus,\n type StaticSound,\n} from '@babylonjs/core';\n\n// Interfaces\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\n\n// Utils\nimport type { LoggingUtility } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class AudioEngine implements Disposable\n{\n private _engine : AudioEngineV2 | null = null;\n private _mainBus : MainAudioBus | null = null;\n private _buses = new Map<string, AudioBus>();\n private _log : LoggerInterface;\n\n constructor(logger : LoggingUtility)\n {\n this._log = logger.getLogger('AudioEngine');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n async initialize() : Promise<void>\n {\n this._log.info('Initializing audio engine...');\n\n this._engine = await CreateAudioEngineAsync();\n this._mainBus = await this._engine.createMainBusAsync('main');\n\n this._log.info('Audio engine initialized.');\n }\n\n async $teardown() : Promise<void>\n {\n this._log.info('Tearing down audio engine...');\n\n this._buses.clear();\n this._mainBus = null;\n\n if(this._engine)\n {\n this._engine.dispose();\n this._engine = null;\n }\n\n this._log.info('Audio engine torn down.');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Bus Management\n //------------------------------------------------------------------------------------------------------------------\n\n async createBus(name : string) : Promise<AudioBus>\n {\n if(!this._engine || !this._mainBus)\n {\n throw new Error('AudioEngine not initialized');\n }\n\n this._log.debug(`Creating audio bus: ${ name }`);\n const bus = await this._engine.createBusAsync(name, { outBus: this._mainBus });\n this._buses.set(name, bus);\n return bus;\n }\n\n getBus(name : string) : AudioBus | undefined\n {\n return this._buses.get(name);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Sound Creation\n //------------------------------------------------------------------------------------------------------------------\n\n async createSound(\n name : string,\n url : string,\n bus ?: AudioBus,\n options ?: Partial<IStaticSoundOptions>\n ) : Promise<StaticSound>\n {\n if(!this._engine)\n {\n throw new Error('AudioEngine not initialized');\n }\n\n const outBus : PrimaryAudioBus | undefined = bus ?? this._mainBus ?? undefined;\n\n // Spread user options and inject outBus; the cast is needed because\n // IStaticSoundOptions.outBus accepts Nullable<PrimaryAudioBus>\n const soundOptions = {\n ...options,\n ...(outBus ? { outBus } : {}),\n } as Partial<IStaticSoundOptions>;\n\n return this._engine.createSoundAsync(name, url, soundOptions);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Volume\n //------------------------------------------------------------------------------------------------------------------\n\n setMasterVolume(volume : number) : void\n {\n if(!this._engine)\n {\n throw new Error('AudioEngine not initialized');\n }\n\n this._engine.volume = volume;\n }\n\n getMasterVolume() : number\n {\n if(!this._engine)\n {\n return 1;\n }\n\n return this._engine.volume;\n }\n\n setBusVolume(bus : AudioBus, volume : number) : void\n {\n bus.volume = volume;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Scene Engine\n//\n// Wraps BabylonJS scene lifecycle, mesh loading, physics, cameras, and lights.\n// All built-in loaders (glTF, OBJ, STL, etc.) are registered via registerBuiltInLoaders()\n// and loaded on-demand when first used.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport {\n AbstractEngine,\n AbstractMesh,\n AssetContainer,\n DirectionalLight,\n FreeCamera,\n HavokPlugin,\n HemisphericLight,\n ImportMeshAsync,\n Light,\n LoadAssetContainerAsync,\n Mesh,\n MeshBuilder,\n PhysicsAggregate,\n PhysicsShapeType,\n PointLight,\n RectAreaLight,\n Scene,\n SpotLight,\n Vector3,\n} from '@babylonjs/core';\n\n// Register all built-in loaders (glTF, OBJ, STL, etc.) -- loaded on-demand when first used\nimport { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic';\nregisterBuiltInLoaders();\nimport type { HavokPhysicsWithBindings } from '@babylonjs/havok';\n\n// Interfaces\nimport type { GameCanvas } from '../interfaces/game.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\n\n// Utils\nimport { type LoggingUtility } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class SceneEngine implements Disposable\n{\n private _canvas : GameCanvas;\n private _engine : AbstractEngine;\n private _havok : HavokPhysicsWithBindings;\n private _log : LoggerInterface;\n\n constructor(canvas : GameCanvas, engine : AbstractEngine, havok : HavokPhysicsWithBindings, logger : LoggingUtility)\n {\n this._canvas = canvas;\n this._engine = engine;\n this._havok = havok;\n this._log = logger.getLogger('SceneEngine');\n }\n\n /**\n * Creates a new scene with physics enabled\n */\n public createScene() : Scene\n {\n return new Scene(this._engine);\n }\n\n public enablePhysics(\n scene : Scene,\n gravityVector : Vector3 = new Vector3(0, -9.8, 0),\n floatingOriginWorldRadius ?: number\n ) : void\n {\n this._log.debug(\n `Enabling physics with gravity (${ gravityVector.x }, ${ gravityVector.y }, ${ gravityVector.z })...`\n );\n\n const pluginOptions = floatingOriginWorldRadius !== undefined\n ? { floatingOriginWorldRadius }\n : undefined;\n\n // Each scene gets its own HavokPlugin so that scene.dispose() doesn't corrupt the shared WASM module.\n const plugin = new HavokPlugin(true, this._havok, pluginOptions);\n scene.enablePhysics(gravityVector, plugin);\n }\n\n // Camera Utilities\n /**\n * Creates a free camera at the specified position, targeting the origin. Attaches controls to the provided\n * canvas, or falls back to the engine's default canvas.\n * @param name\n * @param position\n * @param scene\n * @param canvas - Overrides the default canvas for input controls\n */\n public createFreeCamera(\n name : string,\n position : Vector3,\n scene : Scene,\n canvas ?: GameCanvas\n ) : FreeCamera\n {\n const camera = new FreeCamera(name, position, scene);\n camera.setTarget(Vector3.Zero());\n\n canvas = canvas ?? this._canvas;\n\n if(canvas)\n {\n camera.attachControl(canvas, true);\n }\n\n return camera;\n }\n\n // Lighting Utilities\n /**\n * Creates a hemispheric light (ambient lighting)\n * @param name\n * @param direction\n * @param scene\n * @param intensity - Default: 0.7\n */\n public createHemisphericLight(\n name : string,\n direction : Vector3,\n scene : Scene,\n intensity = 0.7\n ) : HemisphericLight\n {\n const light = new HemisphericLight(name, direction, scene);\n light.intensity = intensity;\n return light;\n }\n\n /**\n * Creates a directional light (sun-like lighting)\n * @param name\n * @param direction\n * @param scene\n * @param intensity - Default: 1.0\n */\n public createDirectionalLight(\n name : string,\n direction : Vector3,\n scene : Scene,\n intensity = 1.0\n ) : DirectionalLight\n {\n const light = new DirectionalLight(name, direction, scene);\n light.intensity = intensity;\n return light;\n }\n\n /**\n * Creates a point light (bulb-like lighting)\n * @param name\n * @param position\n * @param scene\n * @param intensity - Default: 1.0\n */\n public createPointLight(\n name : string,\n position : Vector3,\n scene : Scene,\n intensity = 1.0\n ) : PointLight\n {\n const light = new PointLight(name, position, scene);\n light.intensity = intensity;\n return light;\n }\n\n /**\n * Creates a spotlight (flashlight-like lighting)\n * @param name\n * @param position\n * @param direction\n * @param angle - Cone angle in radians\n * @param exponent - Controls light falloff from center to edge of cone\n * @param scene\n * @param intensity - Default: 1.0\n */\n public createSpotLight(\n name : string,\n position : Vector3,\n direction : Vector3,\n angle : number,\n exponent : number,\n scene : Scene,\n intensity = 1.0\n ) : SpotLight\n {\n const light = new SpotLight(name, position, direction, angle, exponent, scene);\n light.intensity = intensity;\n return light;\n }\n\n /**\n * Creates a rectangular area light (emits from a flat rectangle in the -Z direction)\n * @param name\n * @param position\n * @param width\n * @param height\n * @param scene\n * @param intensity - Default: 1.0\n */\n public createRectAreaLight(\n name : string,\n position : Vector3,\n width : number,\n height : number,\n scene : Scene,\n intensity = 1.0\n ) : RectAreaLight\n {\n const light = new RectAreaLight(name, position, width, height, scene);\n light.intensity = intensity;\n return light;\n }\n\n // Mesh Creation Utilities\n /**\n * Creates a sphere mesh. Defaults: diameter 1, 32 segments.\n * @param name\n * @param options\n * @param scene\n */\n public createSphere(\n name : string,\n options : { diameter ?: number; segments ?: number } = {},\n scene : Scene\n ) : Mesh\n {\n const defaultOptions = { diameter: 1, segments: 32 };\n return MeshBuilder.CreateSphere(name, { ...defaultOptions, ...options }, scene);\n }\n\n /**\n * Creates a box mesh. Default size: 1.\n * @param name\n * @param options\n * @param scene\n */\n public createBox(\n name : string,\n options : { size ?: number; width ?: number; height ?: number; depth ?: number } = {},\n scene : Scene\n ) : Mesh\n {\n const defaultOptions = { size: 1 };\n return MeshBuilder.CreateBox(name, { ...defaultOptions, ...options }, scene);\n }\n\n /**\n * Creates a ground mesh. Defaults: 6x6 with 2 subdivisions.\n * @param name\n * @param options\n * @param scene\n */\n public createGround(\n name : string,\n options : { width ?: number; height ?: number; subdivisions ?: number } = {},\n scene : Scene\n ) : Mesh\n {\n const defaultOptions = { width: 6, height: 6, subdivisions: 2 };\n return MeshBuilder.CreateGround(name, { ...defaultOptions, ...options }, scene);\n }\n\n /**\n * Creates a cylinder mesh. Defaults: height 2, diameter 1 (top and bottom).\n * @param name\n * @param options\n * @param scene\n */\n public createCylinder(\n name : string,\n options : { height ?: number; diameterTop ?: number; diameterBottom ?: number } = {},\n scene : Scene\n ) : Mesh\n {\n const defaultOptions = { height: 2, diameterTop: 1, diameterBottom: 1 };\n return MeshBuilder.CreateCylinder(name, { ...defaultOptions, ...options }, scene);\n }\n\n // Physics Utilities\n /**\n * Adds physics to a mesh. Defaults: mass 1, restitution 0.75, friction 0.5.\n * @param mesh\n * @param shapeType\n * @param physicsProps\n * @param scene\n */\n public addPhysics(\n mesh : Mesh,\n shapeType : PhysicsShapeType,\n physicsProps : { mass ?: number; restitution ?: number; friction ?: number } = {},\n scene : Scene\n ) : PhysicsAggregate\n {\n const defaultProps = { mass: 1, restitution: 0.75, friction: 0.5 };\n return new PhysicsAggregate(mesh, shapeType, { ...defaultProps, ...physicsProps }, scene);\n }\n\n // Model Loading Utilities\n /**\n * Loads a 3D model/asset into an AssetContainer without adding it to the scene automatically.\n * @param sceneFilename\n * @param scene\n */\n public async loadModel(\n sceneFilename : string,\n scene : Scene\n ) : Promise<AssetContainer>\n {\n this._log.debug(`Loading model: ${ sceneFilename }`);\n\n try\n {\n const result = await LoadAssetContainerAsync(`${ sceneFilename }`, scene);\n this._log.debug(`Model loaded successfully: ${ sceneFilename }`);\n return result;\n }\n catch (error)\n {\n this._log.error(`Failed to load model: ${ sceneFilename }`, error);\n throw error;\n }\n }\n\n /**\n * Imports meshes from a file directly into the scene.\n * @param meshNames - Specific meshes to import; empty array imports all\n * @param sceneFilename\n * @param scene\n */\n public async importMeshes(\n meshNames : string[],\n sceneFilename : string,\n scene : Scene\n ) : Promise<{\n meshes : AbstractMesh[];\n particleSystems : unknown[];\n skeletons : unknown[];\n animationGroups : unknown[];\n transformNodes : unknown[];\n geometries : unknown[];\n lights : Light[];\n }>\n {\n this._log.debug(`Importing meshes from: ${ sceneFilename }`);\n\n try\n {\n // Note: meshNames as empty array means \"import nothing\", use undefined for \"import all\"\n const options = meshNames.length > 0 ? { meshNames } : undefined;\n const result = await ImportMeshAsync(`${ sceneFilename }`, scene, options);\n this._log.debug(`Meshes imported successfully: ${ meshNames.length > 0 ? meshNames.join(', ') : 'all' }`);\n return result;\n }\n catch (error)\n {\n this._log.error(`Failed to import meshes from: ${ sceneFilename }`, error);\n throw error;\n }\n }\n\n /**\n * Tears down the scene engine and cleans up any resources\n */\n async $teardown() : Promise<void>\n {\n this._log.info('Tearing down SceneEngine');\n\n // Currently no specific resources to clean up\n // The scenes themselves are managed by the LevelManager\n // and physics engine is disposed at the application level\n\n this._log.info('SceneEngine torn down successfully');\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Asset Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { AbstractMesh, AssetContainer, InstancedMesh, Mesh } from '@babylonjs/core';\n\nimport type { GameEventBus } from '../classes/eventBus.ts';\nimport type { SceneEngine } from '../engines/scene.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport { type LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface CachedContainer\n{\n container : AssetContainer;\n refCount : number;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Central asset cache with preloading, GLB fragment extraction, mesh instancing, and reference counting.\n *\n * Supports fragment syntax (`models/props.glb#chest_lid`) to extract individual meshes from a shared container.\n * Containers are reference-counted and only disposed when all consumers release them.\n */\nexport class AssetManager implements Disposable\n{\n private _eventBus : GameEventBus;\n private _sceneEngine : SceneEngine;\n private _log : LoggerInterface;\n\n /** Full GLB/GLTF asset containers, keyed by base path (without fragment) */\n private _containers = new Map<string, CachedContainer>();\n\n /** Extracted fragment meshes (source meshes for instancing), keyed by full fragment path */\n private _sourceMeshes = new Map<string, AbstractMesh>();\n\n //------------------------------------------------------------------------------------------------------------------\n\n constructor(eventBus : GameEventBus, sceneEngine : SceneEngine, logger ?: LoggingUtility)\n {\n this._eventBus = eventBus;\n this._sceneEngine = sceneEngine;\n this._log = logger?.getLogger('AssetManager') ?? new SAGELogger('AssetManager');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Helpers\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Split a path on `#` into base path and optional fragment name.\n */\n private _parsePath(path : string) : { basePath : string; fragment ?: string }\n {\n const hashIdx = path.indexOf('#');\n if(hashIdx === -1)\n {\n return { basePath: path };\n }\n\n return {\n basePath: path.substring(0, hashIdx),\n fragment: path.substring(hashIdx + 1),\n };\n }\n\n /**\n * Load (or retrieve from cache) the AssetContainer for a base path.\n * Increments refCount.\n */\n private async _loadContainer(basePath : string) : Promise<AssetContainer>\n {\n const cached = this._containers.get(basePath);\n if(cached)\n {\n cached.refCount++;\n return cached.container;\n }\n\n this._log.debug(`Loading asset container: ${ basePath }`);\n\n // LoadAssetContainerAsync needs a scene; create a temporary one\n const tempScene = this._sceneEngine.createScene();\n\n try\n {\n const container = await this._sceneEngine.loadModel(basePath, tempScene);\n\n this._containers.set(basePath, { container, refCount: 1 });\n this._log.debug(`Cached asset container: ${ basePath }`);\n\n return container;\n }\n finally\n {\n tempScene.dispose();\n }\n }\n\n /**\n * Find a mesh by name inside a container. Throws if not found.\n */\n private _findMeshInContainer(container : AssetContainer, meshName : string, fullPath : string) : AbstractMesh\n {\n const mesh = container.meshes.find((entry) => entry.name === meshName);\n if(!mesh)\n {\n const available = container.meshes.map((entry) => entry.name).join(', ');\n throw new Error(\n `Mesh '${ meshName }' not found in container for '${ fullPath }'. `\n + `Available meshes: ${ available }`\n );\n }\n\n return mesh;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Load an asset, caching the result. Supports fragment syntax (`path#meshName`).\n *\n * - Without fragment: loads and caches the full AssetContainer, returns it.\n * - With fragment: loads the container, finds the named mesh, caches it as a source mesh, returns the mesh.\n *\n * Increments refCount on the underlying container each time.\n */\n async load(path : string) : Promise<AssetContainer | AbstractMesh>\n {\n const { basePath, fragment } = this._parsePath(path);\n\n if(fragment)\n {\n // Check if we already cached this fragment mesh\n const cachedMesh = this._sourceMeshes.get(path);\n if(cachedMesh)\n {\n // Still increment refCount on the container\n const cached = this._containers.get(basePath);\n if(cached)\n {\n cached.refCount++;\n }\n\n return cachedMesh;\n }\n\n // Load the container (handles caching + refCount internally)\n const container = await this._loadContainer(basePath);\n\n // Find and cache the named mesh\n const mesh = this._findMeshInContainer(container, fragment, path);\n this._sourceMeshes.set(path, mesh);\n\n return mesh;\n }\n\n // No fragment — return the full container\n return this._loadContainer(basePath);\n }\n\n /**\n * Create an InstancedMesh from a cached source mesh. The path must have been loaded first.\n */\n instance(path : string) : InstancedMesh\n {\n const sourceMesh = this._sourceMeshes.get(path);\n if(!sourceMesh)\n {\n throw new Error(`Asset '${ path }' is not loaded. Call load() first.`);\n }\n\n return (sourceMesh as Mesh).createInstance(`${ sourceMesh.name }-instance`);\n }\n\n /**\n * Clone a cached source mesh into an independent copy. The path must have been loaded first.\n */\n clone(path : string) : Mesh\n {\n const sourceMesh = this._sourceMeshes.get(path);\n if(!sourceMesh)\n {\n throw new Error(`Asset '${ path }' is not loaded. Call load() first.`);\n }\n\n return (sourceMesh as Mesh).clone(`${ sourceMesh.name }-clone`);\n }\n\n /**\n * Preload a batch of asset paths. Emits `asset:progress` after each and `asset:complete` when finished.\n */\n async preload(paths : string[]) : Promise<void>\n {\n const total = paths.length;\n\n for(let i = 0; i < paths.length; i++)\n {\n // eslint-disable-next-line no-await-in-loop\n await this.load(paths[i]);\n\n this._eventBus.publish({\n type: 'asset:progress',\n payload: {\n path: paths[i],\n loaded: i + 1,\n total,\n },\n });\n }\n\n this._eventBus.publish({\n type: 'asset:complete',\n payload: { paths },\n });\n }\n\n /**\n * Decrement refCount for the container backing this path.\n * When refCount hits 0, dispose the container and clean up all fragment meshes from it.\n */\n dispose(path : string) : void\n {\n const { basePath } = this._parsePath(path);\n const cached = this._containers.get(basePath);\n if(!cached)\n {\n return;\n }\n\n cached.refCount--;\n\n if(cached.refCount <= 0)\n {\n this._log.debug(`Disposing asset container: ${ basePath } (refCount reached 0)`);\n\n // Clean up all fragment meshes that came from this container\n for(const [ fragmentPath, mesh ] of this._sourceMeshes)\n {\n if(fragmentPath.startsWith(`${ basePath }#`))\n {\n mesh.dispose();\n this._sourceMeshes.delete(fragmentPath);\n }\n }\n\n cached.container.dispose();\n this._containers.delete(basePath);\n }\n }\n\n /**\n * Dispose all cached containers and fragment meshes.\n */\n disposeAll() : void\n {\n for(const cached of this._containers.values())\n {\n cached.container.dispose();\n }\n\n for(const mesh of this._sourceMeshes.values())\n {\n mesh.dispose();\n }\n\n this._containers.clear();\n this._sourceMeshes.clear();\n }\n\n /**\n * Lifecycle teardown — disposes all assets.\n */\n async $teardown() : Promise<void>\n {\n this.disposeAll();\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Binding Interfaces\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { GameEventBus } from '../classes/eventBus.ts';\nimport type { DeviceType, DeviceValueReader, DeviceValueReaderDefinition, InputState } from './input.ts';\nimport type { Action } from './action.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Types of bindings supported by the system\n */\nexport type BindingType = 'trigger' | 'toggle' | 'value';\n\n/**\n * Array of all supported binding types for validation\n */\nexport const bindingTypes : BindingType[] = [\n 'trigger',\n 'toggle',\n 'value',\n];\n\n/**\n * Context for organizing bindings into groups that can be activated/deactivated together\n */\nexport interface Context\n{\n name : string;\n\n /**\n * Whether this context is exclusive (only one exclusive context can be active at a time)\n */\n exclusive : boolean;\n}\n\n/**\n * Input definition that combines device ID and reader configuration into a single object\n */\nexport type InputDefinition = DeviceValueReaderDefinition & {\n deviceID : string;\n};\n\n/**\n * Options for captureInput() — waits for the next intentional input and returns its descriptor.\n */\nexport interface CaptureInputOptions\n{\n /** Device types to listen for */\n deviceTypes : DeviceType[];\n\n /** Optional filter for source types (e.g. ['key', 'button', 'axis']) */\n sourceTypes ?: ('key' | 'button' | 'axis' | 'position' | 'wheel')[];\n\n /** Optional AbortSignal for cancellation or timeout */\n signal ?: AbortSignal;\n}\n\n/**\n * Base interface for binding definitions with object-based sources\n */\nexport interface BindingDefinitionBase\n{\n type : BindingType;\n\n /** Name of an already-registered action */\n action : string;\n\n input : InputDefinition;\n context ?: string;\n options ?: Record<string, unknown>;\n}\n\n/**\n * Trigger binding definition\n */\nexport interface TriggerBindingDefinition extends BindingDefinitionBase\n{\n type : 'trigger';\n\n options ?: {\n edgeMode ?: 'rising' | 'falling' | 'both';\n threshold ?: number;\n passthrough ?: boolean;\n };\n}\n\n/**\n * Toggle binding definition\n */\nexport interface ToggleBindingDefinition extends BindingDefinitionBase\n{\n type : 'toggle';\n state ?: boolean;\n\n options ?: {\n invert ?: boolean;\n initialState ?: boolean;\n threshold ?: number;\n onValue ?: boolean | number;\n offValue ?: boolean | number;\n };\n}\n\n/**\n * Value binding definition\n */\nexport interface ValueBindingDefinition extends BindingDefinitionBase\n{\n type : 'value';\n options ?: {\n scale ?: number;\n offset ?: number;\n min ?: number;\n max ?: number;\n invert ?: boolean;\n emitOnChange ?: boolean;\n deadzone ?: number;\n };\n}\n\n/**\n * Union type of all binding definitions\n */\nexport type BindingDefinition\n = | TriggerBindingDefinition\n | ToggleBindingDefinition\n | ValueBindingDefinition;\n\n/**\n * Base interface for all input bindings\n */\nexport interface Binding\n{\n readonly type : BindingType;\n readonly action : Action;\n readonly context ?: string;\n readonly deviceID : string;\n readonly deviceType : DeviceType;\n readonly reader : DeviceValueReader;\n\n /**\n * Process input state and potentially emit an action event\n *\n * @param state\n * @param eventBus\n */\n process(state : InputState, eventBus : GameEventBus) : void;\n\n /**\n * Reset edge-detection state for a context activation. When an input state is provided, the binding primes its\n * internal state from the current physical input so that already-held inputs don't produce phantom edges. Without\n * a state, resets to a clean baseline (false).\n *\n * @param state - Last known input state for this binding's device, if available\n */\n resetEdgeState(state ?: InputState) : void;\n\n /**\n * Convert the binding to a JSON-serializable object.\n */\n toJSON() : BindingDefinition;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Input Device Interfaces\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Valid input device types\n */\nexport const validDeviceTypes = [ 'keyboard', 'mouse', 'gamepad' ] as const;\nexport type DeviceType = typeof validDeviceTypes[number];\n\n/**\n * Base input device interface\n */\nexport interface InputDevice\n{\n id : string;\n name : string;\n type : DeviceType;\n connected : boolean;\n}\n\n/**\n * Keyboard device information\n */\nexport interface KeyboardDevice extends InputDevice\n{\n type : 'keyboard';\n}\n\n/**\n * Mouse device information\n */\nexport interface MouseDevice extends InputDevice\n{\n type : 'mouse';\n}\n\n/**\n * Gamepad device information\n */\nexport interface GamepadDevice extends InputDevice\n{\n type : 'gamepad';\n index : number;\n mapping : string;\n axes : number[];\n buttons : GamepadButton[];\n}\n\n/**\n * Base interface for all input device states\n */\nexport interface BaseInputState\n{\n event ?: Event;\n}\n\n/**\n * Button state interface (for both mouse and gamepad)\n */\nexport interface ButtonState\n{\n pressed : boolean;\n touched ?: boolean;\n value ?: number;\n}\n\n/**\n * Mouse position interface\n */\nexport interface Position\n{\n absolute : { x : number, y : number };\n relative : { x : number, y : number };\n}\n\n/**\n * Unified keyboard input state with object literals instead of Maps\n */\nexport interface KeyboardInputState extends BaseInputState\n{\n type : 'keyboard';\n keys : Record<string, boolean>;\n delta : Record<string, boolean>;\n event ?: KeyboardEvent;\n}\n\n/**\n * Unified mouse input state with object literals instead of Maps\n */\nexport interface MouseInputState extends BaseInputState\n{\n type : 'mouse';\n buttons : Record<string, ButtonState>;\n axes : Record<string, number>;\n position : Position;\n wheel ?: {\n deltaX : number;\n deltaY : number;\n deltaZ : number;\n deltaMode : number;\n };\n event ?: MouseEvent | WheelEvent;\n}\n\n/**\n * Unified gamepad input state with object literals instead of Maps\n */\nexport interface GamepadInputState extends BaseInputState\n{\n type : 'gamepad';\n buttons : Record<string, ButtonState>;\n axes : Record<string, number>;\n event ?: GamepadEvent;\n}\n\n/**\n * Unified input state interface that can represent any input device\n */\nexport type InputState = KeyboardInputState | MouseInputState | GamepadInputState;\n\n//----------------------------------------------------------------------------------------------------------------------\n// Type Guards\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Type guard to check if an input state is from a keyboard\n */\nexport function isKeyboardState(state : InputState) : state is KeyboardInputState\n{\n return state.type === 'keyboard';\n}\n\n/**\n * Type guard to check if an input state is from a mouse\n */\nexport function isMouseState(state : InputState) : state is MouseInputState\n{\n return state.type === 'mouse';\n}\n\n/**\n * Type guard to check if an input state is from a gamepad\n */\nexport function isGamepadState(state : InputState) : state is GamepadInputState\n{\n return state.type === 'gamepad';\n}\n\n/**\n * Base interface for serialized device value readers\n */\nexport interface DeviceValueReaderDefinitionBase\n{\n /** The device type: 'keyboard', 'mouse', or 'gamepad' */\n type : DeviceType;\n\n /** The input source category (e.g. 'key', 'button', 'axis', 'position', 'wheel') */\n sourceType : string;\n\n /** The specific input identifier (e.g. 'KeyA', 'button-0', 'axis-1', 'absolute:x') */\n sourceKey : string;\n}\n\n/**\n * Keyboard value reader definition\n */\nexport interface KeyboardValueReaderDefinition extends DeviceValueReaderDefinitionBase\n{\n type : 'keyboard';\n sourceType : 'key';\n sourceKey : string;\n options ?: {\n useDelta ?: boolean;\n };\n}\n\n/**\n * Mouse value reader definition\n */\nexport interface MouseValueReaderDefinition extends DeviceValueReaderDefinitionBase\n{\n type : 'mouse';\n sourceType : string;\n sourceKey : string;\n}\n\n/**\n * Gamepad value reader definition\n */\nexport interface GamepadValueReaderDefinition extends DeviceValueReaderDefinitionBase\n{\n type : 'gamepad';\n sourceType : string;\n sourceKey : string;\n options ?: {\n useAnalogValue ?: boolean;\n deadzone ?: number;\n invert ?: boolean;\n };\n}\n\n/**\n * Union type of all device value reader definitions\n */\nexport type DeviceValueReaderDefinition\n = | KeyboardValueReaderDefinition\n | MouseValueReaderDefinition\n | GamepadValueReaderDefinition;\n\n/**\n * Interface for all device value readers\n * Unified interface for all types of input sources that can extract values from device states\n */\nexport interface DeviceValueReader\n{\n /** The input source category (e.g. 'key', 'button', 'axis', 'position', 'wheel') */\n readonly sourceType : string;\n\n /** The specific input identifier (e.g. 'KeyA', 'button-0', 'axis-1', 'absolute:x') */\n readonly sourceKey : string;\n\n /**\n * Gets the value from an input state, or undefined if this reader doesn't apply to the given state type.\n *\n * @param state\n */\n getValue(state : InputState) : boolean | number | undefined;\n\n /**\n * Convert the reader to a JSON-serializable object.\n */\n toJSON() : DeviceValueReaderDefinition;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Trigger Binding\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { GameEventBus } from '../eventBus.ts';\n\n// Interfaces\nimport type { Action } from '../../interfaces/action.ts';\nimport type { Binding, TriggerBindingDefinition } from '../../interfaces/binding.ts';\nimport type { DeviceType, DeviceValueReader, InputState } from '../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * All supported edge modes for validation\n */\nexport const edgeModes = [ 'rising', 'falling', 'both' ] as const;\n\n/**\n * Defines when a trigger binding will fire based on input state changes\n */\nexport type EdgeMode = typeof edgeModes[number];\n\n/**\n * Options for configuring a trigger binding\n */\nexport interface TriggerBindingOptions\n{\n /**\n * Which edge(s) should cause the trigger to fire\n * - 'rising': Only trigger on false -> true transitions\n * - 'falling': Only trigger on true -> false transitions\n * - 'both': Trigger on any state change\n */\n edgeMode ?: EdgeMode;\n\n /**\n * Threshold for analog inputs (0.0 to 1.0)\n * When input value is >= threshold, it's considered \"active\" (true)\n * When input value is < threshold, it's considered \"inactive\" (false)\n * Defaults to 0.5 for analog inputs\n */\n threshold ?: number;\n\n /**\n * Optional context name this binding belongs to\n */\n context ?: string;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Implementation of a trigger binding, which emits an action when a button/key is pressed or released.\n * Can handle both digital and analog inputs with configurable threshold.\n * Can output either digital or analog values based on the action type.\n */\nexport class TriggerBinding implements Binding\n{\n public readonly type = 'trigger' as const;\n public readonly action : Action;\n public readonly context ?: string;\n public readonly deviceID : string;\n public readonly deviceType : DeviceType;\n public readonly reader : DeviceValueReader;\n\n // Trigger-specific options\n private readonly _edgeMode : EdgeMode;\n private readonly _threshold : number;\n\n // State tracking\n private _lastDigitalState = false;\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Getters\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Get the current options for this trigger binding.\n */\n get options() : TriggerBindingOptions\n {\n return {\n edgeMode: this._edgeMode,\n threshold: this._threshold,\n };\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new trigger binding\n *\n * @param action\n * @param deviceID\n * @param deviceType\n * @param reader\n * @param options\n */\n constructor(\n action : Action,\n deviceID : string,\n deviceType : DeviceType,\n reader : DeviceValueReader,\n options : TriggerBindingOptions = {}\n )\n {\n this.action = action;\n this.deviceID = deviceID;\n this.deviceType = deviceType;\n this.reader = reader;\n this.context = options.context;\n\n // Set options with defaults\n this._edgeMode = options.edgeMode ?? 'rising';\n this._threshold = options.threshold ?? 0.5;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Process input state and emit an action event on the configured edge transition(s).\n *\n * @param state\n * @param eventBus\n */\n public process(state : InputState, eventBus : GameEventBus) : void\n {\n // Extract the raw value using our source\n const rawValue = this.reader.getValue(state) ?? false;\n\n // Determine the digital state based on the threshold\n const digitalState = typeof rawValue === 'boolean'\n ? rawValue\n : rawValue >= this._threshold;\n\n // Determine if we should trigger based on the configured edge mode\n let shouldTrigger = false;\n\n switch (this._edgeMode)\n {\n case 'rising':\n shouldTrigger = digitalState && !this._lastDigitalState;\n break;\n case 'falling':\n shouldTrigger = !digitalState && this._lastDigitalState;\n break;\n case 'both':\n shouldTrigger = digitalState !== this._lastDigitalState;\n break;\n }\n\n // Update last known state for next time\n this._lastDigitalState = digitalState;\n\n // If triggered, emit the action event\n if(shouldTrigger)\n {\n let outputValue : boolean | number;\n if(this.action.type === 'analog')\n {\n // Use raw value and convert to a number if needed\n let finalValue = typeof rawValue === 'number' ? rawValue : (rawValue ? 1.0 : 0.0);\n\n // Apply scaling if action parameters exist\n if(this.action.minValue !== undefined || this.action.maxValue !== undefined)\n {\n const min = this.action.minValue ?? 0;\n const max = this.action.maxValue ?? 1;\n finalValue = min + (finalValue * (max - min));\n }\n outputValue = finalValue;\n }\n else\n {\n outputValue = true;\n }\n const actionEvent = {\n type: `action:${ this.action.name }`,\n payload: {\n value: outputValue,\n deviceId: this.deviceID,\n context: this.context,\n },\n };\n eventBus.publish(actionEvent);\n }\n }\n\n /**\n * Reset edge-detection state for a context activation. When a state is provided, primes from the current physical\n * input so that already-held inputs don't produce phantom rising edges in the newly activated context.\n */\n public resetEdgeState(state ?: InputState) : void\n {\n if(state)\n {\n const rawValue = this.reader.getValue(state) ?? false;\n this._lastDigitalState = typeof rawValue === 'boolean'\n ? rawValue\n : rawValue >= this._threshold;\n }\n else\n {\n this._lastDigitalState = false;\n }\n }\n\n /**\n * Returns a JSON-serializable representation of this trigger binding.\n */\n public toJSON() : TriggerBindingDefinition\n {\n return {\n type: this.type,\n action: this.action.name,\n input: {\n deviceID: this.deviceID,\n ...this.reader.toJSON(),\n },\n context: this.context,\n options: this.options,\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Toggle Binding\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { GameEventBus } from '../eventBus.ts';\n\n// Interfaces\nimport type { Action } from '../../interfaces/action.ts';\nimport type { Binding, ToggleBindingDefinition } from '../../interfaces/binding.ts';\nimport type { DeviceType, DeviceValueReader, InputState } from '../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Options for configuring a toggle binding\n */\nexport interface ToggleBindingOptions\n{\n /**\n * If true, toggle on falling edge (true -> false) instead of rising edge (false -> true)\n */\n invert ?: boolean;\n\n /**\n * Initial toggle state (defaults to false - off)\n */\n initialState ?: boolean;\n\n /**\n * Threshold for analog inputs (0.0 to 1.0)\n * When input value is >= threshold, it's considered \"active\" (true)\n * When input value is < threshold, it's considered \"inactive\" (false)\n * Defaults to 0.5 for analog inputs\n */\n threshold ?: number;\n\n /**\n * Value to emit when the toggle is in the \"on\" state for digital actions\n * If undefined, will emit boolean true\n * Ignored for analog actions\n */\n onValue ?: boolean | number;\n\n /**\n * Value to emit when the toggle is in the \"off\" state for digital actions\n * If undefined, will emit boolean false\n * Ignored for analog actions\n */\n offValue ?: boolean | number;\n\n /**\n * Optional context name this binding belongs to\n */\n context ?: string;\n}\n\n/**\n * Implementation of a toggle binding, which maintains and toggles state between true/false\n * when a button/key is pressed or released, remaining in that state until toggled again.\n * Can handle both digital and analog inputs with configurable threshold.\n * Can output either digital or analog values based on the action type.\n */\nexport class ToggleBinding implements Binding\n{\n public readonly type = 'toggle' as const;\n public readonly action : Action;\n public readonly context ?: string;\n public readonly deviceID : string;\n public readonly deviceType : DeviceType;\n public readonly reader : DeviceValueReader;\n\n // Toggle-specific options\n private readonly _invert : boolean;\n private readonly _threshold : number;\n private readonly _initialState : boolean;\n private readonly _onValue : boolean | number;\n private readonly _offValue : boolean | number;\n\n // State tracking\n private _lastDigitalState = false;\n private _toggleState : boolean;\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Getters\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Get the current toggle state (true = on, false = off)\n */\n public get state() : boolean\n {\n return this._toggleState;\n }\n\n /**\n * Set the toggle state directly (useful for programmatic control)\n */\n public set state(value : boolean)\n {\n this._toggleState = value;\n }\n\n /**\n * Get the value emitted when toggle is in the \"on\" state\n */\n public get onValue() : boolean | number\n {\n return this._onValue;\n }\n\n /**\n * Get the value emitted when toggle is in the \"off\" state\n */\n public get offValue() : boolean | number\n {\n return this._offValue;\n }\n\n /**\n * Get the current value based on toggle state.\n * For analog actions, returns min/max value. For digital, returns on/off value.\n */\n public get value() : boolean | number\n {\n if(this.action.type === 'analog')\n {\n return this._toggleState\n ? (this.action.maxValue ?? 1)\n : (this.action.minValue ?? 0);\n }\n else\n {\n return this._toggleState ? this._onValue : this._offValue;\n }\n }\n\n /**\n * Get the current options for this toggle binding.\n */\n public get options() : ToggleBindingOptions\n {\n return {\n invert: this._invert,\n threshold: this._threshold,\n initialState: this._initialState,\n onValue: this._onValue,\n offValue: this._offValue,\n };\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new toggle binding\n *\n * @param action\n * @param deviceID\n * @param deviceType\n * @param reader\n * @param options\n */\n constructor(\n action : Action,\n deviceID : string,\n deviceType : DeviceType,\n reader : DeviceValueReader,\n options : ToggleBindingOptions = {}\n )\n {\n this.action = action;\n this.deviceID = deviceID;\n this.deviceType = deviceType;\n this.reader = reader;\n this.context = options.context;\n\n // Set options with defaults\n this._invert = options.invert ?? false;\n this._threshold = options.threshold ?? 0.5;\n this._initialState = options.initialState ?? false;\n this._toggleState = this._initialState;\n\n // For digital actions, these control the output value\n // For analog actions, these are not used (we use action properties instead)\n this._onValue = options.onValue ?? true;\n this._offValue = options.offValue ?? false;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Process input state and emit an action event if the toggle flipped.\n *\n * @param state\n * @param eventBus\n */\n public process(state : InputState, eventBus : GameEventBus) : void\n {\n const rawValue = this.reader.getValue(state) ?? false;\n const digitalState = typeof rawValue === 'boolean'\n ? rawValue\n : rawValue >= this._threshold;\n\n const shouldToggle = this._invert\n ? !digitalState && this._lastDigitalState\n : digitalState && !this._lastDigitalState;\n\n this._lastDigitalState = digitalState;\n\n if(!shouldToggle) { return; }\n\n this._toggleState = !this._toggleState;\n\n eventBus.publish({\n type: `action:${ this.action.name }`,\n payload: {\n value: this.value,\n deviceId: this.deviceID,\n context: this.context,\n },\n });\n }\n\n /**\n * Reset edge-detection state for a context activation. When a state is provided, primes from the current physical\n * input so that already-held inputs don't produce phantom edges in the newly activated context.\n */\n public resetEdgeState(state ?: InputState) : void\n {\n if(state)\n {\n const rawValue = this.reader.getValue(state) ?? false;\n this._lastDigitalState = typeof rawValue === 'boolean'\n ? rawValue\n : rawValue >= this._threshold;\n }\n else\n {\n this._lastDigitalState = false;\n }\n }\n\n /**\n * Reset toggle to its initial state\n */\n public reset() : void\n {\n this._toggleState = this._initialState;\n }\n\n /**\n * Returns a JSON-serializable representation of this toggle binding.\n */\n public toJSON() : ToggleBindingDefinition\n {\n return {\n type: this.type,\n action: this.action.name,\n input: {\n deviceID: this.deviceID,\n ...this.reader.toJSON(),\n },\n state: this._toggleState,\n context: this.context,\n options: this.options,\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Value Binding\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { GameEventBus } from '../eventBus.ts';\n\n// Interfaces\nimport type { Action } from '../../interfaces/action.ts';\nimport type { Binding, ValueBindingDefinition } from '../../interfaces/binding.ts';\nimport type { DeviceType, DeviceValueReader, InputState } from '../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Options for configuring a value binding\n */\nexport interface ValueBindingOptions\n{\n /**\n * Factor to scale the input value by (1.0 = no scaling)\n */\n scale ?: number;\n\n /**\n * Value to add to the input value after scaling\n */\n offset ?: number;\n\n /**\n * Whether to invert the input value (multiply by -1)\n */\n invert ?: boolean;\n\n /**\n * Optional context name this binding belongs to\n */\n context ?: string;\n\n /**\n * If true, only emit values when they change\n * If false, always emit values on each process call\n */\n emitOnChange ?: boolean;\n\n /**\n * Deadzone threshold (0.0 to 1.0)\n * Input values below this threshold will be treated as 0\n */\n deadzone ?: number;\n\n /**\n * Value to emit when the input is digital and true\n * Only used when action is digital\n */\n onValue ?: boolean | number;\n\n /**\n * Value to emit when the input is digital and false\n * Only used when action is digital\n */\n offValue ?: boolean | number;\n\n /**\n * Minimum value to clamp output to\n */\n min ?: number;\n\n /**\n * Maximum value to clamp output to\n */\n max ?: number;\n}\n\n/**\n * Implementation of a value binding, which directly converts an analog input\n * to an analog output with optional scaling, offset, and clamping.\n * Can handle both digital and analog inputs/outputs based on action type.\n */\nexport class ValueBinding implements Binding\n{\n public readonly type = 'value' as const;\n public readonly action : Action;\n public readonly context ?: string;\n public readonly deviceID : string;\n public readonly deviceType : DeviceType;\n public readonly reader : DeviceValueReader;\n\n // Value-specific options\n private readonly _scale : number;\n private readonly _offset : number;\n private readonly _invert : boolean;\n private readonly _emitOnChange : boolean;\n private readonly _deadzone : number;\n private readonly _onValue : boolean | number;\n private readonly _offValue : boolean | number;\n private readonly _min : number;\n private readonly _max : number;\n\n // State tracking\n private _lastValue : number | boolean | undefined;\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Get the current options for this value binding.\n */\n get options() : ValueBindingOptions\n {\n return {\n scale: this._scale,\n offset: this._offset,\n invert: this._invert,\n emitOnChange: this._emitOnChange,\n deadzone: this._deadzone,\n onValue: this._onValue,\n offValue: this._offValue,\n min: this._min,\n max: this._max,\n };\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new value binding\n *\n * @param action\n * @param deviceID\n * @param reader\n * @param options\n */\n constructor(\n action : Action,\n deviceID : string,\n deviceType : DeviceType,\n reader : DeviceValueReader,\n options : ValueBindingOptions = {}\n )\n {\n this.action = action;\n this.deviceID = deviceID;\n this.deviceType = deviceType;\n this.reader = reader;\n this.context = options.context;\n\n // Common options that apply to both analog and digital\n this._scale = options.scale ?? 1.0;\n this._offset = options.offset ?? 0.0;\n this._invert = options.invert ?? false;\n this._emitOnChange = options.emitOnChange ?? true;\n this._deadzone = options.deadzone ?? 0.0;\n\n // For digital actions, these control the output value\n // For analog actions, these are not used\n this._onValue = options.onValue ?? true;\n this._offValue = options.offValue ?? false;\n\n // Store min/max values explicitly\n const defaultMin = this.action.type === 'analog'\n ? this.action.minValue ?? Number.NEGATIVE_INFINITY\n : Number.NEGATIVE_INFINITY;\n this._min = options.min ?? defaultMin;\n\n const defaultMax = this.action.type === 'analog'\n ? this.action.maxValue ?? Number.POSITIVE_INFINITY\n : Number.POSITIVE_INFINITY;\n this._max = options.max ?? defaultMax;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Process input state, apply scaling/deadzone/clamping, and emit an action event if the value changed.\n *\n * @param state\n * @param eventBus\n */\n public process(state : InputState, eventBus : GameEventBus) : void\n {\n const rawValue = this.reader.getValue(state);\n if(rawValue === undefined) { return; }\n\n const numericValue = typeof rawValue === 'boolean' ? (rawValue ? 1.0 : 0.0) : rawValue;\n\n let value = this._deadzone > 0 && Math.abs(numericValue) < this._deadzone ? 0 : numericValue;\n value = ((this._invert ? -value : value) * this._scale) + this._offset;\n\n // Apply min/max clamping\n value = Math.max(this._min, Math.min(this._max, value));\n\n if(this._emitOnChange && this._lastValue === value) { return; }\n this._lastValue = value;\n\n eventBus.publish({\n type: `action:${ this.action.name }`,\n payload: {\n value,\n deviceId: this.deviceID,\n context: this.context,\n },\n });\n }\n\n /**\n * No-op for value bindings — they have no edge-detection state to reset.\n */\n public resetEdgeState(_state ?: InputState) : void\n {\n // Value bindings don't track edge state\n }\n\n /**\n * Returns a JSON-serializable representation of this value binding.\n */\n public toJSON() : ValueBindingDefinition\n {\n return {\n type: this.type,\n action: this.action.name,\n input: {\n deviceID: this.deviceID,\n ...this.reader.toJSON(),\n },\n context: this.context,\n options: this.options,\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Keyboard Value Reader\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type {\n DeviceValueReader,\n InputState,\n KeyboardInputState,\n KeyboardValueReaderDefinition,\n} from '../../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Types of keyboard input sources\n */\nexport type KeyboardSourceType = 'key';\n\n/**\n * Options for configuring a keyboard value reader\n */\nexport interface KeyboardReaderOptions\n{\n /**\n * Whether to use delta state instead of current state\n * When true, the source will only return true on the initial key press\n */\n useDelta ?: boolean;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Reads values from keyboard input states\n */\nexport class KeyboardValueReader implements DeviceValueReader\n{\n /**\n * Always 'key' for keyboard readers.\n */\n public readonly sourceType : KeyboardSourceType = 'key';\n\n /**\n * The key code to monitor (e.g. \"KeyA\", \"Space\", \"ArrowUp\")\n */\n public readonly sourceKey : string;\n\n /**\n * When true, reads from delta (only true on the frame the key was first pressed/released)\n */\n private readonly useDelta : boolean;\n\n /**\n * Creates a new KeyboardValueReader\n *\n * @param keyCode - KeyboardEvent.code value (e.g. \"KeyA\", \"Space\", \"ArrowUp\")\n * @param options\n */\n constructor(keyCode : string, options : KeyboardReaderOptions = {})\n {\n this.sourceKey = keyCode;\n this.useDelta = options.useDelta || false;\n }\n\n /**\n * Gets the value of the key state. Returns undefined for non-keyboard input states.\n *\n * @param state\n */\n public getValue(state : InputState) : boolean | undefined\n {\n // Check if the state is from the correct device type\n if(state.type !== 'keyboard')\n {\n return undefined;\n }\n\n const keyboardState = state as KeyboardInputState;\n\n // Get value from either the current state or delta state based on configuration\n if(this.useDelta)\n {\n return keyboardState.delta[this.sourceKey];\n }\n else\n {\n return keyboardState.keys[this.sourceKey];\n }\n }\n\n /**\n * Creates a KeyboardValueReader from a string representation\n *\n * @param sourceKey - KeyboardEvent.code value (e.g. \"KeyA\", \"Space\")\n * @param options\n */\n public static fromString(sourceKey : string, options : KeyboardReaderOptions = {}) : KeyboardValueReader\n {\n return new KeyboardValueReader(sourceKey, options);\n }\n\n /**\n * Returns a JSON-serializable representation of this keyboard value reader.\n */\n public toJSON() : KeyboardValueReaderDefinition\n {\n return {\n\n type: 'keyboard',\n sourceType: this.sourceType,\n sourceKey: this.sourceKey,\n options: {\n useDelta: this.useDelta,\n },\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Mouse Value Reader\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { DeviceValueReader, InputState, MouseValueReaderDefinition } from '../../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Types of mouse input sources\n */\nexport type MouseSourceType = 'button' | 'position' | 'wheel';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Reads values from mouse input states\n */\nexport class MouseValueReader implements DeviceValueReader\n{\n /**\n * Which category of mouse input: button press, cursor position, or scroll wheel.\n */\n readonly sourceType : MouseSourceType;\n\n /**\n * Identifies the input within the source type (e.g. \"button-0\", \"absolute:x\", \"deltaY\")\n */\n readonly sourceKey : string;\n\n /**\n * Creates a new MouseValueReader\n *\n * @param sourceType\n * @param sourceKey - e.g. \"button-0\", \"absolute:x\", \"deltaY\"\n */\n constructor(sourceType : MouseSourceType, sourceKey : string)\n {\n this.sourceType = sourceType;\n this.sourceKey = sourceKey;\n }\n\n /**\n * Gets the value of the mouse input source. Returns undefined for non-mouse input states.\n *\n * @param state\n */\n public getValue(state : InputState) : boolean | number | undefined\n {\n // Check if the state is from the correct device type\n if(state.type !== 'mouse')\n {\n return undefined;\n }\n\n switch (this.sourceType)\n {\n case 'button':\n {\n const buttonState = state.buttons[this.sourceKey];\n return buttonState ? buttonState.pressed : undefined;\n }\n\n case 'position':\n {\n // Position keys are in format \"positionType:axis\" (e.g., \"absolute:x\")\n const [ posType, axis ] = this.sourceKey.split(':');\n\n if(!posType || !axis || (posType !== 'absolute' && posType !== 'relative')\n || (axis !== 'x' && axis !== 'y'))\n {\n return undefined;\n }\n\n return state.position[posType][axis];\n }\n\n case 'wheel':\n {\n const isValidWheelKey = this.sourceKey === 'deltaX' || this.sourceKey === 'deltaY'\n || this.sourceKey === 'deltaZ';\n if(state.wheel && isValidWheelKey)\n {\n return state.wheel[this.sourceKey];\n }\n return undefined;\n }\n\n default:\n return undefined;\n }\n }\n\n /**\n * Creates a MouseValueReader from a colon-delimited string (e.g. \"button:0\", \"position:absolute:x\")\n *\n * @param sourceTypeString\n */\n public static fromString(sourceTypeString : string) : MouseValueReader\n {\n const [ sourceType, ...keyParts ] = sourceTypeString.split(':');\n const sourceKey = keyParts.join(':');\n\n if(!sourceType || !sourceKey)\n {\n throw new Error(`Invalid mouse source format: ${ sourceTypeString }`);\n }\n\n return new MouseValueReader(\n sourceType as MouseSourceType,\n sourceKey\n );\n }\n\n /**\n * Returns a JSON-serializable representation of this mouse value reader.\n */\n public toJSON() : MouseValueReaderDefinition\n {\n // Always return consistent 'mouse' type with sourceType and sourceKey\n return {\n type: 'mouse',\n sourceType: this.sourceType,\n sourceKey: this.sourceKey,\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Gamepad Value Reader\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { DeviceValueReader, GamepadValueReaderDefinition, InputState } from '../../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Types of gamepad input sources\n */\nexport type GamepadSourceType = 'button' | 'axis';\n\n/**\n * Options for configuring a gamepad value reader\n */\nexport interface GamepadReaderOptions\n{\n /**\n * Whether to use the analog value instead of boolean pressed state for buttons\n */\n useAnalogValue ?: boolean;\n\n /**\n * Deadzone value (0.0 - 1.0) for analog axes\n * Input values below this threshold will be treated as 0\n */\n deadzone ?: number;\n\n /**\n * Whether to invert the axis values\n */\n invert ?: boolean;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Reads values from gamepad input states\n */\nexport class GamepadValueReader implements DeviceValueReader\n{\n /**\n * Whether this reader targets a button or analog axis.\n */\n readonly sourceType : GamepadSourceType;\n\n /**\n * Identifies the specific button or axis (e.g. \"button-0\", \"axis-1\")\n */\n readonly sourceKey : string;\n\n /**\n * When true, returns the pressure-sensitive float value instead of boolean pressed state for buttons.\n */\n private readonly useAnalogValue : boolean;\n\n /**\n * Axis values below this threshold are treated as 0 (default 10%)\n */\n private readonly deadzone : number;\n\n /**\n * Multiplies axis values by -1 when true\n */\n private readonly invert : boolean;\n\n /**\n * Creates a new GamepadValueReader\n *\n * @param sourceType\n * @param sourceKey - e.g. \"button-0\", \"axis-1\"\n * @param options\n */\n constructor(sourceType : GamepadSourceType, sourceKey : string, options : GamepadReaderOptions = {})\n {\n this.sourceType = sourceType;\n this.sourceKey = sourceKey;\n this.useAnalogValue = options.useAnalogValue || false;\n this.deadzone = options.deadzone || 0.1; // Default 10% deadzone\n this.invert = options.invert || false;\n }\n\n /**\n * Gets the value of the input source. Returns undefined for non-gamepad input states.\n *\n * @param state\n */\n public getValue(state : InputState) : boolean | number | undefined\n {\n // Check if the state is from the correct device type\n if(state.type !== 'gamepad')\n {\n return undefined;\n }\n\n switch (this.sourceType)\n {\n case 'button':\n {\n const buttonState = state.buttons[this.sourceKey];\n\n if(!buttonState)\n {\n return undefined;\n }\n\n return this.useAnalogValue ? buttonState.value : buttonState.pressed;\n }\n\n case 'axis':\n {\n let value = state.axes[this.sourceKey];\n\n if(value === undefined)\n {\n return undefined;\n }\n\n // Apply deadzone\n if(Math.abs(value) < this.deadzone)\n {\n value = 0;\n }\n\n // Apply inversion if needed\n return this.invert ? -value : value;\n }\n\n default:\n return undefined;\n }\n }\n\n /**\n * Creates a GamepadValueReader from a colon-delimited string (e.g. \"button:0\", \"axis:1\")\n *\n * @param sourceTypeString\n * @param options\n */\n public static fromString(sourceTypeString : string, options : GamepadReaderOptions = {}) : GamepadValueReader\n {\n const [ sourceType, sourceKey ] = sourceTypeString.split(':');\n\n if(!sourceType || !sourceKey)\n {\n throw new Error(`Invalid gamepad source format: ${ sourceTypeString }`);\n }\n\n return new GamepadValueReader(\n sourceType as GamepadSourceType,\n sourceKey,\n options\n );\n }\n\n /**\n * Returns a JSON-serializable representation of this gamepad value reader.\n */\n public toJSON() : GamepadValueReaderDefinition\n {\n return {\n type: 'gamepad',\n sourceType: this.sourceType,\n sourceKey: this.sourceKey,\n options: {\n useAnalogValue: this.useAnalogValue,\n deadzone: this.deadzone,\n invert: this.invert,\n },\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Binding Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { GameEventBus } from '../classes/eventBus.ts';\n\n// Interfaces\nimport {\n type Binding,\n type BindingDefinition,\n type CaptureInputOptions,\n type Context,\n type InputDefinition,\n bindingTypes,\n} from '../interfaces/binding.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport {\n type DeviceType,\n type DeviceValueReader,\n type DeviceValueReaderDefinition,\n type InputDevice,\n type InputState,\n isGamepadState,\n isKeyboardState,\n isMouseState,\n} from '../interfaces/input.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type { Action } from '../interfaces/action.ts';\n\n// Bindings\nimport { TriggerBinding } from '../classes/bindings/trigger.ts';\nimport { ToggleBinding } from '../classes/bindings/toggle.ts';\nimport { ValueBinding } from '../classes/bindings/value.ts';\n\n// Value Readers\nimport { KeyboardValueReader } from '../classes/input/readers/keyboard.ts';\nimport { MouseValueReader } from '../classes/input/readers/mouse.ts';\nimport { GamepadValueReader } from '../classes/input/readers/gamepad.ts';\n\n// Utils\nimport { LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Configuration object structure for binding manager\n */\nexport interface BindingConfiguration\n{\n /** All registered actions */\n actions : Action[];\n\n /** All registered bindings */\n bindings : BindingDefinition[];\n\n /** All registered contexts */\n contexts : {\n name : string;\n exclusive : boolean;\n active : boolean;\n }[];\n}\n\n/**\n * Minimum axis magnitude for a gamepad event to count as intentional activity.\n * This is separate from per-binding deadzones — it just prevents idle stick drift\n * from flipping the last-active device type.\n */\nconst GAMEPAD_ACTIVITY_DEADZONE = 0.05;\n\n/**\n * Minimum axis magnitude for a gamepad event to count as intentional during captureInput().\n * Much higher than the activity deadzone to avoid false positives from stick drift.\n */\nconst CAPTURE_AXIS_DEADZONE = 0.5;\n\n/**\n * Internal state tracked while a captureInput() call is in progress.\n */\ninterface CaptureState\n{\n resolve : (result : InputDefinition) => void;\n reject : (reason : unknown) => void;\n options : CaptureInputOptions;\n snapshot : string[];\n abortCleanup ?: () => void;\n}\n\n/* eslint-disable no-continue */\n\n/**\n * Manages input bindings and actions for the game\n */\nexport class BindingManager implements Disposable\n{\n /** Map of device IDs to their bindings */\n private _bindings = new Map<string, Binding[]>();\n\n /** Map of action names to their action definitions */\n private _actions = new Map<string, Action>();\n\n /** Map of all registered contexts by name for O(1) lookup */\n private _contexts = new Map<string, Context>();\n\n /**\n * Set of all active contexts (both exclusive and non-exclusive)\n */\n private _activeContexts = new Set<string>();\n\n /** The most recent device type that produced intentional input */\n private _lastActiveDeviceType : DeviceType | null = null;\n\n /** Last input state per device, used to prime edge state on context activation */\n private _lastInputState = new Map<string, InputState>();\n\n /** Active captureInput() state, or null when not capturing */\n private _captureState : CaptureState | null = null;\n\n /** Event bus for handling game events */\n private _eventBus : GameEventBus;\n\n /** Unsubscribe function for input events */\n private _inputUnsubscribe : (() => void) | null = null;\n\n /** Logger instance */\n private _log : LoggerInterface;\n\n /**\n * Creates an instance of BindingManager.\n * Automatically subscribes to 'input:changed' events on the event bus.\n *\n * @param eventBus\n * @param logger\n */\n constructor(eventBus : GameEventBus, logger ?: LoggingUtility)\n {\n this._eventBus = eventBus;\n\n // Bind to input events\n this._inputUnsubscribe = this._eventBus.subscribe(\n 'input:changed',\n (event) =>\n {\n const payload = event.payload as { device : InputDevice; state : InputState };\n if(payload)\n {\n this.$handleInput(payload.device, payload.state);\n }\n }\n );\n\n this._log = logger?.getLogger('BindingManager') || new SAGELogger('BindingManager');\n this._log.debug('BindingManager initialized');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Checks if a binding's context is currently active.\n * Bindings with no context are always considered active.\n *\n * @param binding\n */\n private _isBindingContextActive(binding : Binding) : boolean\n {\n // If no context is specified, binding is always active regardless of active contexts\n if(!binding.context)\n {\n return true;\n }\n\n // Otherwise check if the binding's specific context is active\n return this._activeContexts.has(binding.context);\n }\n\n /**\n * Get a context by name, creating it if it doesn't exist.\n *\n * @param contextName\n * @param exclusive - Only used when creating a new context\n */\n private _getOrCreateContext(contextName : string, exclusive = true) : Context\n {\n let context = this._contexts.get(contextName);\n\n if(!context)\n {\n context = {\n name: contextName,\n exclusive,\n };\n\n this._contexts.set(contextName, context);\n this._log.debug(`Auto-created context \"${ contextName }\" (exclusive: ${ exclusive })`);\n }\n\n return context;\n }\n\n /**\n * Deactivate all exclusive contexts except the specified one.\n * Non-exclusive contexts are left untouched.\n *\n * @param exceptContextName\n */\n private _deactivateExclusiveContexts(exceptContextName ?: string) : string[]\n {\n const deactivatedContexts : string[] = [];\n\n // Find all active exclusive contexts\n for(const contextName of this._activeContexts)\n {\n // Skip the context we want to keep\n if(contextName === exceptContextName)\n {\n continue;\n }\n\n const context = this._contexts.get(contextName);\n\n // If this is an exclusive context, deactivate it\n if(context?.exclusive)\n {\n this._activeContexts.delete(contextName);\n deactivatedContexts.push(contextName);\n }\n }\n\n return deactivatedContexts;\n }\n\n /**\n * Determines whether an input event represents intentional activity that should\n * update the last-active device type. Filters out noise like idle mousemove.\n *\n * @param state\n */\n private _shouldUpdateActiveDevice(state : InputState) : boolean\n {\n if(isMouseState(state))\n {\n // Only clicks and wheel count — mousemove alone is noise\n return Object.values(state.buttons).some((btn) => btn.pressed) || state.wheel !== undefined;\n }\n\n if(isGamepadState(state))\n {\n // Any button pressed or any axis past the activity deadzone\n return Object.values(state.buttons).some((btn) => btn.pressed)\n || Object.values(state.axes).some((val) => Math.abs(val) > GAMEPAD_ACTIVITY_DEADZONE);\n }\n\n // Keyboard events are always intentional\n return true;\n }\n\n /**\n * Create a binding from a binding definition.\n *\n * @param definition\n * @returns Null if the action is not registered or the binding type is unsupported\n */\n private _createBindingFromDefinition(definition : BindingDefinition) : Binding | null\n {\n // Get the full action object from the action name\n const action = this._actions.get(definition.action);\n\n // If the action doesn't exist, log a warning and return null\n if(!action)\n {\n this._log.warn(`Cannot create binding: Action \"${ definition.action }\" not found.`);\n return null;\n }\n\n // Extract deviceID and create reader from the input definition\n const { deviceID, ...readerDef } = definition.input;\n\n switch (definition.type)\n {\n case 'trigger': {\n // Create the binding with the device value reader based on definition's source\n return new TriggerBinding(\n action,\n deviceID,\n definition.input.type,\n this._createInputSourceFromDefinition(readerDef),\n {\n ...(definition.options || {}),\n context: definition.context,\n }\n );\n }\n\n case 'toggle': {\n // Create the binding with the device value reader based on definition's source\n return new ToggleBinding(\n action,\n deviceID,\n definition.input.type,\n this._createInputSourceFromDefinition(readerDef),\n {\n ...(definition.options || {}),\n context: definition.context,\n }\n );\n }\n\n case 'value': {\n // Create the binding with the device value reader based on definition's source\n return new ValueBinding(\n action,\n deviceID,\n definition.input.type,\n this._createInputSourceFromDefinition(readerDef),\n {\n ...(definition.options || {}),\n context: definition.context,\n }\n );\n }\n\n default:\n this._log.error(`Binding type not implemented: ${ (definition as BindingDefinition).type }`);\n return null;\n }\n }\n\n /**\n * Create a device value reader from a definition object.\n *\n * @param definition\n * @throws Error if the reader type is not supported\n */\n private _createInputSourceFromDefinition(definition : DeviceValueReaderDefinition) : DeviceValueReader\n {\n // Create reader based on device type\n switch (definition.type)\n {\n case 'keyboard':\n return new KeyboardValueReader(\n definition.sourceKey,\n definition.options\n );\n\n case 'mouse':\n {\n // Check if sourceType is valid\n const sourceType = definition.sourceType;\n if(!(sourceType === 'button' || sourceType === 'position' || sourceType === 'wheel'))\n {\n throw new Error(`Invalid mouse source type: ${ sourceType }`);\n }\n\n return new MouseValueReader(\n sourceType,\n definition.sourceKey\n );\n }\n\n case 'gamepad':\n {\n // Check if sourceType is valid\n const sourceType = definition.sourceType;\n if(!(sourceType === 'button' || sourceType === 'axis'))\n {\n throw new Error(`Invalid gamepad source type: ${ sourceType }`);\n }\n return new GamepadValueReader(\n sourceType,\n definition.sourceKey,\n definition.options\n );\n }\n\n default:\n throw new Error(`Unsupported input source type: ${ (definition as DeviceValueReaderDefinition).type }`);\n }\n }\n\n /**\n * Evaluates an input event during capture mode. If the event matches the capture criteria,\n * resolves the capture promise and cleans up. Otherwise does nothing and waits for the next event.\n *\n * @param device\n * @param state\n */\n private _processCaptureInput(device : InputDevice, state : InputState) : void\n {\n const capture = this._captureState;\n if(!capture)\n {\n return;\n }\n\n const { options } = capture;\n\n // Filter by device type\n if(!options.deviceTypes.includes(device.type))\n {\n return;\n }\n\n let result : InputDefinition | null = null;\n\n if(isKeyboardState(state))\n {\n // Only capture key-down, ignore key repeats\n if(state.event?.repeat === true)\n {\n return;\n }\n\n // Find the first key that transitioned to pressed\n for(const key of Object.keys(state.delta))\n {\n if(state.delta[key] === true)\n {\n result = { type: 'keyboard', sourceType: 'key', sourceKey: key, deviceID: device.id };\n break;\n }\n }\n }\n else if(isMouseState(state))\n {\n // Only capture button presses, ignore position and wheel\n for(const key of Object.keys(state.buttons))\n {\n if(state.buttons[key].pressed)\n {\n result = { type: 'mouse', sourceType: 'button', sourceKey: key, deviceID: device.id };\n break;\n }\n }\n }\n else if(isGamepadState(state))\n {\n // Check buttons first\n for(const key of Object.keys(state.buttons))\n {\n if(state.buttons[key].pressed)\n {\n result = { type: 'gamepad', sourceType: 'button', sourceKey: key, deviceID: device.id };\n break;\n }\n }\n\n // Then check axes if no button found\n if(!result)\n {\n for(const key of Object.keys(state.axes))\n {\n if(Math.abs(state.axes[key]) > CAPTURE_AXIS_DEADZONE)\n {\n result = { type: 'gamepad', sourceType: 'axis', sourceKey: key, deviceID: device.id };\n break;\n }\n }\n }\n }\n\n // No intentional input detected\n if(!result)\n {\n return;\n }\n\n // Apply sourceTypes filter — if it doesn't match, keep waiting\n if(options.sourceTypes\n && !options.sourceTypes.includes(result.sourceType as typeof options.sourceTypes[number])\n )\n {\n return;\n }\n\n // We have a match — resolve and clean up\n const { resolve } = capture;\n this._cleanupCapture();\n resolve(result);\n }\n\n /**\n * Tears down capture state, deactivates the temporary context, and restores the\n * snapshotted contexts. Does not resolve or reject — the caller handles that.\n */\n private _cleanupCapture() : void\n {\n const capture = this._captureState;\n if(!capture)\n {\n return;\n }\n\n // Clean up abort listener\n if(capture.abortCleanup)\n {\n capture.abortCleanup();\n }\n\n const { snapshot } = capture;\n this._captureState = null;\n\n // Remove the temporary context entirely and restore previous state\n this.deactivateContext('__sage_capture__');\n this._contexts.delete('__sage_capture__');\n\n for(const contextName of snapshot)\n {\n this.activateContext(contextName);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Internal API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Handle input from a device and process all relevant bindings.\n *\n * **Internal API**: This method is prefixed with `$` to indicate it is intended for internal\n * engine use only. While it is public for testing purposes, external code should not call\n * this method directly. Input handling is triggered automatically via the event bus.\n *\n * @param device - The input device\n * @param state - Current input state\n *\n * @internal\n */\n public $handleInput(device : InputDevice, state : InputState) : void\n {\n // Snapshot input state so context activations can prime edge state from physical reality\n this._lastInputState.set(device.id, state);\n\n // Track last-active device type before processing bindings.\n // This runs even if there are no bindings for this device.\n if(this._shouldUpdateActiveDevice(state) && device.type !== this._lastActiveDeviceType)\n {\n const previousDeviceType = this._lastActiveDeviceType;\n this._lastActiveDeviceType = device.type;\n this._eventBus.publish({\n type: 'input:device-type:changed',\n payload: { deviceType: device.type, previousDeviceType },\n });\n }\n\n // If capturing, route to the capture handler and suppress all normal binding processing\n if(this._captureState)\n {\n this._processCaptureInput(device, state);\n return;\n }\n\n const bindings = this._bindings.get(device.id);\n if(!bindings || bindings.length === 0)\n {\n return;\n }\n\n if(this._activeContexts.size === 0 && bindings.some((binding) => binding.context))\n {\n return;\n }\n\n for(const binding of bindings)\n {\n if(!this._isBindingContextActive(binding))\n {\n continue;\n }\n\n binding.process(state, this._eventBus);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Action Management API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Registers an action.\n *\n * @param action\n * @throws Error if action is already registered\n */\n public registerAction(action : Action) : void\n {\n this._log.debug(`Registering action \"${ action.name }\"`);\n\n if(this._actions.has(action.name))\n {\n const errorMsg = `Action \"${ action.name }\" already registered.`;\n this._log.error(errorMsg);\n throw new Error(errorMsg);\n }\n\n this._actions.set(action.name, action);\n this._log.debug(`Action \"${ action.name }\" registered successfully`);\n }\n\n /**\n * Gets an action by name.\n *\n * @param actionName\n */\n public getAction(actionName : string) : Action | null\n {\n this._log.trace(`Getting action \"${ actionName }\"`);\n\n const action = this._actions.get(actionName) ?? null;\n if(!action)\n {\n this._log.debug(`Action \"${ actionName }\" not found`);\n }\n\n return action;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Device Tracking API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Returns the device type that most recently produced intentional input,\n * or `null` if no input has been received yet.\n */\n public get lastActiveDeviceType() : DeviceType | null\n {\n return this._lastActiveDeviceType;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Input Capture API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Waits for the next intentional input event and returns a descriptor suitable for constructing\n * a binding. While capturing, all normal bindings are suppressed via a temporary exclusive context.\n * Previous context state is restored when the capture completes or is cancelled.\n *\n * @param options\n * @throws Error if a capture is already in progress\n */\n public captureInput(options : CaptureInputOptions) : Promise<InputDefinition>\n {\n if(this._captureState)\n {\n throw new Error('captureInput() already in progress');\n }\n\n // Snapshot current active contexts before suppression\n const snapshot = [ ...this._activeContexts ];\n\n // Push a temporary exclusive context to suppress all normal bindings\n this.registerContext('__sage_capture__', true);\n this.activateContext('__sage_capture__');\n\n return new Promise<InputDefinition>((resolve, reject) =>\n {\n const captureState : CaptureState = { resolve, reject, options, snapshot };\n this._captureState = captureState;\n\n // Wire up abort signal if provided\n if(options.signal)\n {\n if(options.signal.aborted)\n {\n this._cleanupCapture();\n reject(options.signal.reason ?? new Error('captureInput() aborted'));\n return;\n }\n\n const onAbort = () : void =>\n {\n const reason = options.signal?.reason ?? new Error('captureInput() aborted');\n this._cleanupCapture();\n reject(reason);\n };\n\n options.signal.addEventListener('abort', onAbort, { once: true });\n\n captureState.abortCleanup = () =>\n {\n options.signal?.removeEventListener('abort', onAbort);\n };\n }\n });\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Context Management API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Registers a context with specific options.\n * Updates exclusivity if the context already exists.\n *\n * @param contextName\n * @param exclusive\n */\n public registerContext(contextName : string, exclusive = true) : Context\n {\n const context = this._getOrCreateContext(contextName, exclusive);\n\n // Update context's exclusivity if it already exists\n if(context.exclusive !== exclusive)\n {\n context.exclusive = exclusive;\n this._log.info(`Updated context \"${ contextName }\" exclusivity: ${ exclusive }`);\n }\n\n return context;\n }\n\n /**\n * Activates a context, enabling all bindings associated with it.\n * If the context is exclusive, all other exclusive contexts will be deactivated.\n *\n * @param contextName\n */\n public activateContext(contextName : string) : void\n {\n // Get or create context\n const context = this._getOrCreateContext(contextName);\n\n // Get the context's exclusivity setting\n const isExclusive = context.exclusive;\n\n this._log.debug(`Activating context \"${ contextName }\" (exclusive: ${ isExclusive })`);\n\n // If already active, nothing to do unless we're making an exclusive activation\n const isAlreadyActive = this._activeContexts.has(contextName);\n\n if(isExclusive)\n {\n // Deactivate all other exclusive contexts\n const deactivated = this._deactivateExclusiveContexts(contextName);\n\n if(deactivated.length > 0)\n {\n this._log.info(`Deactivated exclusive contexts: ${ deactivated.join(', ') }`);\n }\n }\n\n // Add to active contexts if not already there\n if(!isAlreadyActive)\n {\n this._activeContexts.add(contextName);\n\n // Prime edge state on all bindings in this context from the last known physical input.\n // This prevents already-held inputs from producing phantom edges in the new context while\n // still allowing genuinely new inputs to fire.\n for(const [ deviceId, deviceBindings ] of this._bindings.entries())\n {\n const lastState = this._lastInputState.get(deviceId);\n for(const binding of deviceBindings)\n {\n if(binding.context === contextName)\n {\n binding.resetEdgeState(lastState);\n }\n }\n }\n\n this._log.info(`Context \"${ contextName }\" activated${ isExclusive ? ' as exclusive' : '' }`);\n }\n }\n\n /**\n * Deactivates a context, disabling all bindings associated with it.\n *\n * @param contextName\n */\n public deactivateContext(contextName : string) : void\n {\n this._log.debug(`Deactivating context \"${ contextName }\"`);\n\n if(this._activeContexts.has(contextName))\n {\n this._activeContexts.delete(contextName);\n this._log.info(`Context \"${ contextName }\" deactivated`);\n }\n else\n {\n this._log.debug(`Context \"${ contextName }\" was not active`);\n }\n }\n\n /**\n * Returns a list of all active contexts.\n */\n public getActiveContexts() : string[]\n {\n return [ ...this._activeContexts ];\n }\n\n /**\n * Returns whether a context is active.\n *\n * @param contextName\n */\n public isContextActive(contextName : string) : boolean\n {\n return this._activeContexts.has(contextName);\n }\n\n /**\n * Gets a context by name.\n *\n * @param contextName\n */\n public getContext(contextName : string) : Context | null\n {\n return this._contexts.get(contextName) || null;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Binding Management API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a binding for an input device.\n * Auto-registers the binding's context if not already known.\n *\n * @param binding\n * @throws Error if binding type is invalid\n */\n public $registerBinding(binding : Binding) : void\n {\n if(!bindingTypes.includes(binding.type))\n {\n throw new Error(`Invalid binding type: ${ binding.type }`);\n }\n\n // If binding has a context but context isn't registered yet, register it with default settings\n if(binding.context && !this._contexts.has(binding.context))\n {\n this.registerContext(binding.context);\n }\n\n // Initialize device bindings array if it doesn't exist\n if(!this._bindings.has(binding.deviceID))\n {\n this._bindings.set(binding.deviceID, []);\n }\n\n // Add the binding\n this._bindings.get(binding.deviceID)?.push(binding);\n this._log.debug(`Registered ${ binding.type } binding for \"${ binding.action.name }\" in context `\n + `\"${ binding.context || null }\"`);\n }\n\n /**\n * Register a binding using a binding definition object.\n * Creates the appropriate Binding subclass and registers it.\n *\n * @param definition\n */\n public registerBinding(definition : BindingDefinition) : void\n {\n const binding = this._createBindingFromDefinition(definition);\n\n if(binding)\n {\n this.$registerBinding(binding);\n }\n else\n {\n this._log.error(`Failed to create binding for action \"${ definition.action }\" with type `\n + `\"${ definition.type }\"`);\n }\n }\n\n /**\n * Unregister all bindings for an action within a context.\n *\n * @param actionName\n * @param context - Pass null (default) to target context-free bindings\n */\n public unregisterBindings(actionName : string, context : string | null = null) : void\n {\n this._log.debug(`Unregistering all bindings for action \"${ actionName }\" in context \"${ context }\"`);\n\n let bindingsRemoved = 0;\n\n // Iterate through all devices and their bindings\n for(const [ deviceId, deviceBindings ] of this._bindings.entries())\n {\n const newBindings = deviceBindings.filter((binding) =>\n {\n const bindingContext = binding.context || null;\n const shouldKeep = binding.action.name !== actionName || bindingContext !== context;\n if(!shouldKeep)\n {\n bindingsRemoved++;\n }\n return shouldKeep;\n });\n\n // Only update the map if we actually removed something\n if(newBindings.length !== deviceBindings.length)\n {\n this._bindings.set(deviceId, newBindings);\n }\n }\n\n this._log.info(`Removed ${ bindingsRemoved } bindings for action \"${ actionName }\" in context \"${ context }\"`);\n }\n\n /**\n * Gets all bindings for a specific action, optionally filtered by device type and/or context.\n *\n * @param actionName\n * @param deviceType - If provided, filters to only bindings for that device type\n * @param context - If provided, filters to only bindings in that context\n */\n public getBindingsForAction(actionName : string, deviceType ?: DeviceType, context ?: string | null) : Binding[]\n {\n const result : Binding[] = [];\n\n for(const deviceBindings of this._bindings.values())\n {\n for(const binding of deviceBindings)\n {\n if(binding.action.name !== actionName) { continue; }\n if(deviceType !== undefined && binding.deviceType !== deviceType) { continue; }\n\n const bindingContext = binding.context || null;\n if(context !== undefined && bindingContext !== context) { continue; }\n\n result.push(binding);\n }\n }\n\n return result;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Configuration Management API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Exports the current configuration to a serializable object.\n * Includes all actions, bindings, contexts, and their active states.\n */\n public exportConfiguration() : BindingConfiguration\n {\n this._log.debug('Exporting binding configuration');\n\n // Export all registered actions\n const actions = [ ...this._actions.values() ].map((action) =>\n {\n // Ensure we include all analog action properties when exporting\n if(action.type === 'analog')\n {\n return {\n name: action.name,\n type: action.type,\n label: action.label,\n minValue: action.minValue ?? 0,\n maxValue: action.maxValue ?? 1,\n };\n }\n return action;\n });\n\n // Export all registered bindings using their toJSON methods\n const bindings : BindingDefinition[] = [];\n\n // Iterate through all device bindings\n for(const deviceBindings of this._bindings.values())\n {\n for(const binding of deviceBindings)\n {\n // Let the binding serialize itself to the BindingDefinition format\n bindings.push(binding.toJSON());\n }\n }\n\n // Export all contexts with their active state\n const contexts = [ ...this._contexts.values() ].map((context) => ({\n name: context.name,\n exclusive: context.exclusive,\n active: this._activeContexts.has(context.name),\n }));\n\n this._log.info(`Configuration exported: ${ actions.length } actions, ${ bindings.length } bindings, `\n + `${ contexts.length } contexts`);\n\n return {\n actions,\n bindings,\n contexts,\n };\n }\n\n /**\n * Imports a configuration, replacing all current actions, bindings, and contexts.\n *\n * @param config\n */\n public importConfiguration(config : BindingConfiguration) : void\n {\n this._log.debug('Importing binding configuration');\n\n // Clear existing configuration\n this._bindings.clear();\n this._actions.clear();\n this._contexts.clear();\n this._activeContexts.clear();\n\n // Import actions\n for(const action of config.actions)\n {\n this.registerAction(action);\n }\n\n // Import contexts first so they exist before bindings reference them\n for(const contextData of config.contexts)\n {\n this.registerContext(contextData.name, contextData.exclusive);\n\n // Activate contexts that were active\n if(contextData.active)\n {\n this.activateContext(contextData.name);\n }\n }\n\n // Import bindings\n for(const bindingDef of config.bindings)\n {\n try\n {\n this.registerBinding(bindingDef);\n }\n catch (err : unknown)\n {\n if(err instanceof Error)\n {\n this._log.error(`Failed to import binding for action \"${ bindingDef.action }\": ${ err.message }`);\n }\n }\n }\n\n this._log.info(`Configuration imported: ${ config.actions.length } actions, `\n + `${ config.bindings.length } bindings, ${ config.contexts.length } contexts`);\n }\n\n /**\n * Tears down the binding manager and cleans up any resources\n */\n async $teardown() : Promise<void>\n {\n this._log.info('Tearing down BindingManager');\n\n // Abort any active capture before clearing state\n if(this._captureState)\n {\n const { reject } = this._captureState;\n this._cleanupCapture();\n reject(new Error('BindingManager torn down during captureInput()'));\n }\n\n // Unsubscribe from input events\n if(this._inputUnsubscribe)\n {\n this._inputUnsubscribe();\n this._inputUnsubscribe = null;\n }\n\n // Clear all bindings and cached input state\n this._bindings.clear();\n this._lastInputState.clear();\n\n // Clear all actions\n this._actions.clear();\n\n // Clear all contexts\n this._contexts.clear();\n this._activeContexts.clear();\n\n // Reset device tracking\n this._lastActiveDeviceType = null;\n\n this._log.info('BindingManager torn down successfully');\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Utilities to test for capability of the current environment\n//----------------------------------------------------------------------------------------------------------------------\n\nexport function isBrowser() : boolean\n{\n return typeof window !== 'undefined' && typeof window.document !== 'undefined';\n}\n\nexport function hasWebGPU() : boolean\n{\n return isBrowser()\n && !!window.navigator.gpu;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Game Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { AbstractEngine, Scene } from '@babylonjs/core';\n\n// Interfaces\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\n\n// Classes\nimport type { GameEventBus } from '../classes/eventBus.ts';\n\n// Managers\nimport { GameEntityManager } from './entity.ts';\nimport { LevelManager } from './level.ts';\nimport { UserInputManager } from './input.ts';\n\n// Utils\nimport { isBrowser } from '../utils/capabilities.ts';\nimport { LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Callback function for frame updates.\n * Called once per frame with the delta time in seconds.\n */\nexport type FrameCallback = (deltaTime : number) => void;\n\n/**\n * Internal representation of a registered frame callback.\n */\ninterface RegisteredFrameCallback\n{\n id : number;\n callback : FrameCallback;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class GameManager implements Disposable\n{\n private _engine : AbstractEngine;\n private _eventBus : GameEventBus;\n private _entityManager : GameEntityManager;\n private _inputManager : UserInputManager;\n private _levelManager : LevelManager;\n private _log : LoggerInterface;\n private _boundRenderLoop : () => void;\n private _boundResizeHandler : () => void;\n\n /** Registered frame callbacks, executed in registration order */\n private _frameCallbacks : RegisteredFrameCallback[] = [];\n\n /** Counter for generating unique callback IDs */\n private _nextCallbackId = 0;\n\n /** Whether the game is currently paused */\n private _paused = false;\n\n public started = false;\n\n //------------------------------------------------------------------------------------------------------------------\n\n constructor(\n engine : AbstractEngine,\n eventBus : GameEventBus,\n entityManager : GameEntityManager,\n inputManager : UserInputManager,\n levelManager : LevelManager,\n logger ?: LoggingUtility\n )\n {\n this._engine = engine;\n this._eventBus = eventBus;\n this._entityManager = entityManager;\n this._inputManager = inputManager;\n this._levelManager = levelManager;\n this._log = logger?.getLogger('GameManager') || new SAGELogger('GameManager');\n\n // Store bound function references for later cleanup\n this._boundRenderLoop = this._renderLoop.bind(this);\n this._boundResizeHandler = this._resizeHandler.bind(this);\n\n if(isBrowser())\n {\n // Resize the engine on window resize\n window.addEventListener('resize', this._boundResizeHandler);\n }\n }\n\n get currentScene() : Scene | null\n {\n return this._levelManager.currentLevel?.scene ?? null;\n }\n\n /**\n * Whether the game is currently paused.\n * When paused, entity updates and frame callbacks are skipped, but rendering continues.\n */\n get isPaused() : boolean\n {\n return this._paused;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a callback to be called once per frame.\n *\n * **Advanced Usage**: This is intended for systems that need frame-by-frame updates\n * outside of the entity/behavior system. For game logic, prefer using entity behaviors\n * with their `update()` method instead.\n *\n * Callbacks are executed in registration order, after entity updates but before rendering.\n * Each callback receives the delta time in seconds (typically ~0.016 for 60fps).\n *\n * @param callback\n * @returns A function to unregister the callback\n *\n * @example\n * ```typescript\n * const unsubscribe = gameManager.registerFrameCallback((deltaTime) => {\n * // deltaTime is in seconds (e.g., 0.016 for 60fps)\n * console.log(`Frame delta: ${deltaTime}s`);\n * });\n *\n * // Later, to stop receiving callbacks:\n * unsubscribe();\n * ```\n */\n public registerFrameCallback(callback : FrameCallback) : () => void\n {\n const id = this._nextCallbackId++;\n this._frameCallbacks.push({ id, callback });\n\n this._log.debug(`Registered frame callback (id: ${ id })`);\n\n // Return unsubscribe function\n return () =>\n {\n const index = this._frameCallbacks.findIndex((cb) => cb.id === id);\n if(index !== -1)\n {\n this._frameCallbacks.splice(index, 1);\n this._log.debug(`Unregistered frame callback (id: ${ id })`);\n }\n };\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n private _renderLoop() : void\n {\n const deltaTimeMs = this._engine.getDeltaTime();\n const deltaTime = deltaTimeMs / 1000; // Convert milliseconds to seconds\n\n // Always poll input (needed to unpause)\n this._inputManager.pollGamepads();\n\n // Skip game logic updates when paused\n if(!this._paused)\n {\n // Update entities and entity manager\n this._entityManager.$frameUpdate(deltaTime);\n\n // Run registered frame callbacks\n for(const { callback } of this._frameCallbacks)\n {\n try\n {\n callback(deltaTime);\n }\n catch (err)\n {\n this._log.error('Error in frame callback:', err);\n }\n }\n }\n\n // Always render (shows frozen frame when paused)\n if(this.currentScene)\n {\n this.currentScene.render();\n }\n }\n\n private _resizeHandler() : void\n {\n if(this.started)\n {\n this._engine.resize();\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n async start() : Promise<void>\n {\n // Start the render loop\n this._engine.runRenderLoop(this._boundRenderLoop);\n\n this.started = true;\n\n this._log.info('SkewedAspect Game Engine started successfully');\n }\n\n async stop() : Promise<void>\n {\n this.started = false;\n this._engine.stopRenderLoop();\n\n this._log.info('SkewedAspect Game Engine stopped successfully');\n }\n\n /**\n * Pause the game. Entity updates and frame callbacks will be skipped,\n * but rendering continues (frozen frame). Input is still polled so\n * the game can be unpaused.\n *\n * Emits a 'game:paused' event.\n *\n * @param reason - Optional reason for pausing (included in event payload)\n */\n pause(reason ?: string) : void\n {\n if(this._paused)\n {\n this._log.debug('Game is already paused');\n return;\n }\n\n this._paused = true;\n this._log.info('Game paused', reason ? `(reason: ${ reason })` : '');\n\n this._eventBus.publish({\n type: 'game:paused',\n payload: { reason },\n });\n }\n\n /**\n * Resume the game after being paused. Entity updates and frame callbacks\n * will resume.\n *\n * Emits a 'game:resumed' event.\n *\n * @param reason - Optional reason for resuming (included in event payload)\n */\n resume(reason ?: string) : void\n {\n if(!this._paused)\n {\n this._log.debug('Game is not paused');\n return;\n }\n\n this._paused = false;\n this._log.info('Game resumed', reason ? `(reason: ${ reason })` : '');\n\n this._eventBus.publish({\n type: 'game:resumed',\n payload: { reason },\n });\n }\n\n /**\n * Tears down any resources held by the GameManager\n * This handles unregistering event listeners and any other cleanup\n */\n async $teardown() : Promise<void>\n {\n this._log.info('Tearing down GameManager');\n\n // If we're still running, stop first\n if(this.started)\n {\n await this.stop();\n }\n\n // Remove event listeners\n if(isBrowser())\n {\n window.removeEventListener('resize', this._boundResizeHandler);\n }\n\n // Clear frame callbacks\n this._frameCallbacks = [];\n\n this._log.info('GameManager torn down successfully');\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","for(var r=256,n=[];r--;)n[r]=(r+256).toString(16).substring(1);export function hexoid(r){r=r||16;var t=\"\",o=0;return function(){if(!t||256===o){for(t=\"\",o=(1+r)/2|0;o--;)t+=n[256*Math.random()|0];t=t.substring(o=0,r-2)}return t+n[o++]}}","//----------------------------------------------------------------------------------------------------------------------\n// ID Generation Utility\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { hexoid } from 'hexoid';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Fast, unique ID generator for entities.\n * Uses hexoid for high-performance ID generation (~200M IDs/sec).\n * Generates 16-character hexadecimal strings.\n *\n * @example\n * ```typescript\n * const id = generateId(); // \"8cd88e1a9b13aac0\"\n * ```\n */\nexport const generateId = hexoid(16);\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Game Entity\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { TransformNode } from '@babylonjs/core';\n\nimport type { GameEntityBehaviorConstructor } from '../interfaces/entity.ts';\nimport type { Destroyable } from '../interfaces/lifecycle.ts';\nimport { type GameEvent, GameEventBus } from './eventBus.ts';\nimport type { GameEngine } from './gameEngine.ts';\nimport { generateId } from '../utils/id.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Utility type that removes readonly from all properties.\n * Used internally to allow controlled mutation of readonly properties.\n */\ntype Mutable<T> = { -readonly [P in keyof T] : T[P] };\n\n/**\n * Result of a request sent to an entity's behaviors.\n * `success: true` means a behavior handled the request and returned a value.\n * `success: false` means no behavior handled it, or a behavior threw an error.\n */\nexport type RequestResult<T> = { success : true; value : T } | { success : false; error : string };\n\n/**\n * Narrow interface for entity-to-manager messaging delegation.\n * Avoids a circular import between entity.ts and managers/entity.ts.\n */\ninterface EntityMessenger\n{\n send(target : string, type : string, payload ?: unknown, senderID ?: string) : Promise<void>;\n request<T>(target : string, type : string, payload ?: unknown, senderID ?: string) : Promise<RequestResult<T>>;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Tracks a reference-counted event subscription so multiple behaviors sharing the same event type\n * only produce a single underlying subscription.\n */\ninterface GameEventSubscription\n{\n count : number;\n unsubscribe : () => void;\n}\n\n/**\n * Options for attaching a behavior at a specific position.\n * Only one positioning option should be specified.\n */\nexport interface AttachBehaviorOptions\n{\n /** Insert before behavior of this class */\n before ?: GameEntityBehaviorConstructor;\n /** Insert after behavior of this class */\n after ?: GameEntityBehaviorConstructor;\n /** Insert at specific index (0-based, clamped to valid range) */\n at ?: number;\n}\n\n/**\n * Minimal entity surface exposed to behaviors, preventing them from accessing internals like\n * subscription management or behavior ordering.\n */\ninterface SimpleGameEntity<EntityState extends object = object>\n{\n id : string;\n type : string;\n name ?: string;\n node ?: TransformNode;\n state : EntityState;\n eventBus : GameEventBus;\n tags : ReadonlySet<string>;\n\n send(target : string, type : string, payload ?: unknown) : Promise<void>;\n request<T>(target : string, type : string, payload ?: unknown) : Promise<RequestResult<T>>;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Base class for game entity behaviors. Subclass this to define how an entity reacts to events\n * and updates each frame. Multiple behaviors can be attached to a single entity; event processing\n * order matches the attachment order.\n */\nexport abstract class GameEntityBehavior<RequiredState extends object = object>\n{\n abstract name : string;\n abstract eventSubscriptions : string[];\n\n protected entity : SimpleGameEntity<RequiredState> | null = null;\n\n //------------------------------------------------------------------------------------------------------------------\n // Internal API\n //------------------------------------------------------------------------------------------------------------------\n\n $emit(event : GameEvent) : void\n {\n if(!this.entity)\n {\n throw new Error('Entity is not set for this behavior.');\n }\n\n event.senderID = this.entity.id;\n // Cast to allow publishing events with arbitrary payloads\n (this.entity.eventBus as unknown as GameEventBus<Record<string, unknown>>).publish(event);\n }\n\n /**\n * Emits an entity:state-changed event to notify UI or other observers of state changes.\n * Call this after making significant state changes that external systems should know about.\n *\n * @param state - Optional state to include in the event. If omitted, uses current entity state.\n * @param changes - Optional record of specific changes made (alternative to full state).\n */\n $emitStateChanged(state ?: object, changes ?: Record<string, unknown>) : void\n {\n if(!this.entity)\n {\n throw new Error('Entity is not set for this behavior.');\n }\n\n this.$emit({\n type: 'entity:state-changed',\n payload: {\n entityId: this.entity.id,\n entityType: this.entity.type,\n state: state ?? this.entity.state,\n changes,\n },\n });\n }\n\n /**\n * Sets the entity for this behavior.\n * @param entity\n */\n $setEntity(entity : SimpleGameEntity<RequiredState> | null) : void\n {\n this.entity = entity;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Implementation API\n //------------------------------------------------------------------------------------------------------------------\n\n abstract processEvent(event : GameEvent, state : RequiredState) : Promise<boolean> | boolean;\n\n /**\n * Handles a direct request sent to this entity. Return a value to respond,\n * or `undefined` to pass the request to the next behavior.\n *\n * @param event - The request event (type, payload, senderID, targetID)\n * @param state - Current entity state\n */\n processRequest?(event : GameEvent, state : RequiredState) : Promise<unknown> | unknown | undefined;\n\n update?(dt : number, state : RequiredState) : void;\n\n destroy?() : Promise<void>;\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle Hooks\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Called when the entity is attached to a scene node.\n * Override to perform initialization that requires the node (physics, spatial audio, etc.)\n *\n * The scene is accessible via `node.getScene()`.\n *\n * @param node - The TransformNode (typically a Mesh) the entity is attached to.\n * @param gameEngine - The game engine instance for accessing managers and engines.\n */\n onNodeAttached?(node : TransformNode, gameEngine : GameEngine) : void;\n\n /**\n * Called when the entity is detached from its scene node.\n * Override to clean up node-dependent resources.\n */\n onNodeDetached?() : void;\n\n /**\n * Called when the entity is recycled from an object pool.\n * Override to reset behavior-specific state that isn't covered by entity state reset.\n * Distinct from onNodeDetached — this signals a full lifecycle reset.\n *\n * @param state - The freshly reset entity state.\n */\n onReset?(state : RequiredState) : void;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Base class for game entities. Owns a set of behaviors, manages event subscriptions\n * (reference-counted per event type), and delegates lifecycle calls to its behaviors.\n */\nexport class GameEntity<EntityState extends object = object> implements SimpleGameEntity<EntityState>, Destroyable\n{\n /** The unique identifier of the entity (16-char hex string). */\n public readonly id : string;\n\n public readonly type : string;\n\n /** Optional human-readable name for the entity. */\n public readonly name ?: string;\n\n /**\n * The scene node this entity is attached to (if any).\n * When attached, the entity controls this node's behavior.\n * Use EntityManager.attachToNode() / detachFromNode() to modify.\n */\n public readonly node ?: TransformNode;\n\n /** Child entities in the hierarchy. */\n public readonly children : GameEntity[] = [];\n\n /** Parent entity in the hierarchy, or null if this is a root entity. */\n public readonly parent : GameEntity | null = null;\n\n public state : EntityState;\n\n /** Array of behaviors attached to the entity. Order determines event processing priority. */\n public behaviors : GameEntityBehavior[] = [];\n\n public eventBus : GameEventBus;\n\n private _gameEngine : GameEngine | null = null;\n private _entityManager : EntityMessenger | null = null;\n private _tags : Set<string> = new Set<string>();\n private subscriptions : Map<string, GameEventSubscription> = new Map<string, GameEventSubscription>();\n\n /**\n * Creates an instance of GameEntity.\n * @param type\n * @param eventBus\n * @param initialState\n * @param behaviors - Attached in order; order determines event processing priority.\n * @param name\n */\n constructor(\n type : string,\n eventBus : GameEventBus,\n initialState : EntityState,\n behaviors : GameEntityBehaviorConstructor[],\n name ?: string\n )\n {\n this.id = generateId();\n this.type = type;\n this.name = name;\n this.state = initialState;\n this.eventBus = eventBus;\n\n // Attach behaviors\n for(const behaviorCtor of behaviors)\n {\n this.attachBehavior(new behaviorCtor());\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Tags\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Read-only access to the entity's tags.\n */\n get tags() : ReadonlySet<string>\n {\n return this._tags;\n }\n\n /**\n * Checks if the entity has a specific tag.\n * @param tag\n */\n hasTag(tag : string) : boolean\n {\n return this._tags.has(tag);\n }\n\n /**\n * Internal method for EntityManager to add a tag.\n * @param tag\n */\n $addTag(tag : string) : void\n {\n this._tags.add(tag);\n }\n\n /**\n * Internal method for EntityManager to remove a tag.\n * @param tag\n */\n $removeTag(tag : string) : boolean\n {\n return this._tags.delete(tag);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Node Attachment\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Find a direct child node by name.\n * @param name\n */\n getChildNode(name : string) : TransformNode | undefined\n {\n if(!this.node)\n {\n return undefined;\n }\n\n return this.node.getChildren(\n (child) => child.name === name,\n false\n )[0] as TransformNode | undefined;\n }\n\n /**\n * Find a descendant node by path (e.g., \"armature/hand/finger\").\n * @param path - Slash-separated path of node names.\n */\n findNode(path : string) : TransformNode | undefined\n {\n if(!this.node)\n {\n return undefined;\n }\n\n let current : TransformNode | undefined = this.node;\n\n for(const part of path.split('/'))\n {\n current = current?.getChildren(\n (child) => child.name === part,\n false\n )[0] as TransformNode | undefined;\n }\n\n return current;\n }\n\n /**\n * Internal method for EntityManager to attach entity to a node.\n * Do not call directly - use EntityManager.attachToNode() instead.\n * @param node\n * @param gameEngine\n * @internal\n */\n $attachToNode(node : TransformNode, gameEngine : GameEngine) : void\n {\n (this as Mutable<GameEntity>).node = node;\n this._gameEngine = gameEngine;\n\n // Notify behaviors of node attachment\n for(const behavior of this.behaviors)\n {\n behavior.onNodeAttached?.(node, gameEngine);\n }\n\n // Parent any children's nodes under this node\n for(const child of this.children)\n {\n if(child.node)\n {\n (child.node as { parent : unknown }).parent = node;\n }\n }\n }\n\n /**\n * Internal method for EntityManager to detach entity from its node.\n * Do not call directly - use EntityManager.detachFromNode() instead.\n * @internal\n */\n $detachFromNode() : void\n {\n // Remove from all outline layers before detaching\n this.unhighlight();\n\n // Detach children's nodes before detaching self\n for(const child of this.children)\n {\n if(child.node)\n {\n (child.node as { parent : unknown }).parent = null;\n }\n }\n\n // Notify behaviors before detaching\n for(const behavior of this.behaviors)\n {\n behavior.onNodeDetached?.();\n }\n\n (this as Mutable<GameEntity>).node = undefined;\n this._gameEngine = null;\n }\n\n /**\n * Add this entity to a named outline layer for visual highlighting.\n * Requires the entity to be attached to a node in a level with outlines configured.\n *\n * @param layerName - The name of the outline layer (must exist on the current level)\n */\n highlight(layerName : string) : void\n {\n const outlines = this._gameEngine?.managers?.levelManager?.currentLevel?.outlines;\n if(outlines)\n {\n outlines.highlightEntity(layerName, this);\n }\n }\n\n /**\n * Remove this entity from an outline layer, or from all layers if no name given.\n *\n * @param layerName - Specific layer to remove from, or omit to remove from all\n */\n unhighlight(layerName ?: string) : void\n {\n const outlines = this._gameEngine?.managers?.levelManager?.currentLevel?.outlines;\n if(outlines)\n {\n if(layerName)\n {\n outlines.unhighlightEntity(layerName, this);\n }\n else\n {\n outlines.unhighlightEntityAll(this);\n }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Child Hierarchy\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Adds a child entity to this entity's hierarchy.\n * Sets the child's parent to this entity. If both entities have nodes, parents the child's node.\n * @param child\n */\n $addChild(child : GameEntity) : void\n {\n this.children.push(child);\n (child as Mutable<GameEntity>).parent = this;\n\n // If both have nodes, parent the child's node under ours\n if(this.node && child.node)\n {\n (child.node as { parent : unknown }).parent = this.node;\n }\n }\n\n /**\n * Removes a child entity from this entity's hierarchy.\n * Clears the child's parent reference. If the child has a node, detaches it.\n * @param child\n */\n $removeChild(child : GameEntity) : void\n {\n const index = this.children.indexOf(child);\n if(index !== -1)\n {\n this.children.splice(index, 1);\n (child as Mutable<GameEntity>).parent = null;\n\n // Detach child node from parent node\n if(child.node)\n {\n (child.node as { parent : unknown }).parent = null;\n }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Messaging\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Sets the entity manager reference for cross-entity messaging.\n * Called by the EntityManager after entity creation.\n * @param manager\n * @internal\n */\n $setEntityManager(manager : EntityMessenger) : void\n {\n this._entityManager = manager;\n }\n\n /**\n * Sends a fire-and-forget event directly to a specific entity's behaviors, bypassing the event bus.\n * If the target matches this entity's own ID, dispatches locally.\n *\n * @param target - Entity ID or name\n * @param type - Event type string\n * @param payload - Optional event payload\n */\n async send(target : string, type : string, payload ?: unknown) : Promise<void>\n {\n if(target === this.id)\n {\n return this.$send(type, payload, this.id);\n }\n\n if(!this._entityManager)\n {\n throw new Error('Entity is not registered with an EntityManager.');\n }\n\n return this._entityManager.send(target, type, payload, this.id);\n }\n\n /**\n * Sends a request to a specific entity's behaviors and returns the first response.\n * If the target matches this entity's own ID, dispatches locally.\n *\n * @param target - Entity ID or name\n * @param type - Request type string\n * @param payload - Optional request payload\n */\n async request<T>(target : string, type : string, payload ?: unknown) : Promise<RequestResult<T>>\n {\n if(target === this.id)\n {\n return this.$request<T>(type, payload, this.id);\n }\n\n if(!this._entityManager)\n {\n throw new Error('Entity is not registered with an EntityManager.');\n }\n\n return this._entityManager.request<T>(target, type, payload, this.id);\n }\n\n /**\n * Sends a fire-and-forget event directly to this entity's own behaviors.\n *\n * @param type - Event type string\n * @param payload - Optional event payload\n * @internal\n */\n async $send(type : string, payload ?: unknown, senderID ?: string) : Promise<void>\n {\n await this.$processEvent({ type, targetID: this.id, senderID, payload: payload as never });\n }\n\n /**\n * Sends a request to this entity's own behaviors and returns the first response.\n *\n * @param type - Request type string\n * @param payload - Optional request payload\n * @internal\n */\n async $request<T>(type : string, payload ?: unknown, senderID ?: string) : Promise<RequestResult<T>>\n {\n const event : GameEvent = { type, targetID: this.id, senderID, payload: payload as never };\n let lastError : string | undefined;\n\n for(const behavior of this.behaviors)\n {\n try\n {\n // eslint-disable-next-line no-await-in-loop\n const result = await behavior.processRequest?.(event, this.state);\n if(result !== undefined)\n {\n return { success: true, value: result as T };\n }\n }\n catch (err : unknown)\n {\n // Record the error but keep trying subsequent behaviors\n lastError = err instanceof Error ? err.message : String(err);\n }\n }\n\n return {\n success: false,\n error: lastError ?? `No handler for request \"${ type }\" on entity \"${ this.name ?? this.id }\"`,\n };\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Internal Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Resets the entity for object pool recycling.\n * Restores state and tags to defaults, detaches from node, notifies behaviors, and resets children.\n * @param defaultState - Default state from the entity definition\n * @param defaultTags - Default tags from the entity definition\n */\n $reset(defaultState : object, defaultTags : string[]) : void\n {\n // Reset state to a copy of defaults\n this.state = { ...defaultState } as EntityState;\n\n // Clear all tags, then re-add defaults\n this._tags.clear();\n for(const tag of defaultTags)\n {\n this._tags.add(tag);\n }\n\n // Detach from node if attached\n if(this.node)\n {\n this.$detachFromNode();\n }\n\n // Notify behaviors\n for(const behavior of this.behaviors)\n {\n behavior.onReset?.(this.state);\n }\n\n // Recursively reset children\n for(const child of this.children)\n {\n child.$reset({}, []);\n }\n\n // Clear parent reference (the manager handles the parent side)\n (this as Mutable<GameEntity>).parent = null;\n }\n\n /**\n * Processes a game event by passing it to the entity's behaviors.\n * @param event\n */\n async $processEvent(event : GameEvent) : Promise<void>\n {\n for(const behavior of this.behaviors)\n {\n // eslint-disable-next-line no-await-in-loop\n const result = await behavior.processEvent(event, this.state);\n if(result)\n {\n // If the behavior returns true, stop other behaviors from processing the event.\n break;\n }\n }\n }\n\n /**\n * Updates the entity by calling the update method of its behaviors.\n * @param dt - Seconds elapsed since the last update.\n */\n $update(dt : number) : void\n {\n for(const behavior of this.behaviors)\n {\n behavior.update?.(dt, this.state);\n }\n\n // Cascade update to children\n for(const child of this.children)\n {\n child.$update(dt);\n }\n }\n\n /**\n * Destroys the entity by calling the destroy method of its behaviors and unsubscribing from all events.\n */\n async $destroy() : Promise<void>\n {\n // Destroy children first (iterate a copy to avoid mutation during iteration)\n for(const child of [ ...this.children ])\n {\n // eslint-disable-next-line no-await-in-loop\n await child.$destroy();\n }\n\n this.children.length = 0;\n\n for(const behavior of this.behaviors)\n {\n // eslint-disable-next-line no-await-in-loop\n await behavior.destroy?.();\n }\n\n // Unsubscribe from all event subscriptions\n for(const subscription of this.subscriptions.values())\n {\n subscription.unsubscribe();\n }\n\n this.behaviors = [];\n this.subscriptions.clear();\n }\n\n /**\n * Finds the index of a behavior by its constructor.\n * @param behaviorClass\n */\n private findBehaviorIndex(behaviorClass : GameEntityBehaviorConstructor) : number\n {\n return this.behaviors.findIndex((bh) => bh.constructor === behaviorClass);\n }\n\n /**\n * Checks if a behavior of the given class is attached.\n * @param behaviorClass\n */\n hasBehavior(behaviorClass : GameEntityBehaviorConstructor) : boolean\n {\n return this.findBehaviorIndex(behaviorClass) !== -1;\n }\n\n /**\n * Gets a behavior by its constructor.\n * @param behaviorClass\n */\n getBehavior<T extends GameEntityBehavior>(behaviorClass : new () => T) : T | undefined\n {\n return this.behaviors.find((bh) => bh.constructor === behaviorClass) as T | undefined;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Attaches a behavior to the entity.\n * Without options, appends to the end (most common case).\n * With options, inserts at the specified position.\n *\n * @param behavior\n * @param options\n * @throws If the behavior is already attached.\n * @throws If 'before' or 'after' target is not found.\n * @throws If more than one positioning option is specified.\n */\n attachBehavior(behavior : GameEntityBehavior, options ?: AttachBehaviorOptions) : void\n {\n // Check if a behavior of this class is already attached\n if(this.hasBehavior(behavior.constructor as GameEntityBehaviorConstructor))\n {\n throw new Error(`Behavior ${ behavior.constructor.name } is already attached to this entity.`);\n }\n\n // Subscribe to the behavior's event subscriptions\n for(const event of behavior.eventSubscriptions)\n {\n // Check if the event is already subscribed\n const existingSubscription = this.subscriptions.get(event);\n if(existingSubscription)\n {\n existingSubscription.count++;\n }\n else\n {\n // Create a new subscription\n // Cast eventBus to accept any payload map for entity event handling\n const bus = this.eventBus as unknown as GameEventBus<Record<string, unknown>>;\n const unsubscribe = bus.subscribe(event, this.$processEvent.bind(this));\n this.subscriptions.set(event, { count: 1, unsubscribe });\n }\n }\n\n // Determine insertion position\n if(!options)\n {\n // Common case: simple append\n this.behaviors.push(behavior);\n }\n else\n {\n // Validate: only one positioning option allowed\n const positioningOptions = [ options.before, options.after, options.at ]\n .filter((opt) => opt !== undefined);\n\n if(positioningOptions.length > 1)\n {\n throw new Error('attachBehavior: specify only one of before, after, or at');\n }\n\n if(options.before)\n {\n const index = this.findBehaviorIndex(options.before);\n if(index === -1)\n {\n throw new Error(`attachBehavior: no behavior of type ${ options.before.name } found`);\n }\n this.behaviors.splice(index, 0, behavior);\n }\n else if(options.after)\n {\n const index = this.findBehaviorIndex(options.after);\n if(index === -1)\n {\n throw new Error(`attachBehavior: no behavior of type ${ options.after.name } found`);\n }\n this.behaviors.splice(index + 1, 0, behavior);\n }\n else if(options.at !== undefined)\n {\n // Clamp to valid range rather than throwing\n const clampedIndex = Math.max(0, Math.min(options.at, this.behaviors.length));\n this.behaviors.splice(clampedIndex, 0, behavior);\n }\n else\n {\n // No positioning option specified, append\n this.behaviors.push(behavior);\n }\n }\n\n behavior.$setEntity(this);\n\n // If entity already has a node, notify the new behavior\n if(this.node && this._gameEngine)\n {\n behavior.onNodeAttached?.(this.node, this._gameEngine);\n }\n }\n\n /**\n * Detaches a behavior from the entity by its constructor.\n * @param behaviorClass\n */\n detachBehavior(behaviorClass : GameEntityBehaviorConstructor) : boolean\n {\n const index = this.findBehaviorIndex(behaviorClass);\n if(index === -1)\n {\n return false;\n }\n\n const behavior = this.behaviors[index];\n\n // Notify behavior of node detachment if entity has a node\n if(this.node)\n {\n behavior.onNodeDetached?.();\n }\n\n // Remove the behavior's event subscriptions\n for(const event of behavior.eventSubscriptions)\n {\n const subscription = this.subscriptions.get(event);\n if(subscription)\n {\n subscription.count--;\n if(subscription.count <= 0)\n {\n subscription.unsubscribe();\n this.subscriptions.delete(event);\n }\n }\n }\n\n // Remove the behavior\n this.behaviors.splice(index, 1);\n behavior.$setEntity(null);\n\n return true;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n","//----------------------------------------------------------------------------------------------------------------------\n// Entity Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { TransformNode } from '@babylonjs/core';\n\nimport { GameEntity, type RequestResult } from '../classes/entity.ts';\nimport { GameEventBus } from '../classes/eventBus.ts';\nimport type { GameEngine } from '../classes/gameEngine.ts';\n\n// Interfaces\nimport type { Action } from '../interfaces/action.ts';\nimport type { GameEntityDefinition } from '../interfaces/entity.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type { BindingManager } from '../managers/binding.ts';\n\n// Utils\nimport { LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Options for creating an entity instance.\n */\nexport interface CreateEntityOptions<State extends object = object>\n{\n /** Initial state to merge with defaultState. */\n initialState ?: Partial<State>;\n\n /** Override name from entity definition. */\n name ?: string;\n\n /** Additional tags to add (merged with definition tags). */\n tags ?: string[];\n\n /** Scene node to attach the entity to. */\n node ?: TransformNode;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class GameEntityManager implements Disposable\n{\n /** The event bus for the entity manager. */\n private eventBus : GameEventBus;\n\n /** A map of entities managed by the entity manager. */\n private entities : Map<string, GameEntity> = new Map<string, GameEntity>();\n\n /** A map of entity definitions registered with the entity manager. */\n private entityDefinitions : Map<string, GameEntityDefinition> = new Map<string, GameEntityDefinition>();\n\n /** Late-bound GameEngine reference */\n private _gameEngine : GameEngine | null = null;\n\n /** Reference to the binding manager for registering actions */\n private bindingManager : BindingManager;\n\n /** Logger instance */\n private _log : LoggerInterface;\n\n //------------------------------------------------------------------------------------------------------------------\n // Indices for fast lookups\n //------------------------------------------------------------------------------------------------------------------\n\n /** Index: name -> entity (names are not unique, first match returned by getByName) */\n private _entitiesByName : Map<string, Set<GameEntity>> = new Map<string, Set<GameEntity>>();\n\n /** Index: type -> entities */\n private _entitiesByType : Map<string, Set<GameEntity>> = new Map<string, Set<GameEntity>>();\n\n /** Index: tag -> entities */\n private _entitiesByTag : Map<string, Set<GameEntity>> = new Map<string, Set<GameEntity>>();\n\n /** Index: node -> entity (one entity per node) */\n private _entitiesByNode : Map<TransformNode, GameEntity> = new Map<TransformNode, GameEntity>();\n\n /** Object pool storage: type -> pooled entities */\n private _pools : Map<string, GameEntity[]> = new Map<string, GameEntity[]>();\n\n /**\n * Creates an instance of EntityManager.\n * @param eventBus\n * @param logger\n * @param bindingManager\n */\n constructor(eventBus : GameEventBus, logger : LoggingUtility | undefined, bindingManager : BindingManager)\n {\n this.eventBus = eventBus;\n this.bindingManager = bindingManager;\n this._log = logger?.getLogger('EntityManager') || new SAGELogger('EntityManager');\n this._log.info('EntityManager initialized');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Internal API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Late-bind the GameEngine after construction.\n * Called by createGameEngine() after the GameEngine instance is created.\n */\n $setGameEngine(gameEngine : GameEngine) : void\n {\n this._gameEngine = gameEngine;\n }\n\n private get _engine() : GameEngine\n {\n if(!this._gameEngine)\n {\n throw new Error('EntityManager: gameEngine not set. Call $setGameEngine() first.');\n }\n return this._gameEngine;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods - Index Management\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Gets or creates a Set in a Map, returning the Set.\n */\n private _getOrCreateSet<K>(map : Map<K, Set<GameEntity>>, key : K) : Set<GameEntity>\n {\n let set = map.get(key);\n if(!set)\n {\n set = new Set<GameEntity>();\n map.set(key, set);\n }\n return set;\n }\n\n /**\n * Adds an entity to all relevant indices.\n */\n private _indexEntity(entity : GameEntity) : void\n {\n // Index by type\n this._getOrCreateSet(this._entitiesByType, entity.type).add(entity);\n\n // Index by name (if present)\n if(entity.name)\n {\n this._getOrCreateSet(this._entitiesByName, entity.name).add(entity);\n }\n\n // Index by tags\n for(const tag of entity.tags)\n {\n this._getOrCreateSet(this._entitiesByTag, tag).add(entity);\n }\n\n // Index by node (if attached)\n if(entity.node)\n {\n this._entitiesByNode.set(entity.node, entity);\n }\n }\n\n /**\n * Removes an entity from all indices.\n */\n private _unindexEntity(entity : GameEntity) : void\n {\n // Remove from type index\n const typeSet = this._entitiesByType.get(entity.type);\n if(typeSet)\n {\n typeSet.delete(entity);\n if(typeSet.size === 0)\n {\n this._entitiesByType.delete(entity.type);\n }\n }\n\n // Remove from name index\n if(entity.name)\n {\n const nameSet = this._entitiesByName.get(entity.name);\n if(nameSet)\n {\n nameSet.delete(entity);\n if(nameSet.size === 0)\n {\n this._entitiesByName.delete(entity.name);\n }\n }\n }\n\n // Remove from all tag indices\n for(const tag of entity.tags)\n {\n const tagSet = this._entitiesByTag.get(tag);\n if(tagSet)\n {\n tagSet.delete(entity);\n if(tagSet.size === 0)\n {\n this._entitiesByTag.delete(tag);\n }\n }\n }\n\n // Remove from node index\n if(entity.node)\n {\n this._entitiesByNode.delete(entity.node);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods - Inheritance\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Resolves an entity definition that extends a base definition by merging according to inheritance rules.\n * The base definition must already be registered.\n */\n private _resolveInheritance(childDef : GameEntityDefinition) : GameEntityDefinition\n {\n const baseType = childDef.extends;\n if(!baseType)\n {\n return childDef;\n }\n\n const baseDef = this.entityDefinitions.get(baseType);\n if(!baseDef)\n {\n const errorMsg = `Cannot extend \"${ baseType }\": base entity definition is not registered.`;\n this._log.error(errorMsg);\n throw new Error(errorMsg);\n }\n\n // Merge tags: concatenate and deduplicate\n let mergedTags : string[] | undefined;\n if(baseDef.tags || childDef.tags)\n {\n mergedTags = [ ...new Set([ ...(baseDef.tags ?? []), ...(childDef.tags ?? []) ]) ];\n }\n\n // Build resolved definition — child overrides base for scalar/replace fields\n const resolved : GameEntityDefinition = {\n // Scalar fields: child overrides if provided, else inherit\n type: childDef.type,\n name: childDef.name ?? baseDef.name,\n mesh: childDef.mesh ?? baseDef.mesh,\n\n // Shallow-merged state\n defaultState: { ...baseDef.defaultState, ...childDef.defaultState },\n\n // Replace-entirely fields: child overrides if provided, else inherit\n behaviors: childDef.behaviors ?? baseDef.behaviors,\n actions: childDef.actions ?? baseDef.actions,\n\n // Concatenated tags\n tags: mergedTags,\n\n // Pooling: child overrides if provided, else inherit\n poolable: childDef.poolable ?? baseDef.poolable,\n poolSize: childDef.poolSize ?? baseDef.poolSize,\n\n // Children: child overrides if provided, else inherit\n children: childDef.children ?? baseDef.children,\n\n // Lifecycle hooks: child overrides if provided, else inherit\n onBeforeCreate: childDef.onBeforeCreate ?? baseDef.onBeforeCreate,\n onCreate: childDef.onCreate ?? baseDef.onCreate,\n onBeforeDestroy: childDef.onBeforeDestroy ?? baseDef.onBeforeDestroy,\n onDestroy: childDef.onDestroy ?? baseDef.onDestroy,\n };\n\n return resolved;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods - Actions\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Checks if two actions are compatible (same type and non-conflicting options).\n * @param existingAction - The action already registered\n * @param newAction - The action being registered\n */\n private _areActionsCompatible(existingAction : Action, newAction : Action) : boolean\n {\n // If types don't match, they're incompatible\n if(existingAction.type !== newAction.type)\n {\n return false;\n }\n\n // For analog actions, check min and max values\n if(existingAction.type === 'analog' && newAction.type === 'analog')\n {\n // If minValue or maxValue is specified and differs, they're incompatible\n if((newAction.minValue !== undefined && existingAction.minValue !== newAction.minValue)\n || (newAction.maxValue !== undefined && existingAction.maxValue !== newAction.maxValue))\n {\n return false;\n }\n }\n\n // Digital actions are always compatible if they have the same name\n return true;\n }\n\n /**\n * Registers actions defined in the entity definition with the binding manager.\n * Skips actions already registered with compatible options; warns on conflicts.\n * @param entityDef\n */\n private _registerEntityActions(entityDef : GameEntityDefinition) : void\n {\n if(!entityDef.actions)\n {\n return;\n }\n\n for(const action of entityDef.actions)\n {\n try\n {\n // Check if the action is already registered\n const existingAction = this.bindingManager.getAction(action.name);\n\n if(!existingAction)\n {\n // Action doesn't exist yet, register it\n this._log.debug(`Registering action \"${ action.name }\" from entity type \"${ entityDef.type }\"`);\n this.bindingManager.registerAction(action);\n }\n else if(!this._areActionsCompatible(existingAction, action))\n {\n this._log.warn(\n `Action \"${ action.name }\" already registered with different options. `\n + `Entity \"${ entityDef.type }\" requires: ${ JSON.stringify(action) }, `\n + `but found: ${ JSON.stringify(existingAction) }`\n );\n }\n else\n {\n this._log.trace(\n `Action \"${ action.name }\" already registered with compatible options, skipping registration`\n );\n }\n }\n catch (err)\n {\n this._log.debug(`Failed to register action \"${ action.name }\": `\n + `${ err instanceof Error ? err.message : String(err) }`);\n }\n }\n }\n\n $frameUpdate(dt : number) : void\n {\n this._log.trace(`Updating ${ this.entities.size } entities with dt=${ dt }`);\n for(const entity of this.entities.values())\n {\n // Only update root entities; children are cascaded via parent.$update()\n if(!entity.parent)\n {\n entity.$update(dt);\n }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Registers a new entity definition.\n * Also registers any actions declared in the definition with the binding manager.\n * @param entityDef\n */\n registerEntityDefinition(entityDef : GameEntityDefinition) : void\n {\n this._log.debug(`Registering entity definition: ${ entityDef.type }`);\n\n // Resolve inheritance if extends is specified\n const resolved = entityDef.extends\n ? this._resolveInheritance(entityDef)\n : entityDef;\n\n // Register any actions this entity requires\n this._registerEntityActions(resolved);\n\n // Store the resolved entity definition\n this.entityDefinitions.set(resolved.type, resolved);\n\n // Auto-prewarm pool if poolSize is specified\n if(resolved.poolSize && resolved.poolSize > 0)\n {\n // Prewarm is async but registration is sync — fire and forget is fine here\n // because prewarm just creates entities and pools them\n void this.prewarm(resolved.type, resolved.poolSize);\n }\n }\n\n /**\n * Retrieve an entity definition by type.\n * @param type\n */\n getDefinition(type : string) : GameEntityDefinition | undefined\n {\n return this.entityDefinitions.get(type);\n }\n\n /**\n * Creates a new entity of the given type.\n * Runs onBeforeCreate/onCreate lifecycle hooks, merges state, applies tags, and indexes the entity.\n * @param type - Must match a previously registered entity definition\n * @param options\n */\n async createEntity<\n State extends object = object,\n >\n (\n type : string,\n options : CreateEntityOptions<State> = {}\n ) : Promise<GameEntity<State>>\n {\n this._log.debug(`Creating entity of type: ${ type }`);\n const entityDef = this.entityDefinitions.get(type);\n if(!entityDef)\n {\n const errorMsg = `Entity type ${ type } is not registered.`;\n this._log.error(errorMsg);\n throw new Error(errorMsg);\n }\n\n // Check pool for recyclable entity\n if(entityDef.poolable)\n {\n const pool = this._pools.get(type);\n if(pool && pool.length > 0)\n {\n this._log.trace(`Recycling entity from pool for type: ${ type }`);\n const entity = pool.pop() as GameEntity<State>;\n\n // Reset with definition defaults\n entity.$reset(\n entityDef.defaultState,\n entityDef.tags ?? []\n );\n\n // Re-wire the entity manager back-reference\n entity.$setEntityManager(this);\n\n // Apply any initialState overrides\n if(options.initialState)\n {\n entity.state = { ...entity.state, ...options.initialState } as State;\n }\n\n // Apply additional tags from options\n if(options.tags)\n {\n for(const tag of options.tags)\n {\n entity.$addTag(tag);\n }\n }\n\n // Attach to node if provided\n if(options.node)\n {\n entity.$attachToNode(options.node, this._engine);\n }\n\n // Re-index\n this.entities.set(entity.id, entity);\n this._indexEntity(entity);\n\n this._log.debug(`Recycled entity ${ entity.id } from pool`);\n return entity;\n }\n }\n\n this._log.trace(`Using entity definition with ${ entityDef.behaviors?.length || 0 } behaviors`);\n let mergedState = { ...entityDef.defaultState, ...options.initialState } as State;\n\n // Call onBeforeCreate hook if it exists\n if(entityDef.onBeforeCreate)\n {\n this._log.trace(`Calling onBeforeCreate hook for entity type: ${ type }`);\n const result = await entityDef.onBeforeCreate(mergedState);\n // If the hook returns a new state, use it\n if(result !== undefined)\n {\n mergedState = result as State;\n }\n }\n\n // Determine entity name (options override definition)\n const entityName = options.name ?? entityDef.name;\n\n // Create the entity instance with possibly modified state\n const entity = new GameEntity<State>(\n entityDef.type,\n this.eventBus,\n mergedState,\n entityDef.behaviors,\n entityName\n );\n\n // Wire up the entity manager back-reference for cross-entity messaging\n entity.$setEntityManager(this);\n\n // Attach to node if provided\n if(options.node)\n {\n entity.$attachToNode(options.node, this._engine);\n }\n\n // Apply tags from definition\n if(entityDef.tags)\n {\n for(const tag of entityDef.tags)\n {\n entity.$addTag(tag);\n }\n }\n\n // Apply additional tags from options\n if(options.tags)\n {\n for(const tag of options.tags)\n {\n entity.$addTag(tag);\n }\n }\n\n // Add entity to the manager and index it\n this.entities.set(entity.id, entity);\n this._indexEntity(entity);\n this._log.debug(`Entity created with ID: ${ entity.id }${ entityName ? ` (name: ${ entityName })` : '' }`);\n\n // Call onCreate hook if it exists\n if(entityDef.onCreate)\n {\n this._log.trace(`Calling onCreate hook for entity type: ${ type }`);\n const result = await entityDef.onCreate(entity.state);\n // If the hook returns a new state, update the entity's state\n if(result !== undefined)\n {\n entity.state = result as State;\n }\n }\n\n // Create child entities if defined\n if(entityDef.children)\n {\n for(const childDef of entityDef.children)\n {\n const childInitialState : Record<string, unknown> = { ...childDef.initialState };\n\n // Store position/rotation in initial state for later application\n if(childDef.position)\n {\n childInitialState._position = childDef.position;\n }\n if(childDef.rotation)\n {\n childInitialState._rotation = childDef.rotation;\n }\n\n // eslint-disable-next-line no-await-in-loop\n const childEntity = await this.createEntity(childDef.type, {\n name: childDef.name,\n initialState: childInitialState,\n });\n\n entity.$addChild(childEntity);\n }\n }\n\n return entity;\n }\n\n /**\n * Destroys the entity with the given ID.\n * Runs onBeforeDestroy/onDestroy lifecycle hooks, unindexes, and removes the entity.\n * @param entityID\n */\n async destroyEntity(entityID : string) : Promise<void>\n {\n this._log.debug(`Destroying entity: ${ entityID }`);\n const entity = this.entities.get(entityID);\n if(entity)\n {\n // Get entity definition\n const entityDef = this.entityDefinitions.get(entity.type);\n\n // Check if this entity should be pooled instead of destroyed\n if(entityDef?.poolable)\n {\n this._log.trace(`Pooling entity ${ entityID } instead of destroying`);\n\n // Pool children first (iterate copy to avoid mutation during iteration)\n for(const child of [ ...entity.children ])\n {\n // eslint-disable-next-line no-await-in-loop\n await this.destroyEntity(child.id);\n }\n\n // If the entity has a parent, remove it from the parent's children\n if(entity.parent)\n {\n entity.parent.$removeChild(entity);\n }\n\n // Remove from all indices\n this._unindexEntity(entity);\n\n // Reset the entity\n entity.$reset(entityDef.defaultState, entityDef.tags ?? []);\n\n // Push to pool\n let pool = this._pools.get(entity.type);\n if(!pool)\n {\n pool = [];\n this._pools.set(entity.type, pool);\n }\n pool.push(entity);\n\n // Remove from active entities\n this.entities.delete(entityID);\n\n this._log.debug(`Entity ${ entityID } pooled`);\n return;\n }\n\n // Non-poolable: destroy normally\n\n // Destroy children first (iterate copy to avoid mutation during iteration)\n for(const child of [ ...entity.children ])\n {\n // eslint-disable-next-line no-await-in-loop\n await this.destroyEntity(child.id);\n }\n\n // If the entity has a parent, remove it from the parent's children\n if(entity.parent)\n {\n entity.parent.$removeChild(entity);\n }\n\n // Call onBeforeDestroy hook if it exists\n if(entityDef?.onBeforeDestroy)\n {\n this._log.trace(`Calling onBeforeDestroy hook for entity: ${ entityID }`);\n const result = await entityDef.onBeforeDestroy(entity.state);\n // If the hook returns a new state, update the entity's state\n if(result !== undefined)\n {\n entity.state = result;\n }\n }\n\n // Remove from indices BEFORE destroying\n this._unindexEntity(entity);\n\n // Destroy entity (call $destroy internally)\n await entity.$destroy();\n\n // Call onDestroy hook if it exists\n if(entityDef?.onDestroy)\n {\n this._log.trace(`Calling onDestroy hook for entity: ${ entityID }`);\n await entityDef.onDestroy(entity.state);\n }\n\n // Remove entity from manager\n this.entities.delete(entityID);\n\n this._log.debug(`Entity ${ entityID } destroyed`);\n }\n else\n {\n this._log.warn(`Attempted to destroy non-existent entity: ${ entityID }`);\n }\n }\n\n /**\n * Gets the entity with the given ID.\n * @param entityID\n */\n getEntity(entityID : string) : GameEntity | null\n {\n this._log.trace(`Getting entity: ${ entityID }`);\n return this.entities.get(entityID) ?? null;\n }\n\n /**\n * Adds an existing entity to the entity manager and indexes it.\n * @param entity\n */\n addEntity(entity : GameEntity) : void\n {\n this._log.debug(`Adding existing entity: ${ entity.id } (type: ${ entity.type })`);\n entity.$setEntityManager(this);\n this.entities.set(entity.id, entity);\n this._indexEntity(entity);\n }\n\n /**\n * Removes the entity with the given ID, without destroying it.\n * @param entityID\n */\n removeEntity(entityID : string) : void\n {\n this._log.debug(`Removing entity: ${ entityID }`);\n const entity = this.entities.get(entityID);\n if(entity)\n {\n this._unindexEntity(entity);\n this.entities.delete(entityID);\n this._log.debug(`Entity ${ entityID } removed`);\n }\n else\n {\n this._log.warn(`Attempted to remove non-existent entity: ${ entityID }`);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Parent-Child Hierarchy\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Adds a child entity to a parent entity.\n * @param parentId - ID of the parent entity\n * @param childId - ID of the child entity\n * @returns False if either entity is not found\n */\n addChild(parentId : string, childId : string) : boolean\n {\n const parent = this.entities.get(parentId);\n const child = this.entities.get(childId);\n\n if(!parent)\n {\n this._log.warn(`addChild: parent entity \"${ parentId }\" not found`);\n return false;\n }\n\n if(!child)\n {\n this._log.warn(`addChild: child entity \"${ childId }\" not found`);\n return false;\n }\n\n parent.$addChild(child);\n this._log.trace(`Added child ${ childId } to parent ${ parentId }`);\n return true;\n }\n\n /**\n * Removes a child entity from a parent entity.\n * @param parentId - ID of the parent entity\n * @param childId - ID of the child entity\n * @returns False if either entity is not found\n */\n removeChild(parentId : string, childId : string) : boolean\n {\n const parent = this.entities.get(parentId);\n const child = this.entities.get(childId);\n\n if(!parent)\n {\n this._log.warn(`removeChild: parent entity \"${ parentId }\" not found`);\n return false;\n }\n\n if(!child)\n {\n this._log.warn(`removeChild: child entity \"${ childId }\" not found`);\n return false;\n }\n\n parent.$removeChild(child);\n this._log.trace(`Removed child ${ childId } from parent ${ parentId }`);\n return true;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Query Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Gets the first entity with the given name.\n * Names are not unique; use getEntitiesByName() to retrieve all matches.\n * @param name\n */\n getByName(name : string) : GameEntity | undefined\n {\n const nameSet = this._entitiesByName.get(name);\n if(nameSet && nameSet.size > 0)\n {\n return nameSet.values().next().value;\n }\n return undefined;\n }\n\n /**\n * Gets all entities with the given name.\n * @param name\n */\n getEntitiesByName(name : string) : GameEntity[]\n {\n const nameSet = this._entitiesByName.get(name);\n return nameSet ? Array.from(nameSet) : [];\n }\n\n /**\n * Gets all entities of the given type.\n * @param type\n */\n getByType(type : string) : GameEntity[]\n {\n const typeSet = this._entitiesByType.get(type);\n return typeSet ? Array.from(typeSet) : [];\n }\n\n /**\n * Gets all entities with the given tag.\n * @param tag\n */\n getByTag(tag : string) : GameEntity[]\n {\n const tagSet = this._entitiesByTag.get(tag);\n return tagSet ? Array.from(tagSet) : [];\n }\n\n /**\n * Gets all entities matching multiple tags.\n * @param tags\n * @param mode - 'all' requires every tag (intersection), 'any' requires at least one (union)\n */\n getByTags(tags : string[], mode : 'all' | 'any' = 'all') : GameEntity[]\n {\n if(tags.length === 0)\n {\n return [];\n }\n\n if(mode === 'any')\n {\n // Union - entities with ANY of the tags\n const result = new Set<GameEntity>();\n for(const tag of tags)\n {\n const tagSet = this._entitiesByTag.get(tag);\n if(tagSet)\n {\n for(const entity of tagSet)\n {\n result.add(entity);\n }\n }\n }\n return Array.from(result);\n }\n else\n {\n // Intersection - entities with ALL tags\n // Start with smallest set for efficiency\n const sets = tags\n .map((tag) => this._entitiesByTag.get(tag))\n .filter((set) : set is Set<GameEntity> => set !== undefined);\n\n // If any tag has no entities, result is empty\n if(sets.length !== tags.length)\n {\n return [];\n }\n\n // Sort by size (smallest first) for efficient intersection\n sets.sort((setA, setB) => setA.size - setB.size);\n const [ smallest, ...rest ] = sets;\n\n return Array.from(smallest).filter((entity) => rest.every((set) => set.has(entity)));\n }\n }\n\n /**\n * Returns an iterator over all entities.\n * More memory-efficient than returning an array for large entity counts.\n */\n getAllEntities() : IterableIterator<GameEntity>\n {\n return this.entities.values();\n }\n\n /**\n * Gets the total number of entities.\n */\n get entityCount() : number\n {\n return this.entities.size;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Messaging\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Sends a fire-and-forget event directly to a specific entity's behaviors, bypassing the event bus.\n * Resolves the target by entity ID first (O(1)), then falls back to name lookup (O(1)).\n *\n * @param target - Entity ID or name\n * @param type - Event type string\n * @param payload - Optional event payload\n */\n async send(target : string, type : string, payload ?: unknown, senderID ?: string) : Promise<void>\n {\n const entity = this.entities.get(target) ?? this.getByName(target);\n if(!entity)\n {\n this._log.warn(`send: entity \"${ target }\" not found`);\n return;\n }\n\n await entity.$send(type, payload, senderID);\n }\n\n /**\n * Sends a request to a specific entity's behaviors and returns the first response.\n * Resolves the target by entity ID first (O(1)), then falls back to name lookup (O(1)).\n *\n * @param target - Entity ID or name\n * @param type - Request type string\n * @param payload - Optional request payload\n * @param senderID - Optional sender entity ID (stamped on the event)\n */\n async request<T>(\n target : string,\n type : string,\n payload ?: unknown,\n senderID ?: string\n ) : Promise<RequestResult<T>>\n {\n const entity = this.entities.get(target) ?? this.getByName(target);\n if(!entity)\n {\n return { success: false, error: `Entity \"${ target }\" not found` };\n }\n\n return entity.$request<T>(type, payload, senderID);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Tag Manipulation\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Adds a tag to an entity and updates the tag index.\n * @param entity - Entity instance or entity ID\n * @param tag\n * @returns False if already present or entity not found\n */\n addTag(entity : GameEntity | string, tag : string) : boolean\n {\n const targetEntity = typeof entity === 'string' ? this.entities.get(entity) : entity;\n if(!targetEntity)\n {\n this._log.warn(`addTag: entity not found`);\n return false;\n }\n\n if(targetEntity.hasTag(tag))\n {\n return false; // Already has tag\n }\n\n // Add to entity and update index\n targetEntity.$addTag(tag);\n this._getOrCreateSet(this._entitiesByTag, tag).add(targetEntity);\n\n this._log.trace(`Added tag \"${ tag }\" to entity ${ targetEntity.id }`);\n return true;\n }\n\n /**\n * Removes a tag from an entity and updates the tag index.\n * @param entity - Entity instance or entity ID\n * @param tag\n * @returns False if not present or entity not found\n */\n removeTag(entity : GameEntity | string, tag : string) : boolean\n {\n const targetEntity = typeof entity === 'string' ? this.entities.get(entity) : entity;\n if(!targetEntity)\n {\n this._log.warn(`removeTag: entity not found`);\n return false;\n }\n\n if(!targetEntity.$removeTag(tag))\n {\n return false; // Didn't have tag\n }\n\n // Update index\n const tagSet = this._entitiesByTag.get(tag);\n if(tagSet)\n {\n tagSet.delete(targetEntity);\n if(tagSet.size === 0)\n {\n this._entitiesByTag.delete(tag);\n }\n }\n\n this._log.trace(`Removed tag \"${ tag }\" from entity ${ targetEntity.id }`);\n return true;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Node Attachment\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Gets an entity by its attached scene node.\n * @param node\n */\n getByNode(node : TransformNode) : GameEntity | undefined\n {\n return this._entitiesByNode.get(node);\n }\n\n /**\n * Attaches an entity to a scene node.\n * The entity will reference this node and can be looked up via getByNode().\n *\n * @param entity - Entity instance or entity ID\n * @param node\n * @returns False if entity not found or node already has an entity\n */\n attachToNode(entity : GameEntity | string, node : TransformNode) : boolean\n {\n const targetEntity = typeof entity === 'string' ? this.entities.get(entity) : entity;\n if(!targetEntity)\n {\n this._log.warn(`attachToNode: entity not found`);\n return false;\n }\n\n // Check if node already has an entity\n if(this._entitiesByNode.has(node))\n {\n this._log.warn(`attachToNode: node already has an attached entity`);\n return false;\n }\n\n // Detach from current node if any\n if(targetEntity.node)\n {\n this._entitiesByNode.delete(targetEntity.node);\n }\n\n // Attach to new node\n targetEntity.$attachToNode(node, this._engine);\n this._entitiesByNode.set(node, targetEntity);\n\n this._log.trace(`Attached entity ${ targetEntity.id } to node \"${ node.name }\"`);\n return true;\n }\n\n /**\n * Detaches an entity from its scene node.\n *\n * @param entity - Entity instance or entity ID\n * @returns False if entity not found or had no node\n */\n detachFromNode(entity : GameEntity | string) : boolean\n {\n const targetEntity = typeof entity === 'string' ? this.entities.get(entity) : entity;\n if(!targetEntity)\n {\n this._log.warn(`detachFromNode: entity not found`);\n return false;\n }\n\n if(!targetEntity.node)\n {\n return false; // Nothing to detach\n }\n\n // Remove from index\n this._entitiesByNode.delete(targetEntity.node);\n\n // Detach entity\n const nodeName = targetEntity.node.name;\n targetEntity.$detachFromNode();\n\n this._log.trace(`Detached entity ${ targetEntity.id } from node \"${ nodeName }\"`);\n return true;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Object Pooling\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Pre-creates entities and pools them for fast recycling.\n * @param type - Entity type to prewarm\n * @param count - Number of entities to create and pool\n */\n async prewarm(type : string, count : number) : Promise<void>\n {\n this._log.debug(`Prewarming pool for \"${ type }\" with ${ count } entities`);\n\n for(let i = 0; i < count; i++)\n {\n // eslint-disable-next-line no-await-in-loop\n const entity = await this.createEntity(type);\n // eslint-disable-next-line no-await-in-loop\n await this.destroyEntity(entity.id);\n }\n }\n\n /**\n * Drains the pool for a given type, actually destroying all pooled entities.\n * @param type - Entity type whose pool to drain\n */\n async drainPool(type : string) : Promise<void>\n {\n const pool = this._pools.get(type);\n if(!pool)\n {\n return;\n }\n\n this._log.debug(`Draining pool for \"${ type }\" (${ pool.length } entities)`);\n\n for(const entity of pool)\n {\n // eslint-disable-next-line no-await-in-loop\n await entity.$destroy();\n }\n\n pool.length = 0;\n this._pools.delete(type);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Tears down all entities and releases resources.\n * Destroys entities individually (running lifecycle hooks), then clears definitions and indices.\n */\n async $teardown() : Promise<void>\n {\n this._log.info(`Tearing down EntityManager with ${ this.entities.size } entities`);\n\n // Create a copy of the entity IDs to avoid modification during iteration\n const entityIds = [ ...this.entities.keys() ];\n\n // Destroy all entities\n for(const entityId of entityIds)\n {\n try\n {\n // eslint-disable-next-line no-await-in-loop\n await this.destroyEntity(entityId);\n }\n catch (err)\n {\n this._log.error(`Error destroying entity ${ entityId }: ${ err }`);\n }\n }\n\n // Drain all pools\n for(const type of [ ...this._pools.keys() ])\n {\n try\n {\n // eslint-disable-next-line no-await-in-loop\n await this.drainPool(type);\n }\n catch (err)\n {\n this._log.error(`Error draining pool for \"${ type }\": ${ err }`);\n }\n }\n\n // Clear entity definitions\n this.entityDefinitions.clear();\n\n // Clear indices\n this._entitiesByName.clear();\n this._entitiesByType.clear();\n this._entitiesByTag.clear();\n this._entitiesByNode.clear();\n\n this._log.info('EntityManager torn down successfully');\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n","//----------------------------------------------------------------------------------------------------------------------\n// Post-Processing Pipeline Backend\n//\n// Applies post-processing effects via BabylonJS DefaultRenderingPipeline and SSAO2RenderingPipeline.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport {\n Color4,\n DefaultRenderingPipeline,\n ImageProcessingConfiguration,\n SSAO2RenderingPipeline,\n type Scene,\n} from '@babylonjs/core';\n\nimport type { PostProcessingConfig } from '../interfaces/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Map from user-facing tonemap operator names to BabylonJS constants.\n * BabylonJS only has STANDARD (0) and ACES (1) — the other names all map to STANDARD.\n */\nconst TONEMAP_OPERATORS : Record<string, number> = {\n hable: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n reinhard: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n hejidawson: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n photographic: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n aces: ImageProcessingConfiguration.TONEMAPPING_ACES,\n};\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Apply post-processing effects to a scene based on a declarative config.\n * Creates BabylonJS rendering pipelines and configures them according to the provided settings.\n *\n * This is a level-config handler, not a node property handler.\n */\nexport function applyPipelinePostProcessing(scene : Scene, config : PostProcessingConfig) : void\n{\n // Can't set up post-processing without a camera\n if(!scene.activeCamera)\n {\n return;\n }\n\n // Warn about Frame Graph-only features\n if(config.volumetric)\n {\n console.warn('[SAGE] Volumetric lighting requires renderer: \"frameGraph\". Ignored in pipeline mode.');\n }\n\n // Create the default rendering pipeline\n const pipeline = new DefaultRenderingPipeline('sage-default', true, scene, scene.cameras);\n\n // Bloom\n if(config.bloom)\n {\n pipeline.bloomEnabled = true;\n\n if(config.bloom.weight !== undefined)\n {\n pipeline.bloomWeight = config.bloom.weight;\n }\n if(config.bloom.threshold !== undefined)\n {\n pipeline.bloomThreshold = config.bloom.threshold;\n }\n if(config.bloom.scale !== undefined)\n {\n pipeline.bloomScale = config.bloom.scale;\n }\n if(config.bloom.kernel !== undefined)\n {\n pipeline.bloomKernel = config.bloom.kernel;\n }\n }\n\n // Tone mapping\n if(config.tonemap)\n {\n pipeline.imageProcessingEnabled = true;\n pipeline.imageProcessing.toneMappingEnabled = true;\n\n if(config.tonemap.operator)\n {\n pipeline.imageProcessing.toneMappingType\n = TONEMAP_OPERATORS[config.tonemap.operator] ?? ImageProcessingConfiguration.TONEMAPPING_STANDARD;\n }\n }\n\n // Film grain\n if(config.grain)\n {\n pipeline.grainEnabled = true;\n\n if(config.grain.intensity !== undefined)\n {\n pipeline.grain.intensity = config.grain.intensity;\n }\n if(config.grain.animated !== undefined)\n {\n pipeline.grain.animated = config.grain.animated;\n }\n }\n\n // Vignette\n if(config.vignette)\n {\n pipeline.imageProcessingEnabled = true;\n pipeline.imageProcessing.vignetteEnabled = true;\n\n if(config.vignette.weight !== undefined)\n {\n pipeline.imageProcessing.vignetteWeight = config.vignette.weight;\n }\n if(config.vignette.stretch !== undefined)\n {\n pipeline.imageProcessing.vignetteStretch = config.vignette.stretch;\n }\n if(config.vignette.color)\n {\n pipeline.imageProcessing.vignetteColor = new Color4(\n config.vignette.color.r,\n config.vignette.color.g,\n config.vignette.color.b,\n 1\n );\n }\n }\n\n // Sharpen\n if(config.sharpen)\n {\n pipeline.sharpenEnabled = true;\n\n if(config.sharpen.edge !== undefined)\n {\n pipeline.sharpen.edgeAmount = config.sharpen.edge;\n }\n if(config.sharpen.color !== undefined)\n {\n pipeline.sharpen.colorAmount = config.sharpen.color;\n }\n }\n\n // Chromatic aberration\n if(config.chromaticAberration)\n {\n pipeline.chromaticAberrationEnabled = true;\n\n if(config.chromaticAberration.amount !== undefined)\n {\n pipeline.chromaticAberration.aberrationAmount = config.chromaticAberration.amount;\n }\n }\n\n // SSAO — uses its own separate pipeline\n if(config.ssao)\n {\n const ssao = new SSAO2RenderingPipeline('sage-ssao', scene, { ssaoRatio: 0.5, blurRatio: 1.0 });\n\n if(config.ssao.radius !== undefined)\n {\n ssao.radius = config.ssao.radius;\n }\n if(config.ssao.samples !== undefined)\n {\n ssao.samples = config.ssao.samples;\n }\n if(config.ssao.totalStrength !== undefined)\n {\n ssao.totalStrength = config.ssao.totalStrength;\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Frame Graph Post-Processing Backend\n//\n// Builds a directed task chain using BabylonJS Frame Graph system.\n// Each post-processing effect is a separate task chained via texture handles.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport {\n Constants,\n FrameGraph,\n FrameGraphBloomTask,\n FrameGraphChromaticAberrationTask,\n FrameGraphClearTextureTask,\n FrameGraphCopyToBackbufferColorTask,\n FrameGraphGeometryRendererTask,\n FrameGraphGrainTask,\n FrameGraphImageProcessingTask,\n FrameGraphObjectRendererTask,\n FrameGraphSSAO2RenderingPipelineTask,\n FrameGraphSharpenTask,\n type FrameGraphTextureHandle,\n ImageProcessingConfiguration,\n type Scene,\n} from '@babylonjs/core';\n\nimport type { PostProcessingConfig } from '../interfaces/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Map from user-facing tonemap operator names to BabylonJS constants.\n * BabylonJS only has STANDARD (0) and ACES (1) — the other names all map to STANDARD.\n */\nconst TONEMAP_OPERATORS : Record<string, number> = {\n hable: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n reinhard: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n hejidawson: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n photographic: ImageProcessingConfiguration.TONEMAPPING_STANDARD,\n aces: ImageProcessingConfiguration.TONEMAPPING_ACES,\n};\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport async function applyFrameGraphPostProcessing(scene : Scene, config : PostProcessingConfig) : Promise<void>\n{\n const camera = scene.activeCamera;\n if(!camera)\n {\n return;\n }\n\n const engine = scene.getEngine();\n const fg = new FrameGraph(scene);\n\n // Create the main render target texture\n const colorTexture = fg.textureManager.createRenderTargetTexture('sage-fg-color', {\n size: { width: 100, height: 100 },\n sizeIsPercentage: true,\n options: { formats: [ Constants.TEXTUREFORMAT_RGBA ], samples: 4 },\n });\n\n // 1. Clear\n const clearTask = new FrameGraphClearTextureTask('sage-clear', fg);\n clearTask.targetTexture = colorTexture;\n fg.addTask(clearTask);\n\n // 2. Render objects\n const renderTask = new FrameGraphObjectRendererTask('sage-render', fg, scene);\n renderTask.targetTexture = clearTask.outputTexture;\n renderTask.depthTexture = clearTask.outputDepthTexture;\n renderTask.camera = camera;\n renderTask.objectList = { meshes: scene.meshes, particleSystems: scene.particleSystems };\n renderTask.isMainObjectRenderer = true;\n fg.addTask(renderTask);\n\n // Track the current output texture through the chain\n let currentTexture : FrameGraphTextureHandle = renderTask.outputTexture;\n\n // 3. SSAO (needs geometry renderer for depth/normals)\n if(config.ssao)\n {\n const geoTask = new FrameGraphGeometryRendererTask('sage-geometry', fg, scene);\n fg.addTask(geoTask);\n\n const ssaoTask = new FrameGraphSSAO2RenderingPipelineTask('sage-ssao', fg, 0.5, 1.0);\n ssaoTask.sourceTexture = currentTexture;\n ssaoTask.depthTexture = geoTask.geometryViewDepthTexture;\n ssaoTask.normalTexture = geoTask.geometryViewNormalTexture;\n ssaoTask.camera = camera;\n fg.addTask(ssaoTask);\n\n if(config.ssao.radius !== undefined) { ssaoTask.ssao.radius = config.ssao.radius; }\n if(config.ssao.samples !== undefined) { ssaoTask.ssao.samples = config.ssao.samples; }\n if(config.ssao.totalStrength !== undefined) { ssaoTask.ssao.totalStrength = config.ssao.totalStrength; }\n\n currentTexture = ssaoTask.outputTexture;\n }\n\n // 4. Bloom\n if(config.bloom)\n {\n const bloomTask = new FrameGraphBloomTask(\n 'sage-bloom',\n fg,\n config.bloom.weight ?? 0.5,\n config.bloom.kernel ?? 128,\n config.bloom.threshold ?? 0.1,\n true,\n config.bloom.scale ?? 0.5\n );\n bloomTask.sourceTexture = currentTexture;\n fg.addTask(bloomTask);\n\n currentTexture = bloomTask.outputTexture;\n }\n\n // 5. Grain\n if(config.grain)\n {\n const grainTask = new FrameGraphGrainTask('sage-grain', fg);\n grainTask.sourceTexture = currentTexture;\n fg.addTask(grainTask);\n\n if(config.grain.intensity !== undefined) { grainTask.postProcess.intensity = config.grain.intensity; }\n if(config.grain.animated !== undefined) { grainTask.postProcess.animated = config.grain.animated; }\n\n currentTexture = grainTask.outputTexture;\n }\n\n // 6. Sharpen\n if(config.sharpen)\n {\n const sharpenTask = new FrameGraphSharpenTask('sage-sharpen', fg);\n sharpenTask.sourceTexture = currentTexture;\n fg.addTask(sharpenTask);\n\n if(config.sharpen.edge !== undefined) { sharpenTask.postProcess.edgeAmount = config.sharpen.edge; }\n if(config.sharpen.color !== undefined) { sharpenTask.postProcess.colorAmount = config.sharpen.color; }\n\n currentTexture = sharpenTask.outputTexture;\n }\n\n // 7. Chromatic aberration\n if(config.chromaticAberration)\n {\n const chromTask = new FrameGraphChromaticAberrationTask('sage-chromab', fg);\n chromTask.sourceTexture = currentTexture;\n fg.addTask(chromTask);\n\n if(config.chromaticAberration.amount !== undefined)\n {\n chromTask.postProcess.aberrationAmount = config.chromaticAberration.amount;\n }\n\n currentTexture = chromTask.outputTexture;\n }\n\n // 8. Image processing (tonemap + vignette)\n if(config.tonemap || config.vignette)\n {\n const imgTask = new FrameGraphImageProcessingTask('sage-imgproc', fg);\n imgTask.sourceTexture = currentTexture;\n fg.addTask(imgTask);\n\n if(config.tonemap)\n {\n imgTask.postProcess.toneMappingEnabled = true;\n\n if(config.tonemap.operator)\n {\n imgTask.postProcess.toneMappingType\n = TONEMAP_OPERATORS[config.tonemap.operator]\n ?? ImageProcessingConfiguration.TONEMAPPING_STANDARD;\n }\n }\n\n if(config.vignette)\n {\n imgTask.postProcess.vignetteEnabled = true;\n\n if(config.vignette.weight !== undefined) { imgTask.postProcess.vignetteWeight = config.vignette.weight; }\n if(config.vignette.stretch !== undefined)\n {\n imgTask.postProcess.vignetteStretch = config.vignette.stretch;\n }\n if(config.vignette.color)\n {\n const vc = config.vignette.color;\n imgTask.postProcess.vignetteColor.set(vc.r, vc.g, vc.b, 1);\n }\n }\n\n currentTexture = imgTask.outputTexture;\n }\n\n // 9. Volumetric lighting — not yet wired up for automatic light detection\n if(config.volumetric)\n {\n console.warn(\n '[SAGE] Volumetric lighting config is recognized but automatic light detection is not yet supported. '\n + 'Use scene.frameGraph to add FrameGraphVolumetricLightingTask manually.'\n );\n }\n\n // 10. Output to screen\n const copyTask = new FrameGraphCopyToBackbufferColorTask('sage-output', fg);\n copyTask.sourceTexture = currentTexture;\n fg.addTask(copyTask);\n\n // Wire up the scene — cameraToUseForPointers is needed because activeCamera becomes null between tasks\n scene.cameraToUseForPointers = camera;\n scene.frameGraph = fg;\n\n // Rebuild on resize\n engine.onResizeObservable.add(async () =>\n {\n await fg.buildAsync();\n });\n\n // Initial build\n await fg.buildAsync();\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Post-Processing Dispatcher\n//\n// Routes to either the DefaultRenderingPipeline backend or the Frame Graph backend\n// based on config.renderer.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { Scene } from '@babylonjs/core';\n\nimport type { PostProcessingConfig } from '../interfaces/level.ts';\n\nimport { applyPipelinePostProcessing } from './postProcessingPipeline.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport async function applyPostProcessing(scene : Scene, config : PostProcessingConfig) : Promise<void>\n{\n if(config.renderer === 'frameGraph')\n {\n const { applyFrameGraphPostProcessing } = await import('./postProcessingFrameGraph.ts');\n await applyFrameGraphPostProcessing(scene, config);\n }\n else\n {\n applyPipelinePostProcessing(scene, config);\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Vector / Quaternion Conversion Utilities\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { Matrix, Quaternion, type TransformNode, Vector3 } from '@babylonjs/core';\n\nimport type { QuatConfig, Vec3Config } from '../interfaces/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Convert a BabylonJS Vector3 to a plain { x, y, z } object.\n */\nexport function toVec3Object(vector : Vector3) : Vec3Config\n{\n return { x: vector.x, y: vector.y, z: vector.z };\n}\n\n/**\n * Convert a plain { x, y, z } object to a BabylonJS Vector3.\n */\nexport function toVector3(vec : Vec3Config) : Vector3\n{\n return new Vector3(vec.x, vec.y, vec.z);\n}\n\n/**\n * Convert a BabylonJS Quaternion to a plain { x, y, z, w } object.\n */\nexport function toQuatObject(quat : Quaternion) : QuatConfig\n{\n return { x: quat.x, y: quat.y, z: quat.z, w: quat.w }; // eslint-disable-line id-length\n}\n\n/**\n * Convert a plain { x, y, z, w } object to a BabylonJS Quaternion.\n */\nexport function toQuaternion(quat : QuatConfig) : Quaternion\n{\n return new Quaternion(quat.x, quat.y, quat.z, quat.w);\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n// Transform Hierarchy Utilities\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Compute a node's local matrix from its position, rotation, and scaling.\n */\nfunction computeLocalMatrix(node : TransformNode) : Matrix\n{\n return Matrix.Compose(\n node.scaling,\n node.rotationQuaternion ?? Quaternion.FromEulerVector(node.rotation),\n node.position\n );\n}\n\n/**\n * Compute a node's transform in canonical Babylon space by rebuilding its world\n * matrix from local transforms, skipping any ancestors whose local matrix has a\n * negative determinant (import conversion nodes like glTF's __root__).\n *\n * This avoids decomposing a mirrored matrix (det < 0), which produces poisoned\n * scaling and rotation. The result is a clean, positive-determinant matrix that\n * works uniformly for primitives and GLB entities.\n *\n * Handles arbitrarily nested import hierarchies (GLB inside GLB) — each\n * conversion node in the chain is individually skipped.\n */\nexport function getCanonicalTransform(node : TransformNode) : {\n position : Vector3;\n rotation : Quaternion;\n scaling : Vector3;\n}\n{\n node.computeWorldMatrix(true);\n\n // Rebuild the world matrix from local transforms, skipping negative-determinant\n // ancestors (import conversion roots). Walk bottom-up, composing only clean locals.\n let canonical = computeLocalMatrix(node);\n\n let ancestor = node.parent as TransformNode | null;\n while(ancestor)\n {\n ancestor.computeWorldMatrix(true);\n const ancestorLocal = computeLocalMatrix(ancestor);\n\n if(ancestorLocal.determinant() >= 0)\n {\n canonical = canonical.multiply(ancestorLocal);\n }\n\n ancestor = ancestor.parent as TransformNode | null;\n }\n\n const position = new Vector3();\n const rotation = new Quaternion();\n const scaling = new Vector3();\n canonical.decompose(scaling, rotation, position);\n\n return { position, rotation, scaling };\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Abstract Level Class\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { Scene } from '@babylonjs/core';\nimport { ClusteredLightContainer } from '@babylonjs/core/Lights/Clustered/clusteredLightContainer';\n\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type {\n LevelConfig,\n LevelContext,\n LevelInstance,\n PropertyHandler,\n} from '../interfaces/level.ts';\n\nimport { SAGELogger } from '../utils/logger.ts';\nimport type { GameEngine } from './gameEngine.ts';\nimport { OutlineManager } from '../managers/outline.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Abstract base class for game levels.\n *\n * Levels are responsible for creating and configuring their own scenes via the\n * `buildScene()` method. All runtime dependencies (eventBus, sceneEngine, entityManager)\n * are injected via the constructor and are immediately available.\n *\n * Users should not instantiate Level subclasses directly. Instead, use LevelManager\n * which acts as a factory and injects the required dependencies:\n *\n * ```typescript\n * // Register a custom level class\n * levelManager.registerLevelClass('my-level', MyCustomLevel);\n *\n * // Load the level - manager creates instance with deps injected\n * const level = await levelManager.loadLevel({ name: 'Level1', class: 'my-level' });\n * ```\n */\nexport abstract class Level implements LevelInstance\n{\n public readonly name : string;\n protected readonly _log : LoggerInterface;\n protected readonly _context : LevelContext;\n protected _scene : Scene | null = null;\n public clusteredLights : ClusteredLightContainer | null = null;\n public outlines : OutlineManager | null = null;\n\n //------------------------------------------------------------------------------------------------------------------\n // Constructor\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Creates a new Level instance.\n *\n * @param config\n * @param context\n */\n constructor(config : LevelConfig, context : LevelContext)\n {\n this.name = config.name;\n this._context = context;\n this._log = context.logger?.getLogger(`Level:${ config.name }`)\n ?? new SAGELogger(`Level:${ config.name }`, 'info');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Accessors\n //------------------------------------------------------------------------------------------------------------------\n\n get scene() : Scene | null\n {\n return this._scene;\n }\n\n get isLoaded() : boolean\n {\n return this._scene !== null;\n }\n\n get gameEngine() : GameEngine\n {\n return this._context.gameEngine;\n }\n\n get propertyHandlers() : Map<string, PropertyHandler>\n {\n return this._context.propertyHandlers;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Protected Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Emit a progress event during loading\n *\n * @param progress - Percentage (0-100)\n * @param message\n */\n protected $emitProgress(progress : number, message : string) : void\n {\n this.gameEngine.eventBus.publish({\n type: 'level:progress',\n payload: {\n levelName: this.name,\n progress,\n message,\n },\n });\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Load and create the scene for this level.\n * Progress events will be emitted during loading.\n */\n async load() : Promise<Scene>\n {\n if(this._scene)\n {\n this._log.warn(`Level ${ this.name } is already loaded`);\n return this._scene;\n }\n\n this._log.info(`Loading level: ${ this.name }`);\n this._log.time(`level-${ this.name }-load`);\n\n try\n {\n this.$emitProgress(0, 'Starting level load...');\n\n // Let the concrete level build its content\n this._scene = await this.buildScene();\n\n this.$emitProgress(100, 'Level loaded successfully');\n this.gameEngine.eventBus.publish({\n type: 'level:complete',\n payload: {\n levelName: this.name,\n message: 'Level loaded successfully',\n level: this,\n },\n });\n\n this._log.timeEnd(`level-${ this.name }-load`);\n this._log.info(`Level ${ this.name } loaded successfully`);\n\n return this._scene;\n }\n catch (error)\n {\n this._log.error(`Failed to load level ${ this.name }:`, error);\n this.gameEngine.eventBus.publish({\n type: 'level:error',\n payload: {\n levelName: this.name,\n message: 'Failed to load level',\n error,\n },\n });\n throw error;\n }\n }\n\n /**\n * Abstract method for building the scene content.\n * Concrete levels must implement this to create their specific content.\n *\n * All dependencies are available via `this.gameEngine`.\n *\n */\n protected abstract buildScene() : Promise<Scene>;\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Called when this level becomes the active level during a transition.\n * Override in subclasses to perform activation logic (e.g., start music, show UI).\n * Default implementation is a no-op.\n */\n onActivate?() : Promise<void> | void;\n\n /**\n * Called when this level is being replaced during a transition.\n * Override in subclasses to perform cleanup logic (e.g., fade out, save state).\n * Default implementation is a no-op.\n */\n onDeactivate?() : Promise<void> | void;\n\n /**\n * Dispose of this level's resources and scene.\n *\n * The `$` prefix indicates this is an internal lifecycle method, consistent with\n * other engine lifecycle methods like `$teardown()`.\n */\n async $dispose() : Promise<void>\n {\n if(this._scene)\n {\n this._log.info(`Disposing level: ${ this.name }`);\n\n // Detach all entities from this scene's nodes BEFORE disposing the scene.\n // Behaviors need the scene alive to clean up physics bodies, cameras, etc.\n const entityManager = this._context.gameEngine?.managers?.entityManager;\n if(entityManager?.getAllEntities)\n {\n for(const entity of entityManager.getAllEntities())\n {\n if(entity.node && entity.node.getScene() === this._scene)\n {\n entityManager.detachFromNode(entity);\n }\n }\n }\n\n if(this.outlines)\n {\n this.outlines.dispose();\n this.outlines = null;\n }\n\n if(this.clusteredLights)\n {\n this.clusteredLights.dispose();\n this.clusteredLights = null;\n }\n\n this._scene.dispose();\n this._scene = null;\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Outline Manager\n//\n// Manages named SelectionOutlineLayer instances for entity highlighting.\n// Each layer tracks a Set<GameEntity> and rebuilds its selection on any change.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { Color3, type Scene, SelectionOutlineLayer } from '@babylonjs/core';\n\nimport type { GameEntity } from '../classes/entity.ts';\nimport type { OutlineLayerConfig } from '../interfaces/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface OutlineLayerEntry\n{\n layer : SelectionOutlineLayer;\n entities : Set<GameEntity>;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class OutlineManager\n{\n private _scene : Scene;\n private _layers = new Map<string, OutlineLayerEntry>();\n\n constructor(scene : Scene, configs ?: Record<string, OutlineLayerConfig>)\n {\n this._scene = scene;\n\n if(configs)\n {\n for(const [ name, config ] of Object.entries(configs))\n {\n this.create(name, config);\n }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n get(name : string) : SelectionOutlineLayer | undefined\n {\n return this._layers.get(name)?.layer;\n }\n\n create(name : string, config ?: OutlineLayerConfig) : SelectionOutlineLayer\n {\n const existing = this._layers.get(name);\n if(existing)\n {\n return existing.layer;\n }\n\n const layer = new SelectionOutlineLayer(`sage-outline-${ name }`, this._scene);\n\n if(config?.color)\n {\n layer.outlineColor = new Color3(config.color.r, config.color.g, config.color.b);\n }\n\n if(config?.thickness !== undefined) { layer.outlineThickness = config.thickness; }\n if(config?.occlusionStrength !== undefined) { layer.occlusionStrength = config.occlusionStrength; }\n\n this._layers.set(name, { layer, entities: new Set() });\n return layer;\n }\n\n highlightEntity(layerName : string, entity : GameEntity) : void\n {\n const entry = this._layers.get(layerName);\n if(!entry)\n {\n return;\n }\n\n entry.entities.add(entity);\n this._rebuildSelection(entry);\n }\n\n unhighlightEntity(layerName : string, entity : GameEntity) : void\n {\n const entry = this._layers.get(layerName);\n if(!entry)\n {\n return;\n }\n\n entry.entities.delete(entity);\n this._rebuildSelection(entry);\n }\n\n unhighlightEntityAll(entity : GameEntity) : void\n {\n for(const entry of this._layers.values())\n {\n if(entry.entities.delete(entity))\n {\n this._rebuildSelection(entry);\n }\n }\n }\n\n clear(layerName : string) : void\n {\n const entry = this._layers.get(layerName);\n if(!entry)\n {\n return;\n }\n\n entry.entities.clear();\n entry.layer.clearSelection();\n }\n\n dispose() : void\n {\n for(const entry of this._layers.values())\n {\n entry.entities.clear();\n entry.layer.dispose();\n }\n\n this._layers.clear();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private\n //------------------------------------------------------------------------------------------------------------------\n\n private _rebuildSelection(entry : OutlineLayerEntry) : void\n {\n entry.layer.clearSelection();\n\n for(const entity of entry.entities)\n {\n if(entity.node)\n {\n const meshes = entity.node.getChildMeshes();\n if(meshes.length > 0)\n {\n entry.layer.addSelection(meshes);\n }\n }\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// GameLevel Class - Default Level Implementation\n//----------------------------------------------------------------------------------------------------------------------\n\nimport {\n type AbstractMesh,\n ArcRotateCamera,\n type Camera,\n Color3,\n CubeTexture,\n DirectionalLight,\n FreeCamera,\n HDRCubeTexture,\n HemisphericLight,\n MeshBuilder,\n PBRMaterial,\n PointLight,\n Quaternion,\n RectAreaLight,\n Scene,\n SoundState,\n SpotLight,\n StandardMaterial,\n type StaticSound,\n Texture,\n TransformNode,\n UniversalCamera,\n Vector3,\n} from '@babylonjs/core';\nimport { GeospatialCamera } from '@babylonjs/core/Cameras/geospatialCamera';\nimport { ClusteredLightContainer } from '@babylonjs/core/Lights/Clustered/clusteredLightContainer';\n\nimport type {\n CameraDefinition,\n ColorConfig,\n LevelConfig,\n LevelContext,\n LightDefinition,\n SpawnDefinition,\n} from '../interfaces/level.ts';\nimport { applyPostProcessing } from '../handlers/postProcessing.ts';\nimport { getCanonicalTransform, toQuatObject, toVec3Object, toVector3 } from '../utils/vectors.ts';\n\nimport type { GameEntity } from './entity.ts';\nimport { Level } from './level.ts';\nimport { OutlineManager } from '../managers/outline.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Metadata collected from a spawn point node.\n * Transforms are normalized to canonical Babylon space — the source level's __root__\n * conversion is removed so these values are clean for any entity type.\n */\ninterface SpawnPointData\n{\n name : string;\n position : Vector3;\n rotation : Quaternion;\n scaling : Vector3;\n node : TransformNode;\n}\n\n/**\n * Metadata collected from an entity node\n */\ninterface EntityNodeData\n{\n type : string;\n node : TransformNode;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Default Level implementation that loads from YAML configuration.\n * Supports property handlers for processing scene node metadata.\n */\nexport class GameLevel extends Level\n{\n /** The level configuration */\n protected _config : LevelConfig;\n\n /** Collected spawn points from the scene */\n protected _spawnPoints : SpawnPointData[] = [];\n\n /** Collected entity nodes from the scene */\n protected _entityNodes : EntityNodeData[] = [];\n\n /** Entities spawned by this level */\n protected _spawnedEntities : GameEntity[] = [];\n\n /** Level-scoped sounds created from config */\n protected _levelSounds = new Map<string, StaticSound>();\n\n /** Sounds that were playing before deactivation (for resume on activate) */\n private _playingSoundsBeforeDeactivate = new Set<string>();\n\n //------------------------------------------------------------------------------------------------------------------\n // Constructor\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a GameLevel from a configuration object\n *\n * @param config\n * @param context\n */\n constructor(config : LevelConfig, context : LevelContext)\n {\n super(config, context);\n this._config = config;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Accessors\n //------------------------------------------------------------------------------------------------------------------\n\n get config() : LevelConfig\n {\n return this._config;\n }\n\n get spawnedEntities() : readonly GameEntity[]\n {\n return this._spawnedEntities;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Protected Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Build the scene by loading assets, processing node metadata, and spawning entities.\n */\n protected async buildScene() : Promise<Scene>\n {\n const { sceneEngine } = this.gameEngine.engines;\n const { assetManager } = this.gameEngine.managers;\n\n // Preload assets if configured\n if(this._config.preload && this._config.preload.length > 0)\n {\n this.$emitProgress(2, 'Preloading assets...');\n await assetManager.preload(this._config.preload);\n }\n\n // Create an empty scene and assign it early so property handlers can access it\n const scene = sceneEngine.createScene();\n this._scene = scene;\n\n // Enable physics if configured (must be done before loading scene with colliders)\n if(this._config.physics)\n {\n this.$emitProgress(5, 'Initializing physics...');\n await this._enablePhysics(scene);\n }\n\n // Load the scene file if specified\n if(this._config.scene)\n {\n this.$emitProgress(10, 'Loading scene file...');\n await this._loadSceneFile(scene);\n }\n\n // Set up environment (skybox + IBL) — before cameras so the background is ready\n if(this._config.environment)\n {\n this.$emitProgress(15, 'Setting up environment...');\n this._processEnvironment(scene);\n }\n\n // Process cameras (must happen before post-processing, which requires activeCamera)\n this._processCameras(scene);\n\n // Process lights (override Blender-imported lights, create new ones)\n this._processLights(scene);\n\n // Set up clustered lighting if configured (must be after _processLights)\n this._setupClustering(scene);\n\n // Apply post-processing effects if configured\n if(this._config.postProcessing)\n {\n const renderer = this._config.postProcessing.renderer ?? 'pipeline';\n this._log.info(`Applying post-processing with ${ renderer } renderer`);\n await applyPostProcessing(scene, this._config.postProcessing);\n }\n\n // Create outline manager for entity highlighting\n this.outlines = new OutlineManager(scene, this._config.outlines);\n\n // Process all nodes for property handlers\n this.$emitProgress(50, 'Processing scene properties...');\n await this._processNodeProperties(scene);\n\n // Process spawn points\n this.$emitProgress(70, 'Spawning entities...');\n await this._processSpawnPoints();\n\n // Process entity nodes\n this.$emitProgress(85, 'Configuring entities...');\n await this._processEntityNodes();\n\n // Load level-scoped sounds (last — everything else is ready)\n if(this._config.sounds)\n {\n this.$emitProgress(95, 'Loading sounds...');\n await this._processLevelSounds();\n }\n\n return scene;\n }\n\n /**\n * Import all meshes from the configured scene file (GLB/GLTF/Babylon) into the scene.\n */\n private async _loadSceneFile(scene : Scene) : Promise<void>\n {\n const scenePath = this._getScenePath();\n if(!scenePath)\n {\n return;\n }\n\n this._log.debug(`Loading scene file: ${ scenePath }`);\n\n // Set right-handed coordinate system before loading so loaders respect it\n if(this._isRightHanded())\n {\n scene.useRightHandedSystem = true;\n this._log.debug('Scene set to right-handed coordinate system');\n }\n\n try\n {\n await this.gameEngine.engines.sceneEngine.importMeshes([], scenePath, scene);\n this._log.debug('Scene file loaded successfully');\n }\n catch (error)\n {\n this._log.error(`Failed to load scene file: ${ scenePath }`, error);\n throw error;\n }\n }\n\n /**\n * Extract the scene file path from the config, which can be a string or SceneConfig object.\n */\n private _getScenePath() : string | undefined\n {\n if(!this._config.scene)\n {\n return undefined;\n }\n\n return typeof this._config.scene === 'string'\n ? this._config.scene\n : this._config.scene.path;\n }\n\n /**\n * Check if the scene config specifies a right-handed coordinate system.\n */\n private _isRightHanded() : boolean\n {\n if(!this._config.scene || typeof this._config.scene === 'string')\n {\n return false;\n }\n\n return this._config.scene.rightHanded === true;\n }\n\n /**\n * Set up the scene environment: IBL (image-based lighting) for PBR reflections and/or a visible skybox.\n */\n private _processEnvironment(scene : Scene) : void\n {\n const env = this._config.environment;\n if(!env) { return; }\n\n // Set up IBL for PBR reflections\n if(env.ibl)\n {\n scene.environmentTexture = this._createEnvironmentTexture(env.ibl, env.iblResolution ?? 256, scene);\n this._log.debug(`IBL set from: ${ env.ibl }`);\n }\n\n // Set up visible skybox\n if(env.skybox)\n {\n const ext = this._getFileExtension(env.skybox);\n\n if(ext === '.hdr' || ext === '.env')\n {\n // HDR/env skybox — use as IBL too if no separate IBL was specified\n if(!env.ibl)\n {\n const res = env.iblResolution ?? 256;\n scene.environmentTexture = this._createEnvironmentTexture(env.skybox, res, scene);\n }\n\n if(scene.environmentTexture)\n {\n scene.createDefaultSkybox(scene.environmentTexture, true, env.skyboxSize ?? 1000, 0, false);\n }\n }\n else\n {\n // JPG/PNG — create a textured sphere\n const size = env.skyboxSize ?? 1000;\n const skybox = MeshBuilder.CreateSphere('skybox', { diameter: size, segments: 32 }, scene);\n\n const mat = new StandardMaterial('skybox-mat', scene);\n mat.backFaceCulling = false;\n mat.disableLighting = true;\n mat.emissiveTexture = new Texture(env.skybox, scene);\n skybox.material = mat;\n skybox.infiniteDistance = true;\n }\n\n this._log.debug(`Skybox set from: ${ env.skybox }`);\n }\n\n // Apply rotation to environment texture\n if(env.rotation !== undefined && scene.environmentTexture)\n {\n (scene.environmentTexture as HDRCubeTexture).rotationY = env.rotation;\n }\n }\n\n /**\n * Create an environment texture from an HDR or env file.\n */\n private _createEnvironmentTexture(path : string, resolution : number, scene : Scene) : CubeTexture | HDRCubeTexture\n {\n const ext = this._getFileExtension(path);\n\n if(ext === '.env')\n {\n return CubeTexture.CreateFromPrefilteredData(path, scene);\n }\n\n return new HDRCubeTexture(path, scene, resolution);\n }\n\n private _getFileExtension(path : string) : string\n {\n const dotIndex = path.lastIndexOf('.');\n return dotIndex >= 0 ? path.slice(dotIndex).toLowerCase() : '';\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Process cameras: apply YAML overrides to imported cameras, create new ones, and set the active camera.\n * If no YAML cameras config exists, the first imported camera (if any) is activated.\n */\n private _processCameras(scene : Scene) : void\n {\n const cameraConfigs = this._config.cameras;\n const canvas = this.gameEngine.canvas as HTMLCanvasElement | null;\n\n if(!cameraConfigs)\n {\n // No YAML config — if the scene has imported cameras, activate the first one\n if(scene.cameras.length > 0)\n {\n scene.activeCamera = scene.cameras[0];\n\n if(canvas)\n {\n scene.activeCamera.attachControl(canvas, true);\n }\n\n this._log.debug(`Activated imported camera: \"${ scene.activeCamera.name }\"`);\n }\n\n return;\n }\n\n let activeCamera : Camera | null = null;\n\n for(const [ name, config ] of Object.entries(cameraConfigs))\n {\n const existing = scene.getCameraByName(name);\n\n if(existing)\n {\n this._applyCameraConfig(existing, config);\n this._log.debug(`Applied overrides to camera: \"${ name }\"`);\n\n if(!activeCamera || config.active)\n {\n activeCamera = existing;\n }\n }\n else if(config.type)\n {\n const camera = this._createCamera(name, config, scene);\n this._log.debug(`Created ${ config.type } camera: \"${ name }\"`);\n\n if(!activeCamera || config.active)\n {\n activeCamera = camera;\n }\n }\n else\n {\n this._log.warn(\n `Camera \"${ name }\" not found in scene and no type specified — skipping`\n );\n }\n }\n\n if(activeCamera)\n {\n scene.activeCamera = activeCamera;\n\n const activeDef = cameraConfigs[activeCamera.name];\n if(canvas && activeDef?.attachControl !== false)\n {\n activeCamera.attachControl(canvas, true);\n }\n }\n }\n\n /**\n * Create a new camera from a YAML definition.\n */\n private _createCamera(name : string, config : CameraDefinition, scene : Scene) : Camera\n {\n const pos = config.position ? toVector3(config.position) : Vector3.Zero();\n\n let camera : Camera;\n\n switch (config.type)\n {\n case 'arcRotate':\n {\n const target = config.target ? toVector3(config.target) : Vector3.Zero();\n\n camera = new ArcRotateCamera(\n name,\n config.alpha ?? Math.PI / 2,\n config.beta ?? Math.PI / 3,\n config.radius ?? 10,\n target,\n scene\n );\n break;\n }\n\n case 'universal':\n {\n camera = new UniversalCamera(name, pos, scene);\n break;\n }\n\n case 'geospatial':\n {\n if(!config.planetRadius)\n {\n this._log.warn(`Geospatial camera \"${ name }\" requires planetRadius -- skipping`);\n return new FreeCamera(name, Vector3.Zero(), scene);\n }\n\n const cam = new GeospatialCamera(name, scene, {\n planetRadius: config.planetRadius,\n });\n\n if(config.center) { cam.center = toVector3(config.center); }\n if(config.yaw !== undefined) { cam.yaw = config.yaw; }\n if(config.pitch !== undefined) { cam.pitch = config.pitch; }\n if(config.radius !== undefined) { cam.radius = config.radius; }\n if(config.checkCollisions !== undefined) { cam.checkCollisions = config.checkCollisions; }\n\n if(config.radiusMin !== undefined) { cam.limits.radiusMin = config.radiusMin; }\n if(config.radiusMax !== undefined) { cam.limits.radiusMax = config.radiusMax; }\n if(config.pitchMin !== undefined) { cam.limits.pitchMin = config.pitchMin; }\n if(config.pitchMax !== undefined) { cam.limits.pitchMax = config.pitchMax; }\n if(config.yawMin !== undefined) { cam.limits.yawMin = config.yawMin; }\n if(config.yawMax !== undefined) { cam.limits.yawMax = config.yawMax; }\n\n camera = cam;\n break;\n }\n\n case 'free':\n default:\n {\n camera = new FreeCamera(name, pos, scene);\n break;\n }\n }\n\n this._applyCameraConfig(camera, config);\n return camera;\n }\n\n /**\n * Apply shared camera properties from a config to an existing camera.\n */\n private _applyCameraConfig(camera : Camera, config : CameraDefinition) : void\n {\n if(config.fov !== undefined) { camera.fov = config.fov; }\n if(config.position) { camera.position = toVector3(config.position); }\n if(config.minZ !== undefined) { camera.minZ = config.minZ; }\n if(config.maxZ !== undefined) { camera.maxZ = config.maxZ; }\n\n if(camera instanceof FreeCamera)\n {\n if(config.speed !== undefined) { camera.speed = config.speed; }\n if(config.rotation) { camera.rotation = toVector3(config.rotation); }\n }\n\n if(camera instanceof ArcRotateCamera)\n {\n if(config.target) { camera.setTarget(toVector3(config.target)); }\n if(config.lowerRadiusLimit !== undefined) { camera.lowerRadiusLimit = config.lowerRadiusLimit; }\n if(config.upperRadiusLimit !== undefined) { camera.upperRadiusLimit = config.upperRadiusLimit; }\n if(config.lowerBetaLimit !== undefined) { camera.lowerBetaLimit = config.lowerBetaLimit; }\n if(config.upperBetaLimit !== undefined) { camera.upperBetaLimit = config.upperBetaLimit; }\n if(config.wheelPrecision !== undefined) { camera.wheelPrecision = config.wheelPrecision; }\n }\n\n if(camera instanceof GeospatialCamera)\n {\n if(config.yaw !== undefined) { camera.yaw = config.yaw; }\n if(config.pitch !== undefined) { camera.pitch = config.pitch; }\n if(config.radius !== undefined) { camera.radius = config.radius; }\n if(config.center) { camera.center = toVector3(config.center); }\n if(config.checkCollisions !== undefined) { camera.checkCollisions = config.checkCollisions; }\n if(config.radiusMin !== undefined) { camera.limits.radiusMin = config.radiusMin; }\n if(config.radiusMax !== undefined) { camera.limits.radiusMax = config.radiusMax; }\n if(config.pitchMin !== undefined) { camera.limits.pitchMin = config.pitchMin; }\n if(config.pitchMax !== undefined) { camera.limits.pitchMax = config.pitchMax; }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Process lights: apply YAML overrides to imported lights and create new ones.\n * If no YAML lights config exists, imported lights are left unchanged.\n */\n private _processLights(scene : Scene) : void\n {\n const lightConfigs = this._config.lights;\n if(!lightConfigs) { return; }\n\n for(const [ name, config ] of Object.entries(lightConfigs))\n {\n const existing = scene.getLightByName(name);\n\n if(existing)\n {\n this._applyLightConfig(existing, config);\n this._log.debug(`Applied overrides to light: \"${ name }\"`);\n }\n else if(config.type)\n {\n this._createLight(name, config, scene);\n this._log.debug(`Created ${ config.type } light: \"${ name }\"`);\n }\n else\n {\n this._log.warn(\n `Light \"${ name }\" not found in scene and no type specified — skipping`\n );\n }\n }\n }\n\n /**\n * Create a new light from a YAML definition.\n */\n private _createLight(name : string, config : LightDefinition, scene : Scene) : void\n {\n const dir = config.direction ? toVector3(config.direction) : new Vector3(0, -1, 0);\n const pos = config.position ? toVector3(config.position) : Vector3.Zero();\n\n switch (config.type)\n {\n case 'hemispheric':\n {\n const light = new HemisphericLight(name, dir, scene);\n\n if(config.groundColor)\n {\n light.groundColor = this._toColor3(config.groundColor);\n }\n\n this._applyLightConfig(light, config);\n break;\n }\n\n case 'directional':\n {\n const light = new DirectionalLight(name, dir, scene);\n this._applyLightConfig(light, config);\n break;\n }\n\n case 'point':\n {\n const light = new PointLight(name, pos, scene);\n this._applyLightConfig(light, config);\n break;\n }\n\n case 'spot':\n {\n const light = new SpotLight(\n name,\n pos,\n dir,\n config.angle ?? Math.PI / 3,\n config.exponent ?? 2,\n scene\n );\n this._applyLightConfig(light, config);\n break;\n }\n\n case 'rectarea':\n {\n const light = new RectAreaLight(\n name,\n pos,\n config.width ?? 1,\n config.height ?? 1,\n scene\n );\n this._applyLightConfig(light, config);\n\n // RectAreaLight emits along local -Z and has no direction property. If a direction\n // is specified, parent the light to a rotated pivot node that orients -Z toward it.\n if(config.direction)\n {\n const pivot = new TransformNode(`${ name }_pivot`, scene);\n pivot.position = light.position.clone();\n light.position = Vector3.Zero();\n light.parent = pivot;\n\n // lookAt aligns +Z toward the target; we want -Z toward dir, so look opposite\n pivot.lookAt(pivot.position.subtract(dir));\n }\n\n break;\n }\n }\n }\n\n /**\n * Apply shared light properties from a config to an existing light.\n */\n private _applyLightConfig(\n light : { intensity : number; diffuse : Color3; specular : Color3 },\n config : LightDefinition\n ) : void\n {\n if(config.intensity !== undefined) { light.intensity = config.intensity; }\n if(config.diffuse) { light.diffuse = this._toColor3(config.diffuse); }\n if(config.specular) { light.specular = this._toColor3(config.specular); }\n }\n\n /**\n * Set up clustered lighting: create a container, move eligible lights into it,\n * and fix PBR material falloff for compatibility.\n */\n private _setupClustering(scene : Scene) : void\n {\n const clusterConfig = this._config.clustering;\n if(!clusterConfig?.enabled)\n {\n return;\n }\n\n // Collect eligible lights from the scene\n const eligibleLights = scene.lights.filter(\n (light) => ClusteredLightContainer.IsLightSupported(light)\n );\n\n if(eligibleLights.length === 0)\n {\n this._log.debug('Clustering enabled but no eligible lights found');\n return;\n }\n\n // Remove eligible lights from the scene before adding to container\n for(const light of eligibleLights)\n {\n scene.removeLight(light);\n }\n\n // Create the container with collected lights\n const container = new ClusteredLightContainer('sage-clustered', eligibleLights, scene);\n\n // Apply tuning parameters\n if(clusterConfig.horizontalTiles !== undefined) { container.horizontalTiles = clusterConfig.horizontalTiles; }\n if(clusterConfig.verticalTiles !== undefined) { container.verticalTiles = clusterConfig.verticalTiles; }\n if(clusterConfig.depthSlices !== undefined) { container.depthSlices = clusterConfig.depthSlices; }\n if(clusterConfig.maxRange !== undefined) { container.maxRange = clusterConfig.maxRange; }\n\n this.clusteredLights = container;\n\n this._log.debug(\n `Clustered lighting enabled: ${ eligibleLights.length } lights, `\n + `${ container.horizontalTiles }x${ container.verticalTiles } tiles, `\n + `${ container.depthSlices } depth slices`\n );\n\n // Fix PBR material falloff — physical falloff is incompatible with clustering\n for(const material of scene.materials)\n {\n if(material instanceof PBRMaterial)\n {\n material.useGLTFLightFalloff = true;\n }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n private _toColor3(color : ColorConfig) : Color3\n {\n return new Color3(color.r, color.g, color.b);\n }\n\n /**\n * Configure and enable the Havok physics plugin with the level's gravity settings.\n */\n private async _enablePhysics(scene : Scene) : Promise<void>\n {\n const physicsConfig = this._config.physics;\n\n // Determine gravity\n let gravity = new Vector3(0, -9.81, 0); // Default gravity\n\n if(typeof physicsConfig === 'object' && physicsConfig.gravity)\n {\n gravity = new Vector3(\n physicsConfig.gravity.x,\n physicsConfig.gravity.y,\n physicsConfig.gravity.z\n );\n }\n\n // When large world rendering is active, enable Havok multi-region physics\n const floatingOriginRadius = this.gameEngine.largeWorldRendering\n ? 100000\n : undefined;\n\n this._log.debug(`Enabling physics with gravity: ${ gravity.toString() }`);\n await this.gameEngine.engines.sceneEngine.enablePhysics(scene, gravity, floatingOriginRadius);\n }\n\n /**\n * Walk all transform nodes and meshes, normalizing glTF metadata and dispatching to property handlers.\n */\n private async _processNodeProperties(scene : Scene) : Promise<void>\n {\n const nodes = scene.transformNodes.concat(scene.meshes as TransformNode[]);\n\n for(const node of nodes)\n {\n // Normalize metadata - glTF extras are in node.metadata.gltf.extras\n const metadata = this._getNormalizedMetadata(node);\n\n if(metadata && Object.keys(metadata).length > 0)\n {\n // Store normalized metadata back on node for property handlers\n node.metadata = { ...node.metadata, ...metadata };\n\n // eslint-disable-next-line no-await-in-loop\n await this._processNodeMetadata(node);\n }\n }\n\n this._log.debug(\n `Processed ${ nodes.length } nodes, found ${ this._spawnPoints.length } spawn points `\n + `and ${ this._entityNodes.length } entity nodes`\n );\n }\n\n /**\n * Get normalized metadata from a node, handling glTF extras.\n *\n * Babylon.js stores glTF custom properties in node.metadata.gltf.extras,\n * so we need to extract them for easier access.\n */\n private _getNormalizedMetadata(node : TransformNode) : Record<string, unknown> | null\n {\n if(!node.metadata)\n {\n return null;\n }\n\n // Check for glTF extras (from imported GLB/GLTF files)\n const gltfExtras = node.metadata?.gltf?.extras as Record<string, unknown> | undefined;\n\n if(gltfExtras)\n {\n // Merge glTF extras with any existing top-level metadata\n return { ...gltfExtras };\n }\n\n // Return existing metadata if no glTF extras\n return node.metadata as Record<string, unknown>;\n }\n\n /**\n * Check a node's metadata for spawn/entity markers and run any registered property handlers.\n */\n private async _processNodeMetadata(node : TransformNode) : Promise<void>\n {\n // Normalize the spawn marker into canonical Babylon space. glTF imports\n // wrap content under a conversion root (RH→LH), and decomposing a\n // mirrored matrix produces poisoned scaling/rotation. getCanonicalTransform\n // cancels the import root before decomposing, giving clean values that\n // work uniformly for primitives and GLB entities.\n if('spawn' in node.metadata)\n {\n const { position, rotation, scaling } = getCanonicalTransform(node);\n\n this._spawnPoints.push({\n name: node.metadata.spawn as string,\n position,\n rotation,\n scaling,\n node,\n });\n }\n\n // Check for entity property\n if('entity' in node.metadata)\n {\n this._entityNodes.push({\n type: node.metadata.entity as string,\n node,\n });\n }\n\n // Run registered property handlers\n await this._runPropertyHandlers(node);\n }\n\n /**\n * Run registered property handlers on a node.\n */\n private async _runPropertyHandlers(node : TransformNode) : Promise<void>\n {\n for(const [ property, handler ] of this.propertyHandlers)\n {\n if(property in node.metadata)\n {\n try\n {\n // eslint-disable-next-line no-await-in-loop\n await handler(node, node.metadata[property], this, this.gameEngine);\n }\n catch (error)\n {\n this._log.error(\n `Error in property handler '${ property }' for node '${ node.name }':`,\n error\n );\n }\n }\n }\n }\n\n /**\n * Match collected spawn points against the level config's spawn definitions and instantiate entities.\n */\n private async _processSpawnPoints() : Promise<void>\n {\n for(const spawnPoint of this._spawnPoints)\n {\n const spawnDef = this._config.spawns?.[spawnPoint.name];\n\n if(!spawnDef)\n {\n this._log.warn(`No spawn definition found for '${ spawnPoint.name }', skipping`);\n }\n else\n {\n // eslint-disable-next-line no-await-in-loop\n await this._processSpawnPoint(spawnPoint, spawnDef);\n }\n }\n }\n\n /**\n * Spawn an entity at the given spawn point and dispose of the placeholder node.\n */\n private async _processSpawnPoint(spawnPoint : SpawnPointData, spawnDef : SpawnDefinition) : Promise<void>\n {\n try\n {\n const entity = await this._spawnEntity(spawnDef, spawnPoint);\n this._spawnedEntities.push(entity);\n\n // Remove the placeholder node from the scene\n spawnPoint.node.dispose();\n\n this._log.debug(`Spawned entity '${ spawnDef.entity }' at spawn point '${ spawnPoint.name }'`);\n }\n catch (error)\n {\n this._log.error(`Failed to spawn entity at spawn point '${ spawnPoint.name }':`, error);\n }\n }\n\n /**\n * Create an entity from a spawn definition, inheriting position/rotation/scaling from the spawn point.\n */\n private async _spawnEntity(spawnDef : SpawnDefinition, spawnPoint : SpawnPointData) : Promise<GameEntity>\n {\n const initialState = {\n ...spawnDef.config,\n position: toVec3Object(spawnPoint.position),\n rotation: toQuatObject(spawnPoint.rotation),\n scaling: toVec3Object(spawnPoint.scaling),\n };\n\n const entity = await this.gameEngine.managers.entityManager.createEntity(spawnDef.entity, {\n name: spawnDef.name,\n tags: spawnDef.tags,\n initialState,\n });\n\n // Create mesh if the entity definition specifies one\n await this._createEntityMesh(entity, spawnPoint);\n\n return entity;\n }\n\n /**\n * Build or import a mesh from the entity definition and attach it to the entity.\n * Supports primitive shapes (box/sphere/capsule/cylinder) and GLB/GLTF file paths.\n */\n private async _createEntityMesh(entity : GameEntity, spawnPoint : SpawnPointData) : Promise<void>\n {\n if(!this._scene)\n {\n return;\n }\n\n // Get the entity definition to check for mesh config\n const definition = this.gameEngine.managers.entityManager.getDefinition(entity.type);\n if(!definition?.mesh)\n {\n return;\n }\n\n const meshConfig = definition.mesh;\n const scene = this._scene;\n\n const node = new TransformNode(`entity-${ entity.id }`, scene);\n\n // Create or import meshes based on source type\n let meshes : AbstractMesh[];\n\n switch (meshConfig.source)\n {\n case 'box':\n meshes = [ MeshBuilder.CreateBox(`${ entity.type }-mesh`, {\n size: meshConfig.params?.size ?? 1,\n width: meshConfig.params?.width,\n height: meshConfig.params?.height,\n depth: meshConfig.params?.depth,\n }, scene) ];\n break;\n\n case 'sphere':\n meshes = [ MeshBuilder.CreateSphere(`${ entity.type }-mesh`, {\n diameter: meshConfig.params?.diameter ?? 1,\n segments: meshConfig.params?.segments ?? 16,\n }, scene) ];\n break;\n\n case 'capsule':\n meshes = [ MeshBuilder.CreateCapsule(`${ entity.type }-mesh`, {\n height: meshConfig.params?.height ?? 1.8,\n radius: meshConfig.params?.radius ?? 0.4,\n }, scene) ];\n break;\n\n case 'cylinder':\n meshes = [ MeshBuilder.CreateCylinder(`${ entity.type }-mesh`, {\n height: meshConfig.params?.height ?? 1,\n diameter: meshConfig.params?.diameter ?? 1,\n }, scene) ];\n break;\n\n default:\n {\n // GLB/GLTF file path — import all meshes from the file\n const result = await this.gameEngine.engines.sceneEngine.importMeshes([], meshConfig.source, scene);\n\n if(result.meshes.length === 0)\n {\n this._log.warn(`No meshes loaded from: ${ meshConfig.source }`);\n node.dispose();\n return;\n }\n\n meshes = result.meshes;\n\n // Re-parent root-level imported transform nodes under the entity node\n for(const tn of result.transformNodes as TransformNode[])\n {\n if(!tn.parent)\n {\n tn.parent = node;\n }\n }\n\n break;\n }\n }\n\n // Parent any orphan meshes under the entity node.\n // Primitives are always orphans; GLB meshes are already parented in their hierarchy.\n for(const mesh of meshes)\n {\n if(!mesh.parent)\n {\n mesh.parent = node;\n }\n }\n\n // Spawn transforms are already normalized to canonical Babylon space\n // (source __root__ stripped at collection time). Apply directly —\n // the entity's own __root__ (if GLB) handles its handedness conversion.\n node.position.copyFrom(spawnPoint.position);\n node.rotationQuaternion = spawnPoint.rotation.clone();\n node.scaling.copyFrom(spawnPoint.scaling);\n\n // Entity definition scale overrides spawn point scaling\n if(meshConfig.scale)\n {\n if(typeof meshConfig.scale === 'number')\n {\n node.scaling.setAll(meshConfig.scale);\n }\n else\n {\n node.scaling.set(meshConfig.scale.x, meshConfig.scale.y, meshConfig.scale.z);\n }\n }\n\n // Apply material override (typically used for primitives; GLB files usually bring their own materials)\n const matConfig = meshConfig.material;\n if(matConfig?.color)\n {\n const color = matConfig.color;\n\n if(matConfig.type === 'pbr')\n {\n const material = new PBRMaterial(`${ entity.type }-material`, scene);\n material.albedoColor = this._toColor3(color);\n\n if(matConfig.emissive)\n {\n material.emissiveColor = this._toColor3(matConfig.emissive);\n }\n\n material.metallic = matConfig.metallic ?? 0;\n material.roughness = matConfig.roughness ?? 1;\n\n for(const mesh of meshes)\n {\n mesh.material = material;\n }\n }\n else\n {\n const material = new StandardMaterial(`${ entity.type }-material`, scene);\n material.diffuseColor = this._toColor3(color);\n\n if(matConfig.emissive)\n {\n material.emissiveColor = this._toColor3(matConfig.emissive);\n }\n\n for(const mesh of meshes)\n {\n mesh.material = material;\n }\n }\n }\n\n // Process property handlers (colliders, etc.) on imported nodes.\n // These don't go through _processNodeProperties automatically.\n // Use getDescendants() to avoid duplicates from meshes + getChildren() overlap.\n const descendants = node.getDescendants(false)\n .filter((child) : child is TransformNode => child instanceof TransformNode);\n\n for(const imported of descendants)\n {\n const metadata = this._getNormalizedMetadata(imported);\n\n if(metadata && Object.keys(metadata).length > 0)\n {\n imported.metadata = {\n ...imported.metadata,\n ...metadata,\n };\n\n // eslint-disable-next-line no-await-in-loop\n await this._runPropertyHandlers(imported);\n }\n }\n\n // Attach the node to the entity\n this.gameEngine.managers.entityManager.attachToNode(entity, node);\n }\n\n /**\n * Create game entities for all nodes marked with the 'entity' metadata property.\n */\n private async _processEntityNodes() : Promise<void>\n {\n for(const entityNode of this._entityNodes)\n {\n // eslint-disable-next-line no-await-in-loop\n await this._processEntityNode(entityNode);\n }\n }\n\n /**\n * Create a game entity for an entity-tagged node, attaching the existing scene node to it.\n */\n private async _processEntityNode(entityNode : EntityNodeData) : Promise<void>\n {\n const entityDef = this._config.entities?.[entityNode.type];\n\n try\n {\n const initialState = {\n ...entityDef?.config,\n position: toVec3Object(entityNode.node.position),\n };\n\n const entity = await this.gameEngine.managers.entityManager.createEntity(entityNode.type, {\n name: entityDef?.name ?? entityNode.node.name,\n tags: entityDef?.tags,\n initialState,\n node: entityNode.node,\n });\n this._spawnedEntities.push(entity);\n\n this._log.debug(`Created entity '${ entityNode.type }' for node '${ entityNode.node.name }'`);\n }\n catch (error)\n {\n this._log.error(`Failed to create entity for node '${ entityNode.node.name }':`, error);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Level Sounds\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create level-scoped sounds from YAML config. Sounds are tracked for lifecycle management\n * (pause on deactivate, resume on activate, dispose on unload).\n */\n private async _processLevelSounds() : Promise<void>\n {\n const soundConfigs = this._config.sounds;\n if(!soundConfigs) { return; }\n\n const { audioManager } = this.gameEngine.managers;\n if(!audioManager)\n {\n this._log.warn('No AudioManager configured. Level sounds will not be created.');\n return;\n }\n\n for(const [ name, config ] of Object.entries(soundConfigs))\n {\n try\n {\n // eslint-disable-next-line no-await-in-loop\n const sound = await audioManager.createSound(name, config.url, config.channel, {\n loop: config.loop ?? false,\n volume: config.volume ?? 1,\n });\n\n this._levelSounds.set(name, sound);\n\n if(config.autoplay)\n {\n sound.play();\n }\n\n this._log.debug(`Created level sound: \"${ name }\"`);\n }\n catch (error)\n {\n this._log.error(`Failed to create level sound \"${ name }\":`, error);\n }\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Pause all playing level sounds when the level is deactivated.\n * Subclasses that override this must call `super.onDeactivate()`.\n */\n async onDeactivate() : Promise<void>\n {\n for(const [ name, sound ] of this._levelSounds)\n {\n if(sound.state === SoundState.Started)\n {\n this._playingSoundsBeforeDeactivate.add(name);\n sound.pause();\n }\n }\n }\n\n /**\n * Resume previously playing level sounds when the level is reactivated.\n * Subclasses that override this must call `super.onActivate()`.\n */\n async onActivate() : Promise<void>\n {\n for(const name of this._playingSoundsBeforeDeactivate)\n {\n const sound = this._levelSounds.get(name);\n if(sound)\n {\n sound.play();\n }\n }\n\n this._playingSoundsBeforeDeactivate.clear();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Dispose of this level's resources\n */\n override async $dispose() : Promise<void>\n {\n // Destroy all spawned entities\n const { entityManager } = this.gameEngine.managers;\n const destroyPromises = this._spawnedEntities.map(async (entity) =>\n {\n try\n {\n await entityManager.destroyEntity(entity.id);\n }\n catch (error)\n {\n this._log.error(`Failed to destroy entity ${ entity.id }:`, error);\n }\n });\n\n await Promise.all(destroyPromises);\n\n this._spawnedEntities = [];\n this._spawnPoints = [];\n this._entityNodes = [];\n\n // Dispose level sounds\n for(const sound of this._levelSounds.values())\n {\n sound.dispose();\n }\n this._levelSounds.clear();\n this._playingSoundsBeforeDeactivate.clear();\n\n await super.$dispose();\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Level Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type {\n LevelConfig,\n LevelConstructor,\n LevelContext,\n PropertyHandler,\n} from '../interfaces/level.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type {\n LevelCompletePayload,\n LevelErrorPayload,\n LevelProgressPayload,\n} from '../events/payloads.ts';\n\nimport { Level } from '../classes/level.ts';\nimport { GameLevel } from '../classes/gameLevel.ts';\nimport { GameEventBus } from '../classes/eventBus.ts';\nimport type { GameEngine } from '../classes/gameEngine.ts';\n\n// Utils\nimport { type LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Options for level transitions.\n */\nexport interface TransitionOptions\n{\n /** If true, the old level is kept loaded (not disposed) after transitioning away */\n keepAlive ?: boolean;\n\n /** If true, the new level is loaded but not activated — currentLevel remains unchanged */\n preloadOnly ?: boolean;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Manages game levels, acting as a factory for level instantiation.\n *\n * The LevelManager is responsible for:\n * - Registering level configurations and custom level classes\n * - Creating level instances with properly injected dependencies\n * - Managing the lifecycle of loaded levels (loading, activation, disposal)\n *\n * Levels are not instantiated directly. Instead, use the manager's factory methods:\n *\n * ```typescript\n * // Register a config\n * levelManager.registerLevelConfig({ name: 'Level1', scene: 'level1.glb' });\n *\n * // Register a custom level class\n * levelManager.registerLevelClass('custom', MyCustomLevel);\n *\n * // Load creates the instance with deps injected\n * await levelManager.loadLevel('Level1');\n * ```\n */\nexport class LevelManager implements Disposable\n{\n private _eventBus : GameEventBus;\n private _gameEngine : GameEngine | null = null;\n private _log : LoggerInterface;\n private _logger ?: LoggingUtility;\n\n /** Map of registered level configurations by name */\n private _levelConfigs = new Map<string, LevelConfig>();\n\n /** Map of registered custom level classes by name */\n private _levelClasses = new Map<string, LevelConstructor>();\n\n /** Map of loaded level instances by name */\n private _loadedLevels = new Map<string, Level>();\n\n /** Map of registered property handlers for scene node metadata */\n private _propertyHandlers = new Map<string, PropertyHandler>();\n\n /** The currently active level */\n private _currentLevel : Level | null = null;\n\n /** Unsubscribe functions for event subscriptions */\n private _eventUnsubscribers : (() => void)[] = [];\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Gets the currently active level\n */\n public get currentLevel() : Level | null\n {\n return this._currentLevel;\n }\n\n /**\n * Gets the registered property handlers\n */\n public get propertyHandlers() : Map<string, PropertyHandler>\n {\n return this._propertyHandlers;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n\n constructor(\n eventBus : GameEventBus,\n logger ?: LoggingUtility\n )\n {\n this._eventBus = eventBus;\n this._logger = logger;\n this._log = logger?.getLogger('LevelManager') || new SAGELogger('LevelManager');\n\n // Subscribe to level events and store unsubscribe handles\n this._eventUnsubscribers.push(\n this._eventBus.subscribe('level:progress', (event) =>\n {\n this._handleProgress(event.payload);\n })\n );\n\n this._eventUnsubscribers.push(\n this._eventBus.subscribe('level:complete', (event) =>\n {\n this._handleComplete(event.payload);\n })\n );\n\n this._eventUnsubscribers.push(\n this._eventBus.subscribe('level:error', (event) =>\n {\n this._handleError(event.payload);\n })\n );\n\n this._log.info('LevelManager initialized');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Internal API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Late-bind the GameEngine after construction.\n * Called by createGameEngine() after the GameEngine instance is created.\n */\n $setGameEngine(gameEngine : GameEngine) : void\n {\n this._gameEngine = gameEngine;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods\n //------------------------------------------------------------------------------------------------------------------\n\n private _handleProgress(payload : LevelProgressPayload) : void\n {\n this._log.debug(`Level ${ payload.levelName } progress: ${ payload.progress }%`);\n }\n\n private _handleComplete(payload : LevelCompletePayload) : void\n {\n this._log.info(`Level ${ payload.levelName } loaded successfully`);\n }\n\n private _handleError(payload : LevelErrorPayload) : void\n {\n this._log.error(`Level ${ payload.levelName } error: ${ payload.message }`);\n }\n\n /**\n * Creates the LevelContext with all dependencies\n */\n private _createContext() : LevelContext\n {\n if(!this._gameEngine)\n {\n throw new Error('LevelManager: gameEngine not set. Call $setGameEngine() first.');\n }\n\n return {\n gameEngine: this._gameEngine,\n propertyHandlers: this._propertyHandlers,\n logger: this._logger,\n };\n }\n\n /**\n * Creates a Level instance from a config using the appropriate class\n */\n private _createLevelInstance(config : LevelConfig) : Level\n {\n const context = this._createContext();\n\n if(config.class)\n {\n // Look up the registered class\n const LevelClass = this._levelClasses.get(config.class);\n if(!LevelClass)\n {\n throw new Error(`Level class '${ config.class }' is not registered.`);\n }\n\n this._log.debug(`Creating level '${ config.name }' using class '${ config.class }'`);\n return new LevelClass(config, context) as Level;\n }\n\n // Use GameLevel as default\n this._log.debug(`Creating level '${ config.name }' using GameLevel`);\n return new GameLevel(config, context);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Configuration Registration API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Registers a level configuration.\n * The level will be instantiated when loadLevel() is called.\n *\n * @param config\n */\n public registerLevelConfig(config : LevelConfig) : void\n {\n if(this._levelConfigs.has(config.name))\n {\n this._log.warn(`Level config '${ config.name }' is already registered. Overwriting.`);\n }\n\n this._levelConfigs.set(config.name, config);\n this._log.info(`Registered level config: ${ config.name }`);\n }\n\n /**\n * Registers a custom Level class that can be referenced in configs.\n * When a config specifies `class: \"my-level\"`, this class will be instantiated.\n *\n * @param name - The key used in LevelConfig.class to reference this constructor\n * @param levelClass\n */\n public registerLevelClass(name : string, levelClass : LevelConstructor) : void\n {\n if(this._levelClasses.has(name))\n {\n this._log.warn(`Level class '${ name }' is already registered. Overwriting.`);\n }\n\n this._levelClasses.set(name, levelClass);\n this._log.info(`Registered level class: ${ name }`);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Property Handler API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a property handler for processing custom properties on scene nodes.\n * Handlers are called for each node that has the specified property in its metadata.\n *\n * @param property - The metadata property name to match (e.g., 'sound', 'collider')\n * @param handler\n */\n public registerPropertyHandler(property : string, handler : PropertyHandler) : void\n {\n if(this._propertyHandlers.has(property))\n {\n this._log.warn(`Property handler '${ property }' is already registered. Overwriting.`);\n }\n\n this._propertyHandlers.set(property, handler);\n this._log.debug(`Registered property handler: ${ property }`);\n }\n\n /**\n * Unregister a property handler.\n *\n * @param property\n */\n public unregisterPropertyHandler(property : string) : void\n {\n this._propertyHandlers.delete(property);\n }\n\n /**\n * Clear all registered property handlers.\n * Primarily useful for testing to ensure a clean state between tests.\n */\n public clearAllPropertyHandlers() : void\n {\n this._propertyHandlers.clear();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Level Loading API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Gets a loaded level by name.\n * Returns null if the level is not currently loaded.\n *\n * @param name\n */\n public getLevel(name : string) : Level | null\n {\n return this._loadedLevels.get(name) || null;\n }\n\n /**\n * Gets a registered level config by name.\n *\n * @param name\n */\n public getLevelConfig(name : string) : LevelConfig | null\n {\n return this._levelConfigs.get(name) || null;\n }\n\n /**\n * Loads a level by name. The level config must be registered first.\n *\n * @param name\n */\n public async loadLevel(name : string) : Promise<Level>;\n\n /**\n * Loads a level from a config. The config will be registered automatically.\n *\n * @param config\n */\n public async loadLevel(config : LevelConfig) : Promise<Level>;\n\n /**\n * Loads a level. If a name is passed, the config must be registered.\n * If a config is passed, it will be registered automatically.\n * Returns the existing instance if already loaded.\n *\n * @param configOrName\n */\n public async loadLevel(configOrName : string | LevelConfig) : Promise<Level>\n {\n // Resolve the config\n let config : LevelConfig;\n if(typeof configOrName === 'string')\n {\n const registered = this._levelConfigs.get(configOrName);\n if(!registered)\n {\n throw new Error(`Level config '${ configOrName }' is not registered.`);\n }\n config = registered;\n }\n else\n {\n config = configOrName;\n // Auto-register the config\n this.registerLevelConfig(config);\n }\n\n // Check if already loaded\n const existing = this._loadedLevels.get(config.name);\n if(existing)\n {\n this._log.warn(`Level '${ config.name }' is already loaded`);\n return existing;\n }\n\n // Create and load the level\n this._log.debug(`Loading level: ${ config.name }`);\n const level = this._createLevelInstance(config);\n await level.load();\n\n // Store the loaded level\n this._loadedLevels.set(config.name, level);\n\n return level;\n }\n\n /**\n * Activates a level by name, loading it if necessary.\n *\n * @param name\n */\n public async activateLevel(name : string) : Promise<Level>;\n\n /**\n * Activates a level from a config, loading it if necessary.\n *\n * @param config\n */\n public async activateLevel(config : LevelConfig) : Promise<Level>;\n\n /**\n * Activates a level, loading it if necessary.\n * Sets it as the currentLevel used by the render loop.\n *\n * @param configOrName\n */\n public async activateLevel(configOrName : string | LevelConfig) : Promise<Level>\n {\n const level = typeof configOrName === 'string'\n ? await this.loadLevel(configOrName)\n : await this.loadLevel(configOrName);\n this._currentLevel = level;\n this._log.info(`Activated level: ${ level.name }`);\n return level;\n }\n\n /**\n * Unloads a level and disposes its resources.\n * Clears currentLevel if this was the active level.\n *\n * @param name\n */\n public async unloadLevel(name : string) : Promise<void>\n {\n const level = this._loadedLevels.get(name);\n if(level)\n {\n // If this is the current level, clear it\n if(this._currentLevel === level)\n {\n this._currentLevel = null;\n }\n\n // Dispose the level\n await level.$dispose();\n\n // Remove from loaded levels\n this._loadedLevels.delete(name);\n\n this._log.info(`Unloaded level: ${ name }`);\n }\n }\n\n /**\n * Unloads the current level and disposes its resources\n */\n public async unloadCurrentLevel() : Promise<void>\n {\n if(this._currentLevel)\n {\n await this.unloadLevel(this._currentLevel.name);\n }\n else\n {\n this._log.warn('No current level to unload');\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Transitions\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Orchestrates a level-to-level transition with lifecycle hooks and events.\n *\n * Flow: deactivate old -> load new -> dispose old (unless keepAlive) -> activate new.\n * If `preloadOnly` is true, the new level is loaded but not activated.\n *\n * @param levelName - The name of the level to transition to (must be registered)\n * @param options\n */\n public async transition(levelName : string, options ?: TransitionOptions) : Promise<void>\n {\n const oldLevel = this._currentLevel;\n\n try\n {\n // 1. Emit start\n this._eventBus.publish({\n type: 'level:transition:start',\n payload: {\n from: oldLevel?.name,\n to: levelName,\n },\n });\n\n // 2. Deactivate the old level\n if(oldLevel?.onDeactivate)\n {\n await oldLevel.onDeactivate();\n }\n\n // 3. Emit loading progress\n this._eventBus.publish({\n type: 'level:transition:progress',\n payload: {\n stage: 'loading',\n levelName,\n },\n });\n\n // 4. Load the new level\n const newLevel = await this.loadLevel(levelName);\n\n // 5. If preloadOnly, stop here\n if(options?.preloadOnly)\n {\n return;\n }\n\n // 6. Dispose old level unless keepAlive\n if(oldLevel && !options?.keepAlive)\n {\n await this.unloadLevel(oldLevel.name);\n }\n\n // 7. Set new level as current\n this._currentLevel = newLevel;\n\n // 8. Activate the new level\n if(newLevel.onActivate)\n {\n await newLevel.onActivate();\n }\n\n // 9. Emit complete\n this._eventBus.publish({\n type: 'level:transition:complete',\n payload: { levelName },\n });\n }\n catch (error)\n {\n // 10. Emit error event and re-throw\n this._eventBus.publish({\n type: 'level:transition:error',\n payload: {\n from: oldLevel?.name,\n to: levelName,\n error,\n },\n });\n\n throw error;\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Tears down the level manager and cleans up any resources\n */\n async $teardown() : Promise<void>\n {\n // Unsubscribe from all events\n for(const unsubscribe of this._eventUnsubscribers)\n {\n unsubscribe();\n }\n this._eventUnsubscribers = [];\n\n // Dispose of all loaded levels\n for(const level of this._loadedLevels.values())\n {\n // eslint-disable-next-line no-await-in-loop\n await level.$dispose();\n }\n\n this._loadedLevels.clear();\n this._levelConfigs.clear();\n this._currentLevel = null;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Audio Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { AudioBus, IStaticSoundOptions, StaticSound } from '@babylonjs/core';\n\n// Engines\nimport type { AudioEngine } from '../engines/audio.ts';\n\n// Interfaces\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\n\n// Utils\nimport type { LoggingUtility } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Public channel info exposed to consumers (no BabylonJS internals).\n */\nexport interface ChannelInfo\n{\n volume : number;\n muted : boolean;\n}\n\n/**\n * Internal channel state including the BabylonJS bus reference.\n */\ninterface ChannelState extends ChannelInfo\n{\n bus : AudioBus;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class AudioManager implements Disposable\n{\n private _audioEngine : AudioEngine;\n private _channels = new Map<string, ChannelState>();\n private _masterMuted = false;\n private _masterVolumeBeforeMute = 1;\n private _log : LoggerInterface;\n\n constructor(audioEngine : AudioEngine, logger : LoggingUtility)\n {\n this._audioEngine = audioEngine;\n this._log = logger.getLogger('AudioManager');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n async initialize(channels : string[]) : Promise<void>\n {\n this._log.info(`Initializing audio channels: ${ channels.join(', ') }`);\n\n for(const name of channels)\n {\n // eslint-disable-next-line no-await-in-loop\n const bus = await this._audioEngine.createBus(name);\n this._channels.set(name, { bus, volume: 1, muted: false });\n }\n\n this._log.info('Audio channels initialized.');\n }\n\n async $teardown() : Promise<void>\n {\n this._log.info('Tearing down audio manager...');\n\n for(const state of this._channels.values())\n {\n state.bus.dispose();\n }\n\n this._channels.clear();\n this._log.info('Audio manager torn down.');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Sound Creation\n //------------------------------------------------------------------------------------------------------------------\n\n async createSound(\n name : string,\n url : string,\n channel ?: string,\n options ?: Partial<IStaticSoundOptions>\n ) : Promise<StaticSound>\n {\n let bus : AudioBus | undefined;\n\n if(channel)\n {\n const state = this._channels.get(channel);\n if(!state)\n {\n this._log.warn(`Unknown audio channel \"${ channel }\", sound \"${ name }\" will use default bus.`);\n }\n else\n {\n bus = state.bus;\n }\n }\n\n return this._audioEngine.createSound(name, url, bus, options);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Master Volume\n //------------------------------------------------------------------------------------------------------------------\n\n setMasterVolume(volume : number) : void\n {\n if(!this._masterMuted)\n {\n this._audioEngine.setMasterVolume(volume);\n }\n else\n {\n // Store for when we unmute\n this._masterVolumeBeforeMute = volume;\n }\n }\n\n getMasterVolume() : number\n {\n if(this._masterMuted)\n {\n return this._masterVolumeBeforeMute;\n }\n\n return this._audioEngine.getMasterVolume();\n }\n\n setMasterMuted(muted : boolean) : void\n {\n if(muted === this._masterMuted) { return; }\n\n this._masterMuted = muted;\n\n if(muted)\n {\n this._masterVolumeBeforeMute = this._audioEngine.getMasterVolume();\n this._audioEngine.setMasterVolume(0);\n }\n else\n {\n this._audioEngine.setMasterVolume(this._masterVolumeBeforeMute);\n }\n }\n\n isMasterMuted() : boolean\n {\n return this._masterMuted;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Channel Volume\n //------------------------------------------------------------------------------------------------------------------\n\n setChannelVolume(channel : string, volume : number) : void\n {\n const state = this._channels.get(channel);\n if(!state)\n {\n this._log.warn(`Unknown audio channel: ${ channel }`);\n return;\n }\n\n state.volume = volume;\n\n if(!state.muted)\n {\n this._audioEngine.setBusVolume(state.bus, volume);\n }\n }\n\n getChannelVolume(channel : string) : number\n {\n const state = this._channels.get(channel);\n return state?.volume ?? 1;\n }\n\n setChannelMuted(channel : string, muted : boolean) : void\n {\n const state = this._channels.get(channel);\n if(!state)\n {\n this._log.warn(`Unknown audio channel: ${ channel }`);\n return;\n }\n\n if(muted === state.muted) { return; }\n\n state.muted = muted;\n this._audioEngine.setBusVolume(state.bus, muted ? 0 : state.volume);\n }\n\n isChannelMuted(channel : string) : boolean\n {\n const state = this._channels.get(channel);\n return state?.muted ?? false;\n }\n\n getChannels() : string[]\n {\n return Array.from(this._channels.keys());\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Save Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type { GameEntityManager } from './entity.ts';\nimport type { LevelManager } from './level.ts';\n\n// Utils\nimport { type LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n// Types\n//----------------------------------------------------------------------------------------------------------------------\n\nexport interface SaveData\n{\n version : number;\n levelName : string;\n entities : SerializedEntity[];\n custom : Record<string, unknown>;\n}\n\nexport interface SerializedEntity\n{\n id : string;\n type : string;\n name ?: string;\n tags : string[];\n state : object;\n parentId ?: string;\n transform ?: {\n position : { x : number; y : number; z : number };\n rotation : { x : number; y : number; z : number };\n scaling : { x : number; y : number; z : number };\n };\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\ntype BeforeSerializeHook = () => Record<string, unknown>;\ntype AfterDeserializeHook = (custom : Record<string, unknown>) => void;\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class SaveManager implements Disposable\n{\n private _entityManager : GameEntityManager;\n private _levelManager : LevelManager;\n private _log : LoggerInterface;\n\n private _beforeSerializeHooks : BeforeSerializeHook[] = [];\n private _afterDeserializeHooks : AfterDeserializeHook[] = [];\n\n constructor(\n entityManager : GameEntityManager,\n levelManager : LevelManager,\n logger ?: LoggingUtility\n )\n {\n this._entityManager = entityManager;\n this._levelManager = levelManager;\n this._log = logger?.getLogger('SaveManager') || new SAGELogger('SaveManager');\n this._log.info('SaveManager initialized');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Hook Registration\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a hook that returns custom data to include in save data.\n * Multiple hooks can be registered; their results are merged into the `custom` field.\n */\n onBeforeSerialize(hook : BeforeSerializeHook) : void\n {\n this._beforeSerializeHooks.push(hook);\n }\n\n /**\n * Register a hook to process custom data when loading a save.\n * Multiple hooks can be registered; each receives the full custom data object.\n */\n onAfterDeserialize(hook : AfterDeserializeHook) : void\n {\n this._afterDeserializeHooks.push(hook);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Serialize\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Captures the current game state as a serializable SaveData object.\n * Iterates all entities, captures transforms, state, tags, and parent references.\n * Calls all beforeSerialize hooks to gather custom data.\n */\n serialize() : SaveData\n {\n this._log.debug('Serializing game state...');\n\n const levelName = this._levelManager.currentLevel?.name ?? '';\n const entities : SerializedEntity[] = [];\n\n for(const entity of this._entityManager.getAllEntities())\n {\n const serialized : SerializedEntity = {\n id: entity.id,\n type: entity.type,\n tags: Array.from(entity.tags),\n state: structuredClone(entity.state),\n };\n\n if(entity.name)\n {\n serialized.name = entity.name;\n }\n\n if(entity.parent)\n {\n serialized.parentId = entity.parent.id;\n }\n\n if(entity.node)\n {\n const { position, rotation, scaling } = entity.node;\n serialized.transform = {\n position: { x: position.x, y: position.y, z: position.z },\n rotation: { x: rotation.x, y: rotation.y, z: rotation.z },\n scaling: { x: scaling.x, y: scaling.y, z: scaling.z },\n };\n }\n\n entities.push(serialized);\n }\n\n // Collect custom data from hooks\n let custom : Record<string, unknown> = {};\n for(const hook of this._beforeSerializeHooks)\n {\n const hookData = hook();\n custom = { ...custom, ...hookData };\n }\n\n const saveData : SaveData = {\n version: 1,\n levelName,\n entities,\n custom,\n };\n\n this._log.info(`Serialized ${ entities.length } entities from level \"${ levelName }\"`);\n return saveData;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Deserialize\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Restores game state from a SaveData object.\n *\n * Flow:\n * 1. Transition to the saved level\n * 2. Destroy all existing entities (level-spawned ones)\n * 3. Recreate entities from save data\n * 4. Restore transforms on entities with nodes\n * 5. Re-establish parent-child hierarchy\n * 6. Call afterDeserialize hooks\n */\n async deserialize(data : SaveData) : Promise<void>\n {\n this._log.debug(`Deserializing save data (version ${ data.version }, level \"${ data.levelName }\")...`);\n\n // 1. Transition to the level\n await this._levelManager.transition(data.levelName);\n\n // 2. Destroy all existing entities spawned by the level\n const existingIds : string[] = [];\n for(const entity of this._entityManager.getAllEntities())\n {\n existingIds.push(entity.id);\n }\n\n for(const id of existingIds)\n {\n // eslint-disable-next-line no-await-in-loop\n await this._entityManager.destroyEntity(id);\n }\n\n // 3. Recreate entities — track old ID -> new ID mapping for hierarchy restoration\n const idMap = new Map<string, string>();\n\n for(const serialized of data.entities)\n {\n // eslint-disable-next-line no-await-in-loop\n const entity = await this._entityManager.createEntity(serialized.type, {\n name: serialized.name,\n initialState: serialized.state,\n tags: serialized.tags,\n });\n\n idMap.set(serialized.id, entity.id);\n\n // 4. Restore transform if present\n if(serialized.transform && entity.node)\n {\n const { position, rotation, scaling } = serialized.transform;\n entity.node.position.x = position.x;\n entity.node.position.y = position.y;\n entity.node.position.z = position.z;\n entity.node.rotation.x = rotation.x;\n entity.node.rotation.y = rotation.y;\n entity.node.rotation.z = rotation.z;\n entity.node.scaling.x = scaling.x;\n entity.node.scaling.y = scaling.y;\n entity.node.scaling.z = scaling.z;\n }\n }\n\n // 5. Restore parent-child hierarchy\n for(const serialized of data.entities)\n {\n if(serialized.parentId)\n {\n const newParentId = idMap.get(serialized.parentId);\n const newChildId = idMap.get(serialized.id);\n\n if(newParentId && newChildId)\n {\n this._entityManager.addChild(newParentId, newChildId);\n }\n else\n {\n this._log.warn(\n `Could not restore parent-child: ${ serialized.id } -> ${ serialized.parentId } `\n + `(mapped: ${ newChildId } -> ${ newParentId })`\n );\n }\n }\n }\n\n // 6. Call afterDeserialize hooks\n for(const hook of this._afterDeserializeHooks)\n {\n hook(data.custom);\n }\n\n this._log.info(`Deserialized ${ data.entities.length } entities for level \"${ data.levelName }\"`);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n async $teardown() : Promise<void>\n {\n this._beforeSerializeHooks = [];\n this._afterDeserializeHooks = [];\n this._log.info('SaveManager torn down');\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Keyboard Input Resource Access\n//----------------------------------------------------------------------------------------------------------------------\n\n// Interfaces\nimport type { KeyboardDevice, KeyboardInputState } from '../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Callback type for keyboard device connection events\n */\nexport type KeyboardDeviceCallback = (device : KeyboardDevice) => void;\n\n/**\n * Callback type for keyboard input events\n */\nexport type KeyboardInputCallback = (device : KeyboardDevice, state : KeyboardInputState) => void;\n\n/**\n * Responsible for tracking keyboard state and notifying subscribers of changes.\n */\nexport class KeyboardInputPlugin\n{\n private _keyboardDevice : KeyboardDevice;\n private _keysState : Record<string, boolean> = {};\n\n // Callbacks\n private _onDeviceConnected ?: KeyboardDeviceCallback;\n private _onInputChanged ?: KeyboardInputCallback;\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new KeyboardInputPlugin\n */\n constructor()\n {\n // Initialize keyboard device\n this._keyboardDevice = {\n id: 'keyboard-0',\n name: 'Keyboard',\n type: 'keyboard',\n connected: true,\n };\n\n // Set up event listeners\n this._setupKeyboardEvents();\n\n // Notify that device is connected (on next tick to allow callback registration)\n setTimeout(() => this._notifyDeviceConnected(), 0);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a callback for device connected events\n *\n * @param callback\n */\n public onDeviceConnected(callback : KeyboardDeviceCallback) : void\n {\n this._onDeviceConnected = callback;\n }\n\n /**\n * Register a callback for input changed events\n *\n * @param callback\n */\n public onInputChanged(callback : KeyboardInputCallback) : void\n {\n this._onInputChanged = callback;\n }\n\n /**\n * Get the current keyboard state\n */\n public getState() : KeyboardInputState\n {\n return {\n type: 'keyboard',\n keys: { ...this._keysState },\n delta: {},\n };\n }\n\n /**\n * Get the keyboard device\n */\n public getDevice() : KeyboardDevice\n {\n return { ...this._keyboardDevice };\n }\n\n /**\n * Tears down the keyboard resource access and cleans up event listeners\n */\n public $teardown() : Promise<void>\n {\n // Remove keyboard event listeners\n window.removeEventListener('keydown', this._handleKeyDown);\n window.removeEventListener('keyup', this._handleKeyUp);\n\n return Promise.resolve();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Set up keyboard event listeners\n */\n private _setupKeyboardEvents() : void\n {\n // Bind handlers to this instance\n this._handleKeyDown = this._handleKeyDown.bind(this);\n this._handleKeyUp = this._handleKeyUp.bind(this);\n\n // Add event listeners\n window.addEventListener('keydown', this._handleKeyDown);\n window.addEventListener('keyup', this._handleKeyUp);\n }\n\n /**\n * Handle keyboard key down events\n */\n private _handleKeyDown(event : KeyboardEvent) : void\n {\n this._keysState[event.code] = true;\n\n const delta : Record<string, boolean> = {};\n delta[event.code] = true;\n\n const state : KeyboardInputState = {\n type: 'keyboard',\n keys: { ...this._keysState },\n delta,\n event,\n };\n\n this._notifyInputChanged(state);\n }\n\n /**\n * Handle keyboard key up events\n */\n private _handleKeyUp(event : KeyboardEvent) : void\n {\n this._keysState[event.code] = false;\n\n const delta : Record<string, boolean> = {};\n delta[event.code] = false;\n\n const state : KeyboardInputState = {\n type: 'keyboard',\n keys: { ...this._keysState },\n delta,\n event,\n };\n\n this._notifyInputChanged(state);\n }\n\n /**\n * Notify subscribers of device connected event\n */\n private _notifyDeviceConnected() : void\n {\n if(this._onDeviceConnected)\n {\n this._onDeviceConnected(this._keyboardDevice);\n }\n }\n\n /**\n * Notify subscribers of input changed event\n */\n private _notifyInputChanged(state : KeyboardInputState) : void\n {\n if(this._onInputChanged)\n {\n this._onInputChanged(this._keyboardDevice, state);\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Mouse Input Resource Access\n//----------------------------------------------------------------------------------------------------------------------\n\n// Interfaces\nimport type { ButtonState, MouseDevice, MouseInputState, Position } from '../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Callback type for mouse device connection events\n */\nexport type MouseDeviceCallback = (device : MouseDevice) => void;\n\n/**\n * Callback type for mouse input events\n */\nexport type MouseInputCallback = (device : MouseDevice, state : MouseInputState) => void;\n\n/**\n * Responsible for tracking mouse state and notifying subscribers of changes.\n */\nexport class MouseInputPlugin\n{\n private _targetElement : HTMLElement;\n private _mouseDevice : MouseDevice;\n private _buttonState : Record<string, ButtonState> = {};\n private _axesState : Record<string, number> = {};\n private _position : Position = {\n absolute: { x: 0, y: 0 },\n relative: { x: 0, y: 0 },\n };\n\n private _wheelState = {\n deltaX: 0,\n deltaY: 0,\n deltaZ: 0,\n deltaMode: 0,\n };\n\n // Callbacks\n private _onDeviceConnected ?: MouseDeviceCallback;\n private _onInputChanged ?: MouseInputCallback;\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new MouseInputPlugin\n *\n * @param targetElement - DOM element to attach mouse listeners to (defaults to document.body)\n */\n constructor(targetElement : HTMLElement = document.body)\n {\n this._targetElement = targetElement;\n\n // Initialize mouse device\n this._mouseDevice = {\n id: 'mouse-0',\n name: 'Mouse',\n type: 'mouse',\n connected: true,\n };\n\n // Set up event listeners\n this._setupMouseEvents();\n\n // Initialize axes to zero\n this._axesState['axis-x'] = 0;\n this._axesState['axis-y'] = 0;\n\n // Notify that device is connected (on next tick to allow callback registration)\n setTimeout(() => this._notifyDeviceConnected(), 0);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a callback for device connected events\n *\n * @param callback\n */\n public onDeviceConnected(callback : MouseDeviceCallback) : void\n {\n this._onDeviceConnected = callback;\n }\n\n /**\n * Register a callback for input changed events\n *\n * @param callback\n */\n public onInputChanged(callback : MouseInputCallback) : void\n {\n this._onInputChanged = callback;\n }\n\n /**\n * Get the current mouse state\n */\n public getState() : MouseInputState\n {\n return {\n type: 'mouse',\n buttons: { ...this._buttonState },\n axes: { ...this._axesState },\n position: {\n absolute: { ...this._position.absolute },\n relative: { ...this._position.relative },\n },\n wheel: { ...this._wheelState },\n };\n }\n\n /**\n * Get the mouse device\n */\n public getDevice() : MouseDevice\n {\n return { ...this._mouseDevice };\n }\n\n /**\n * Tears down the mouse resource access and cleans up event listeners\n */\n public $teardown() : Promise<void>\n {\n this._targetElement.removeEventListener('pointerdown', this._handlePointerDown);\n this._targetElement.removeEventListener('pointerup', this._handlePointerUp);\n this._targetElement.removeEventListener('pointermove', this._handlePointerMove);\n this._targetElement.removeEventListener('wheel', this._handleMouseWheel);\n\n return Promise.resolve();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Set up pointer and wheel event listeners. We use pointer events instead of mouse events\n * because BabylonJS calls preventDefault() on pointerdown, which suppresses the\n * corresponding mousedown/mouseup per the Pointer Events spec.\n */\n private _setupMouseEvents() : void\n {\n // Bind handlers to this instance\n this._handlePointerDown = this._handlePointerDown.bind(this);\n this._handlePointerUp = this._handlePointerUp.bind(this);\n this._handlePointerMove = this._handlePointerMove.bind(this);\n this._handleMouseWheel = this._handleMouseWheel.bind(this);\n\n this._targetElement.addEventListener('pointerdown', this._handlePointerDown);\n this._targetElement.addEventListener('pointerup', this._handlePointerUp);\n this._targetElement.addEventListener('pointermove', this._handlePointerMove);\n this._targetElement.addEventListener('wheel', this._handleMouseWheel);\n }\n\n /**\n * Handle pointer down events (mouse buttons)\n */\n private _handlePointerDown(event : PointerEvent) : void\n {\n if(event.pointerType !== 'mouse') { return; }\n\n const buttonKey = `button-${ event.button }`;\n this._buttonState[buttonKey] = { pressed: true };\n\n this._notifyInputChanged({\n type: 'mouse',\n event,\n buttons: { ...this._buttonState },\n axes: { ...this._axesState },\n position: { ...this._position },\n });\n }\n\n /**\n * Handle pointer up events (mouse buttons)\n */\n private _handlePointerUp(event : PointerEvent) : void\n {\n if(event.pointerType !== 'mouse') { return; }\n\n const buttonKey = `button-${ event.button }`;\n this._buttonState[buttonKey] = { pressed: false };\n\n this._notifyInputChanged({\n type: 'mouse',\n event,\n buttons: { ...this._buttonState },\n axes: { ...this._axesState },\n position: { ...this._position },\n });\n }\n\n /**\n * Handle pointer move events (mouse position)\n */\n private _handlePointerMove(event : PointerEvent) : void\n {\n if(event.pointerType !== 'mouse') { return; }\n\n this._position = {\n absolute: {\n x: event.clientX,\n y: event.clientY,\n },\n relative: {\n x: event.movementX,\n y: event.movementY,\n },\n };\n\n this._axesState['axis-x'] = event.clientX;\n this._axesState['axis-y'] = event.clientY;\n\n this._notifyInputChanged({\n type: 'mouse',\n event,\n buttons: { ...this._buttonState },\n axes: { ...this._axesState },\n position: { ...this._position },\n });\n }\n\n /**\n * Handle mouse wheel events\n */\n private _handleMouseWheel(event : WheelEvent) : void\n {\n this._wheelState = {\n deltaX: event.deltaX,\n deltaY: event.deltaY,\n deltaZ: event.deltaZ,\n deltaMode: event.deltaMode,\n };\n\n this._notifyInputChanged({\n type: 'mouse',\n event,\n buttons: { ...this._buttonState },\n axes: { ...this._axesState },\n position: { ...this._position },\n wheel: { ...this._wheelState },\n });\n }\n\n /**\n * Notify subscribers of device connected event\n */\n private _notifyDeviceConnected() : void\n {\n if(this._onDeviceConnected)\n {\n this._onDeviceConnected(this._mouseDevice);\n }\n }\n\n /**\n * Notify subscribers of input changed event\n */\n private _notifyInputChanged(state : MouseInputState) : void\n {\n if(this._onInputChanged)\n {\n this._onInputChanged(this._mouseDevice, state);\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Gamepad Input\n//----------------------------------------------------------------------------------------------------------------------\n\n// Interfaces\nimport type { ButtonState, GamepadDevice, GamepadInputState } from '../../interfaces/input.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Callback type for gamepad device connection events\n */\nexport type GamepadDeviceCallback = (device : GamepadDevice) => void;\n\n/**\n * Callback type for gamepad input events\n */\nexport type GamepadInputCallback = (device : GamepadDevice, state : GamepadInputState) => void;\n\n/**\n * Responsible for tracking gamepad state and notifying subscribers of changes.\n */\nexport class GamepadInputPlugin\n{\n private _gamepadDevices : Record<number, GamepadDevice | undefined> = {};\n private _buttonStates : Record<number, Record<string, ButtonState> | undefined> = {};\n private _axesStates : Record<number, Record<string, number> | undefined> = {};\n\n // Callbacks\n private _onDeviceConnected ?: GamepadDeviceCallback;\n private _onDeviceDisconnected ?: GamepadDeviceCallback;\n private _onInputChanged ?: GamepadInputCallback;\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new GamepadInputPlugin\n */\n constructor()\n {\n // Set up event listeners for gamepad connection and disconnection\n this._setupGamepadEvents();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a callback for device connected events\n *\n * @param callback\n */\n public onDeviceConnected(callback : GamepadDeviceCallback) : void\n {\n this._onDeviceConnected = callback;\n }\n\n /**\n * Register a callback for device disconnected events\n *\n * @param callback\n */\n public onDeviceDisconnected(callback : GamepadDeviceCallback) : void\n {\n this._onDeviceDisconnected = callback;\n }\n\n /**\n * Register a callback for input changed events\n *\n * @param callback\n */\n public onInputChanged(callback : GamepadInputCallback) : void\n {\n this._onInputChanged = callback;\n }\n\n /**\n * Get all connected gamepad devices\n */\n public getDevices() : GamepadDevice[]\n {\n return Object.values(this._gamepadDevices).map((device) => ({ ...device }) as GamepadDevice);\n }\n\n /**\n * Get all connected gamepad states\n */\n public getStates() : Record<number, GamepadInputState>\n {\n const states : Record<number, GamepadInputState> = {};\n\n Object.keys(this._buttonStates).forEach((indexStr) =>\n {\n const index = Number(indexStr);\n const buttons = this._buttonStates[index] || {};\n const axes = this._axesStates[index] || {};\n\n states[index] = {\n type: 'gamepad',\n buttons: { ...buttons },\n axes: { ...axes },\n };\n });\n\n return states;\n }\n\n /**\n * Get a specific gamepad device by index\n *\n * @param index\n */\n public getDevice(index : number) : GamepadDevice | null\n {\n const device = this._gamepadDevices[index];\n return device ? { ...device } : null;\n }\n\n /**\n * Get a specific gamepad state by index\n *\n * @param index\n */\n public getState(index : number) : GamepadInputState | null\n {\n const buttons = this._buttonStates[index];\n const axes = this._axesStates[index];\n\n if(!buttons && !axes) { return null; }\n\n return {\n type: 'gamepad',\n buttons: buttons ? { ...buttons } : {},\n axes: axes ? { ...axes } : {},\n };\n }\n\n /**\n * Poll for gamepad state updates - call this in your game loop\n */\n public pollGamepads() : void\n {\n /* eslint-disable no-continue */\n\n if(!navigator.getGamepads) { return; }\n\n const gamepads = navigator.getGamepads();\n for(const gamepad of gamepads)\n {\n if(!gamepad) { continue; }\n\n const index = gamepad.index;\n\n if(!this._gamepadDevices[index])\n {\n this._handleGamepadConnected(gamepad);\n continue;\n }\n\n // Get device\n const device = this._gamepadDevices[index];\n if(!device) { continue; }\n\n // Get current button and axis states or initialize them\n const currentButtons = this._buttonStates[index] || {};\n const currentAxes = this._axesStates[index] || {};\n\n // Build new button state object\n const newButtons : Record<string, ButtonState> = {};\n let buttonsChanged = false;\n\n gamepad.buttons.forEach((btn, i) =>\n {\n const buttonKey = `button-${ i }`;\n const buttonState : ButtonState = {\n pressed: btn.pressed,\n touched: btn.touched,\n value: btn.value,\n };\n\n newButtons[buttonKey] = buttonState;\n\n const prevButton = currentButtons[buttonKey];\n if(!prevButton\n || prevButton.pressed !== buttonState.pressed\n || prevButton.touched !== buttonState.touched\n || prevButton.value !== buttonState.value)\n {\n buttonsChanged = true;\n }\n });\n\n // Build new axis state object\n const newAxes : Record<string, number> = {};\n let axesChanged = false;\n\n gamepad.axes.forEach((axisValue, i) =>\n {\n const axisKey = `axis-${ i }`;\n newAxes[axisKey] = axisValue;\n\n const prevAxis = currentAxes[axisKey];\n if(prevAxis !== axisValue)\n {\n axesChanged = true;\n }\n });\n\n // Update state objects\n this._buttonStates[index] = newButtons;\n this._axesStates[index] = newAxes;\n\n // Notify if state changed\n if(buttonsChanged || axesChanged)\n {\n const state : GamepadInputState = {\n type: 'gamepad',\n buttons: { ...newButtons },\n axes: { ...newAxes },\n };\n\n this._notifyInputChanged(device, state);\n }\n }\n\n /* eslint-enable no-continue */\n }\n\n /**\n * Tears down the gamepad resource access and cleans up event listeners\n */\n public $teardown() : Promise<void>\n {\n // Remove gamepad event listeners\n window.removeEventListener('gamepadconnected', this._handleGamepadConnected);\n window.removeEventListener('gamepaddisconnected', this._handleGamepadDisconnected);\n\n return Promise.resolve();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Set up gamepad event listeners\n */\n private _setupGamepadEvents() : void\n {\n // Bind handlers to this instance\n this._handleGamepadConnected = this._handleGamepadConnected.bind(this);\n this._handleGamepadDisconnected = this._handleGamepadDisconnected.bind(this);\n\n // Add event listeners\n window.addEventListener('gamepadconnected', this._handleGamepadConnected);\n window.addEventListener('gamepaddisconnected', this._handleGamepadDisconnected);\n\n // Check for already connected gamepads (browsers sometimes miss the connected event)\n if(navigator.getGamepads)\n {\n const gamepads = navigator.getGamepads();\n for(const gamepad of gamepads)\n {\n if(gamepad)\n {\n this._handleGamepadConnected(gamepad);\n }\n }\n }\n }\n\n /**\n * Handle gamepad connected event\n */\n private _handleGamepadConnected(event : GamepadEvent | Gamepad) : void\n {\n const gamepad = event instanceof GamepadEvent ? event.gamepad : event;\n const index = gamepad.index;\n\n // Create gamepad device object\n const gamepadDevice : GamepadDevice = {\n id: `gamepad-${ index }`,\n name: gamepad.id,\n type: 'gamepad',\n connected: true,\n index,\n mapping: gamepad.mapping,\n axes: Array.from(gamepad.axes),\n buttons: Array.from(gamepad.buttons),\n };\n\n // Store the device\n this._gamepadDevices[index] = gamepadDevice;\n\n // Initialize button state object\n const buttonStateObj : Record<string, ButtonState> = {};\n Array.from(gamepad.buttons).forEach((btn, i) =>\n {\n buttonStateObj[`button-${ i }`] = {\n pressed: btn.pressed,\n touched: btn.touched,\n value: btn.value,\n };\n });\n this._buttonStates[index] = buttonStateObj;\n\n // Initialize axis state object\n const axesStateObj : Record<string, number> = {};\n Array.from(gamepad.axes).forEach((axisValue, i) =>\n {\n axesStateObj[`axis-${ i }`] = axisValue;\n });\n this._axesStates[index] = axesStateObj;\n\n // Notify device connected with initial state\n this._notifyDeviceConnected(gamepadDevice);\n\n // Emit input changed event with initial state\n this._notifyInputChanged(gamepadDevice, {\n type: 'gamepad',\n buttons: { ...buttonStateObj },\n axes: { ...axesStateObj },\n event: event instanceof GamepadEvent ? event : undefined,\n });\n }\n\n /**\n * Handle gamepad disconnected event\n */\n private _handleGamepadDisconnected(event : GamepadEvent) : void\n {\n const gamepad = event.gamepad;\n const index = gamepad.index;\n\n // Get the device\n const gamepadDevice = this._gamepadDevices[index];\n\n if(gamepadDevice)\n {\n // Update connection status\n gamepadDevice.connected = false;\n\n // Notify device disconnected\n this._notifyDeviceDisconnected(gamepadDevice);\n\n // Remove from tracking objects\n this._gamepadDevices[index] = undefined;\n this._buttonStates[index] = undefined;\n this._axesStates[index] = undefined;\n }\n }\n\n /**\n * Notify subscribers of device connected event\n */\n private _notifyDeviceConnected(device : GamepadDevice) : void\n {\n if(this._onDeviceConnected)\n {\n this._onDeviceConnected(device);\n }\n }\n\n /**\n * Notify subscribers of device disconnected event\n */\n private _notifyDeviceDisconnected(device : GamepadDevice) : void\n {\n if(this._onDeviceDisconnected)\n {\n this._onDeviceDisconnected(device);\n }\n }\n\n /**\n * Notify subscribers of input changed event\n */\n private _notifyInputChanged(device : GamepadDevice, state : GamepadInputState) : void\n {\n if(this._onInputChanged)\n {\n this._onInputChanged(device, state);\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// User Input Manager\n//----------------------------------------------------------------------------------------------------------------------\n\n// Interfaces\nimport type { GamepadInputState, InputDevice, KeyboardInputState, MouseInputState } from '../interfaces/input.ts';\nimport type { Disposable } from '../interfaces/lifecycle.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\n\n// Classes\nimport { GameEventBus } from '../classes/eventBus.ts';\n\n// Utils\nimport { LoggingUtility, SAGELogger } from '../utils/logger.ts';\n\n// Resource Access\nimport { KeyboardInputPlugin } from '../classes/input/keyboard.ts';\nimport { MouseInputPlugin } from '../classes/input/mouse.ts';\nimport { GamepadInputPlugin } from '../classes/input/gamepad.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Manager for handling user input from various devices (keyboard, mouse, gamepad)\n */\nexport class UserInputManager implements Disposable\n{\n private _eventBus : GameEventBus;\n\n private _keyboardRA : KeyboardInputPlugin;\n private _mouseRA : MouseInputPlugin;\n private _gamepadRA : GamepadInputPlugin;\n\n /** Logger instance */\n private _log : LoggerInterface;\n\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Create a new UserInputManager.\n *\n * @param eventBus\n * @param canvas - The DOM element to attach input listeners to (used by mouse input)\n * @param logger\n */\n constructor(\n eventBus : GameEventBus,\n canvas : HTMLElement,\n logger ?: LoggingUtility\n )\n {\n this._eventBus = eventBus;\n\n // Initialize logger\n this._log = logger?.getLogger('UserInputManager') || new SAGELogger('UserInputManager');\n\n this._log.info('Initializing UserInputManager');\n\n // Initialize resource access classes\n this._log.debug('Initializing input resource access classes');\n this._keyboardRA = new KeyboardInputPlugin();\n this._mouseRA = new MouseInputPlugin(canvas);\n this._gamepadRA = new GamepadInputPlugin();\n\n // Register callbacks\n this._log.debug('Registering input event callbacks');\n this._keyboardRA.onDeviceConnected(this._publishDeviceConnected.bind(this));\n this._keyboardRA.onInputChanged(this._publishInputChanged.bind(this));\n this._mouseRA.onDeviceConnected(this._publishDeviceConnected.bind(this));\n this._mouseRA.onInputChanged(this._publishInputChanged.bind(this));\n this._gamepadRA.onDeviceConnected(this._publishDeviceConnected.bind(this));\n this._gamepadRA.onDeviceDisconnected(this._publishDeviceDisconnected.bind(this));\n this._gamepadRA.onInputChanged(this._publishInputChanged.bind(this));\n\n this._log.info('UserInputManager initialized successfully');\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Publish device connected event to the event bus\n */\n private _publishDeviceConnected(device : InputDevice) : void\n {\n this._log.debug(`Device connected: ${ device.id } (${ device.name })`);\n this._eventBus.publish({ type: 'input:device:connected', payload: { device } });\n }\n\n /**\n * Publish device disconnected event to the event bus\n */\n private _publishDeviceDisconnected(device : InputDevice) : void\n {\n this._log.debug(`Device disconnected: ${ device.id } (${ device.name })`);\n this._eventBus.publish({ type: 'input:device:disconnected', payload: { device } });\n }\n\n /**\n * Publish input changed event to the event bus, used by all device types\n */\n private _publishInputChanged(\n device : InputDevice,\n state : KeyboardInputState | MouseInputState | GamepadInputState\n ) : void\n {\n this._log.trace(`Input changed: ${ device.id }`, state);\n this._eventBus.publish({\n type: 'input:changed',\n payload: {\n deviceId: device.id,\n device,\n state,\n },\n });\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Internal methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Tears down the input manager and clean up event listeners\n */\n public $teardown() : Promise<void>\n {\n this._log.info('Tearing down UserInputManager');\n\n // Cleanup all resource access instances\n this._log.debug('Cleaning up input resource access instances');\n\n // Using Promise.all to handle multiple async teardowns\n return Promise.all([\n this._keyboardRA.$teardown(),\n this._mouseRA.$teardown(),\n this._gamepadRA.$teardown(),\n ]).then(() =>\n {\n this._log.info('UserInputManager torn down successfully');\n });\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Get all connected input devices (keyboard, mouse, and any gamepads).\n */\n public listDevices() : InputDevice[]\n {\n this._log.debug('Getting all connected input devices');\n\n const devices : InputDevice[] = [];\n\n // Add keyboard and mouse\n devices.push(this._keyboardRA.getDevice());\n devices.push(this._mouseRA.getDevice());\n\n // Add all gamepads\n devices.push(...this._gamepadRA.getDevices());\n\n this._log.debug(`Found ${ devices.length } connected devices`);\n return devices;\n }\n\n /**\n * Get a specific input device by ID.\n * Uses device ID prefix (keyboard-, mouse-, gamepad-) for fast lookup.\n *\n * @param deviceId\n */\n public getDevice(deviceId : string) : InputDevice | null\n {\n this._log.debug(`Getting device: ${ deviceId }`);\n\n // Fast path for known device prefixes\n if(deviceId.startsWith('keyboard-'))\n {\n return this._keyboardRA.getDevice();\n }\n\n if(deviceId.startsWith('mouse-'))\n {\n return this._mouseRA.getDevice();\n }\n\n if(deviceId.startsWith('gamepad-'))\n {\n const index = parseInt(deviceId.split('-')[1], 10);\n return this._gamepadRA.getDevice(index);\n }\n\n // Slow path fallback - iterate through all devices\n const device = this.listDevices().find((dev) => dev.id === deviceId);\n if(device)\n {\n return device;\n }\n\n this._log.warn(`Device not found: ${ deviceId }`);\n return null;\n }\n\n /**\n * Poll for gamepad state updates - call this in your game loop\n */\n public pollGamepads() : void\n {\n this._gamepadRA.pollGamepads();\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n","//----------------------------------------------------------------------------------------------------------------------\n// Graphics Utility\n//----------------------------------------------------------------------------------------------------------------------\n\nimport {\n AbstractEngine,\n Engine,\n type EngineOptions,\n NullEngine,\n type NullEngineOptions,\n WebGPUEngine,\n type WebGPUEngineOptions,\n} from '@babylonjs/core';\n\n// Interfaces\nimport type { BabylonEngineOptions, GameCanvas, RenderEngineOptions } from '../interfaces/game.ts';\n\n// Utilities\nimport { hasWebGPU } from './capabilities.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Builds and initializes a WebGPU engine.\n *\n * @param canvas\n * @param options\n */\nasync function _buildWebGPUEngine(\n canvas : HTMLCanvasElement | OffscreenCanvas,\n options : WebGPUEngineOptions\n) : Promise<WebGPUEngine>\n{\n const engine = new WebGPUEngine(canvas, options);\n await engine.initAsync();\n return engine;\n}\n\n/**\n * Builds a WebGL engine.\n *\n * @param canvas\n * @param options\n */\nfunction _buildWebGLEngine(canvas : GameCanvas, options : BabylonEngineOptions) : Engine\n{\n return new Engine(canvas, options.antialias, options.options, options.adaptToDeviceRatio);\n}\n\n/**\n * Builds a Null engine, which is used when no rendering is required.\n *\n * @param options\n */\nfunction _buildNullEngine(options : NullEngineOptions) : NullEngine\n{\n return new NullEngine(options as NullEngineOptions);\n}\n\n//------------------------------------------------------------------------------------------------------------------\n\n/**\n * Creates an appropriate engine based on the provided canvas and options.\n * Passing null for canvas creates a NullEngine. Otherwise auto-detects WebGPU with WebGL fallback,\n * unless a specific engine type is forced via `options.engine`.\n *\n * @param canvas - Pass null to get a NullEngine (headless/testing)\n * @param options\n */\nexport async function createEngine(\n canvas : GameCanvas | null,\n options : RenderEngineOptions,\n largeWorldRendering = false\n) : Promise<AbstractEngine>\n{\n // Check if we should use a null engine (no canvas provided)\n if(canvas === null)\n {\n console.debug('Using Null Engine');\n return _buildNullEngine(options as NullEngineOptions);\n }\n\n // Inject large world rendering into engine options if requested. We have to build new objects\n // here because BabylonJS declares useLargeWorldRendering as readonly on its option types.\n if(largeWorldRendering)\n {\n if('options' in options)\n {\n const babylonOpts = options as BabylonEngineOptions;\n options = {\n ...babylonOpts,\n options: { ...babylonOpts.options, useLargeWorldRendering: true },\n } as RenderEngineOptions;\n }\n else\n {\n // WebGPUEngineOptions -- useLargeWorldRendering lives at the top level\n options = { ...options, useLargeWorldRendering: true } as RenderEngineOptions;\n }\n }\n\n // Extract the forceEngine option if available\n const forceEngine = options.engine || 'auto';\n\n // Force WebGPU engine if specified and available\n if(forceEngine === 'webgpu')\n {\n if(hasWebGPU())\n {\n try\n {\n console.debug('Using forced WebGPU engine');\n return await _buildWebGPUEngine(canvas, options);\n }\n catch (error)\n {\n console.error('Forced WebGPU initialization failed:', error);\n throw new Error(\n 'Forced WebGPU failed to initialize. If WebGPU is required, check browser compatibility.',\n { cause: error }\n );\n }\n }\n else\n {\n throw new Error('WebGPU was forced but is not available in this browser.');\n }\n }\n\n // Force WebGL engine if specified\n if(forceEngine === 'webgl')\n {\n console.debug('Using forced WebGL engine');\n return _buildWebGLEngine(canvas, options as BabylonEngineOptions);\n }\n\n // Auto detection (default behavior)\n if(hasWebGPU())\n {\n try\n {\n console.debug('Using WebGPU');\n\n return _buildWebGPUEngine(canvas, options)\n .catch((error) =>\n {\n console.warn('WebGPU initialization failed, falling back to WebGL:', error);\n return _buildWebGLEngine(canvas, options as EngineOptions);\n });\n }\n catch (error)\n {\n console.warn('WebGPU initialization failed, falling back to WebGL:', error);\n }\n }\n else\n {\n console.warn('WebGPU not supported, falling back to WebGL.');\n }\n\n console.debug('Using WebGL');\n\n return _buildWebGLEngine(canvas, options as EngineOptions);\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n","//----------------------------------------------------------------------------------------------------------------------\n// Physics Utility\n//----------------------------------------------------------------------------------------------------------------------\n\nimport HavokPhysics, { type HavokPhysicsWithBindings } from '@babylonjs/havok';\nimport { HavokPlugin } from '@babylonjs/core';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Initializes the Havok WASM module and returns the raw bindings.\n *\n * Each BabylonJS scene needs its own HavokPlugin instance (because scene.dispose() disposes the\n * plugin attached to it). The raw WASM module can be shared across plugins safely.\n */\nexport async function initHavok() : Promise<HavokPhysicsWithBindings>\n{\n return await HavokPhysics();\n}\n\n/**\n * Creates a new HavokPlugin from an existing WASM instance.\n */\nexport function createHavokPlugin(havok : HavokPhysicsWithBindings) : HavokPlugin\n{\n return new HavokPlugin(true, havok);\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Raycast Utilities\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { AbstractMesh, Camera, PickingInfo, Ray, Scene, TransformNode, Vector3 } from '@babylonjs/core';\n\nimport type { GameEntity } from '../classes/entity.ts';\nimport type { GameEntityManager } from '../managers/entity.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport interface EntityPickResult\n{\n entity : GameEntity;\n point : Vector3;\n distance : number;\n normal : Vector3;\n mesh : AbstractMesh;\n pickingInfo : PickingInfo;\n}\n\nexport interface EntityPickFilter\n{\n type ?: string;\n tags ?: string[];\n predicate ?: (entity : GameEntity) => boolean;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class RaycastHelper\n{\n private _entityManager : GameEntityManager;\n\n constructor(entityManager : GameEntityManager)\n {\n this._entityManager = entityManager;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n pickEntity(scene : Scene, x : number, y : number, filter ?: EntityPickFilter) : EntityPickResult | null\n {\n const info = scene.pick(x, y);\n return this._toEntityResult(info, filter);\n }\n\n pickEntities(scene : Scene, x : number, y : number, filter ?: EntityPickFilter) : EntityPickResult[]\n {\n const infos = scene.multiPick(x, y);\n if(!infos) { return []; }\n\n const results : EntityPickResult[] = [];\n for(const info of infos)\n {\n const result = this._toEntityResult(info, filter);\n if(result)\n {\n results.push(result);\n }\n }\n return results;\n }\n\n pickEntityWithRay(scene : Scene, ray : Ray, filter ?: EntityPickFilter) : EntityPickResult | null\n {\n const info = scene.pickWithRay(ray);\n if(!info) { return null; }\n return this._toEntityResult(info, filter);\n }\n\n pickEntitiesWithRay(scene : Scene, ray : Ray, filter ?: EntityPickFilter) : EntityPickResult[]\n {\n const infos = scene.multiPickWithRay(ray);\n if(!infos) { return []; }\n\n const results : EntityPickResult[] = [];\n for(const info of infos)\n {\n const result = this._toEntityResult(info, filter);\n if(result)\n {\n results.push(result);\n }\n }\n return results;\n }\n\n pickEntityForward(\n scene : Scene,\n camera : Camera,\n maxDistance = 1000,\n filter ?: EntityPickFilter\n ) : EntityPickResult | null\n {\n const ray = camera.getForwardRay(maxDistance);\n return this.pickEntityWithRay(scene, ray, filter);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private\n //------------------------------------------------------------------------------------------------------------------\n\n private _toEntityResult(info : PickingInfo, filter ?: EntityPickFilter) : EntityPickResult | null\n {\n if(!info.hit || !info.pickedMesh || !info.pickedPoint)\n {\n return null;\n }\n\n const mesh = info.pickedMesh;\n\n // Look up entity on the picked mesh, then walk up parent chain\n let entity = this._entityManager.getByNode(mesh as unknown as TransformNode);\n if(!entity)\n {\n let current : TransformNode | null = (mesh as unknown as TransformNode).parent as TransformNode | null;\n while(current)\n {\n entity = this._entityManager.getByNode(current);\n if(entity) { break; }\n current = current.parent as TransformNode | null;\n }\n }\n\n if(!entity)\n {\n return null;\n }\n\n // Apply filter\n if(filter)\n {\n if(filter.type && entity.type !== filter.type)\n {\n return null;\n }\n\n if(filter.tags)\n {\n for(const tag of filter.tags)\n {\n if(!entity.hasTag(tag))\n {\n return null;\n }\n }\n }\n\n if(filter.predicate && !filter.predicate(entity))\n {\n return null;\n }\n }\n\n const normal = info.getNormal(true, true);\n\n return {\n entity,\n point: info.pickedPoint,\n distance: info.distance,\n normal: normal ?? { x: 0, y: 0, z: 0 } as unknown as Vector3,\n mesh,\n pickingInfo: info,\n };\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// GameTimer - Pause-aware timer utilities\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A pollable cooldown handle.\n */\nexport interface CooldownHandle\n{\n readonly ready : boolean;\n reset() : void;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface DelayEntry\n{\n id : number;\n remaining : number;\n callback : () => void;\n}\n\ninterface IntervalEntry\n{\n id : number;\n periodMs : number;\n elapsed : number;\n callback : () => void;\n}\n\ninterface CooldownEntry\n{\n id : number;\n periodMs : number;\n remaining : number;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Pause-aware game timer providing delay, interval, and cooldown primitives.\n *\n * All times are in game-time milliseconds. When the game is paused, `tick()` is simply not called, so all timers\n * freeze automatically.\n */\nexport class GameTimer\n{\n private _nextId = 0;\n private _delays : DelayEntry[] = [];\n private _intervals : IntervalEntry[] = [];\n private _cooldowns : CooldownEntry[] = [];\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * One-shot timer. Fires the callback after `ms` milliseconds of game time.\n *\n * @returns A cancel function that prevents the callback from firing.\n */\n delay(ms : number, callback : () => void) : () => void\n {\n if(ms < 0)\n {\n throw new RangeError(`delay() requires ms >= 0, got ${ ms }`);\n }\n\n const id = this._nextId++;\n this._delays.push({ id, remaining: ms, callback });\n\n return () =>\n {\n this._delays = this._delays.filter((entry) => entry.id !== id);\n };\n }\n\n /**\n * Repeating timer. Fires the callback every `ms` milliseconds of game time.\n *\n * @returns A cancel function that stops the interval.\n */\n interval(ms : number, callback : () => void) : () => void\n {\n if(ms <= 0)\n {\n throw new RangeError(`interval() requires ms > 0, got ${ ms }`);\n }\n\n const id = this._nextId++;\n this._intervals.push({ id, periodMs: ms, elapsed: 0, callback });\n\n return () =>\n {\n this._intervals = this._intervals.filter((entry) => entry.id !== id);\n };\n }\n\n /**\n * Pollable cooldown. Starts ready; after calling `reset()`, becomes not-ready until `ms` elapses.\n */\n cooldown(ms : number) : CooldownHandle\n {\n const id = this._nextId++;\n const entry : CooldownEntry = { id, periodMs: ms, remaining: 0 };\n this._cooldowns.push(entry);\n\n return {\n get ready() : boolean\n {\n return entry.remaining <= 0;\n },\n reset() : void\n {\n entry.remaining = entry.periodMs;\n },\n };\n }\n\n /**\n * Cancel all active delays, intervals, and cooldowns.\n */\n cancelAll() : void\n {\n this._delays = [];\n this._intervals = [];\n\n // Set all cooldowns to ready before clearing, so existing CooldownHandle references aren't stuck\n for(const entry of this._cooldowns)\n {\n entry.remaining = 0;\n }\n\n this._cooldowns = [];\n }\n\n /**\n * Advance all timers by `dtMs` milliseconds. Called once per frame by the game loop.\n */\n tick(dtMs : number) : void\n {\n // Process delays\n const firedDelays : number[] = [];\n for(const entry of this._delays)\n {\n entry.remaining -= dtMs;\n if(entry.remaining <= 0)\n {\n entry.callback();\n firedDelays.push(entry.id);\n }\n }\n\n if(firedDelays.length > 0)\n {\n this._delays = this._delays.filter((entry) => !firedDelays.includes(entry.id));\n }\n\n // Process intervals\n for(const entry of this._intervals)\n {\n entry.elapsed += dtMs;\n while(entry.elapsed >= entry.periodMs)\n {\n entry.callback();\n entry.elapsed -= entry.periodMs;\n }\n }\n\n // Process cooldowns\n for(const entry of this._cooldowns)\n {\n if(entry.remaining > 0)\n {\n entry.remaining = Math.max(0, entry.remaining - dtMs);\n }\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Logger Interfaces\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Defines the available logging levels as string literals.\n */\nexport const LogLevels = [\n 'trace',\n 'debug',\n 'info',\n 'warn',\n 'error',\n 'none',\n] as const;\n\n/**\n * Type for LogLevel values\n */\nexport type LogLevel = typeof LogLevels[number];\n\n/**\n * Interface for a logging backend implementation.\n * Backends handle the actual output of log messages.\n */\nexport interface LoggingBackend\n{\n /**\n * Log a message at TRACE level\n *\n * @param category - Typically the class or module name\n * @param message\n * @param args\n */\n trace(category : string, message : string, ...args : unknown[]) : void;\n\n /**\n * Log a message at DEBUG level\n *\n * @param category - Typically the class or module name\n * @param message\n * @param args\n */\n debug(category : string, message : string, ...args : unknown[]) : void;\n\n /**\n * Log a message at INFO level\n *\n * @param category - Typically the class or module name\n * @param message\n * @param args\n */\n info(category : string, message : string, ...args : unknown[]) : void;\n\n /**\n * Log a message at WARN level\n *\n * @param category - Typically the class or module name\n * @param message\n * @param args\n */\n warn(category : string, message : string, ...args : unknown[]) : void;\n\n /**\n * Log a message at ERROR level\n *\n * @param category - Typically the class or module name\n * @param message\n * @param args\n */\n error(category : string, message : string, ...args : unknown[]) : void;\n\n /**\n * Start a timer with the specified label.\n *\n * @param category - Typically the class or module name\n * @param label - Must be unique; used to match with timeEnd()\n */\n time(category : string, label : string) : void;\n\n /**\n * End a timer with the specified label and log the time elapsed.\n *\n * @param category - Typically the class or module name\n * @param label - Must match a previous time() call\n */\n timeEnd(category : string, label : string) : void;\n}\n\n/**\n * Interface for a logger instance.\n * Each instance is typically associated with a specific category/component.\n */\nexport interface LoggerInterface\n{\n /** Log a message at TRACE level */\n trace(message : string, ...args : unknown[]) : void;\n\n /** Log a message at DEBUG level */\n debug(message : string, ...args : unknown[]) : void;\n\n /** Log a message at INFO level */\n info(message : string, ...args : unknown[]) : void;\n\n /** Log a message at WARN level */\n warn(message : string, ...args : unknown[]) : void;\n\n /** Log a message at ERROR level */\n error(message : string, ...args : unknown[]) : void;\n\n /** Start a timer with the given label */\n time(label : string) : void;\n\n /** End a timer and log the elapsed time */\n timeEnd(label : string) : void;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Sound Behavior\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { SoundState, type StaticSound, type TransformNode } from '@babylonjs/core';\n\nimport { GameEntityBehavior } from '../classes/entity.ts';\nimport type { GameEngine } from '../classes/gameEngine.ts';\nimport type { GameEvent } from '../classes/eventBus.ts';\nimport type { LoggerInterface } from '../interfaces/logger.ts';\nimport type { AudioManager } from '../managers/audio.ts';\nimport { SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Configuration for a single sound.\n */\nexport interface SoundConfig\n{\n /** Path to the sound file */\n url : string;\n\n /** Volume level (0-1), default 1 */\n volume ?: number;\n\n /** Whether to loop the sound, default false */\n loop ?: boolean;\n\n /** Whether to use spatial 3D audio, default false */\n spatial ?: boolean;\n\n /** Maximum distance for spatial audio, default 100 */\n maxDistance ?: number;\n\n /** Whether to autoplay when registered, default false */\n autoplay ?: boolean;\n\n /** Audio channel name (e.g., 'sfx', 'music'). Used with AudioManager. */\n channel ?: string;\n}\n\n/**\n * State interface for entities using SoundBehavior.\n * Entity state should include a `sounds` property with sound configurations.\n */\nexport interface SoundEntityState\n{\n /** Map of sound name to configuration */\n sounds ?: Record<string, SoundConfig>;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Behavior for managing sounds on an entity.\n *\n * Sounds are configured in the entity's state under the `sounds` property.\n * The behavior creates sounds via AudioManager (BabylonJS AudioV2).\n *\n * Requires AudioManager to be configured on the GameEngine. If no AudioManager is found,\n * a warning is logged and no sounds are created.\n *\n * @example\n * ```typescript\n * // Entity definition\n * {\n * type: 'player',\n * behaviors: [SoundBehavior, ...],\n * defaultState: {\n * sounds: {\n * jump: { url: 'sounds/jump.mp3', volume: 0.8, channel: 'sfx' },\n * footstep: { url: 'sounds/footstep.mp3', loop: true, channel: 'sfx' }\n * }\n * }\n * }\n *\n * // In another behavior, get the sound behavior and control sounds\n * const soundBehavior = entity.getBehavior(SoundBehavior);\n * soundBehavior?.play('jump');\n * ```\n */\nexport class SoundBehavior extends GameEntityBehavior<SoundEntityState>\n{\n name = 'sound';\n eventSubscriptions : string[] = [];\n\n /** Map of sound name to StaticSound instance */\n private _sounds = new Map<string, StaticSound>();\n\n /** Whether sounds have been initialized */\n private _initialized = false;\n\n /** AudioManager reference */\n private _audioManager : AudioManager | null = null;\n\n /** Node reference for spatial detach on cleanup */\n private _node : TransformNode | null = null;\n\n private _log : LoggerInterface = new SAGELogger('SoundBehavior');\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n onNodeAttached(node : TransformNode, gameEngine : GameEngine) : void\n {\n if(this._initialized)\n {\n return;\n }\n\n this._node = node;\n this._initialized = true;\n\n this._log = gameEngine.logger.getLogger('SoundBehavior');\n\n if(gameEngine.managers.audioManager)\n {\n this._audioManager = gameEngine.managers.audioManager;\n }\n else\n {\n this._log.warn('No AudioManager configured. Sounds will not be created.');\n return;\n }\n\n if(!this.entity)\n {\n return;\n }\n\n const state = this.entity.state;\n const soundConfigs = state.sounds;\n if(!soundConfigs)\n {\n return;\n }\n\n for(const [ name, config ] of Object.entries(soundConfigs))\n {\n this._createSound(name, config as SoundConfig);\n }\n }\n\n processEvent(_event : GameEvent, _state : SoundEntityState) : boolean\n {\n return false;\n }\n\n onReset(_state : SoundEntityState) : void\n {\n this._disposeSounds();\n }\n\n async destroy() : Promise<void>\n {\n this._disposeSounds();\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Private Methods\n //------------------------------------------------------------------------------------------------------------------\n\n private _createSound(name : string, config : SoundConfig) : void\n {\n if(!this._audioManager || !this.entity)\n {\n return;\n }\n\n const entityId = this.entity.id;\n const node = this._node;\n\n this._audioManager.createSound(\n `${ entityId }_${ name }`,\n config.url,\n config.channel,\n {\n loop: config.loop ?? false,\n autoplay: config.autoplay ?? false,\n volume: config.volume ?? 1,\n spatialEnabled: config.spatial ?? false,\n spatialMaxDistance: config.maxDistance ?? 100,\n spatialDistanceModel: 'linear',\n }\n )\n .then((sound) =>\n {\n this._sounds.set(name, sound);\n\n if((config.spatial ?? false) && node)\n {\n sound.spatial.attach(node);\n }\n })\n .catch((err) =>\n {\n this._log.error(`Failed to create sound \"${ name }\": ${ err }`);\n });\n }\n\n private _disposeSounds() : void\n {\n for(const sound of this._sounds.values())\n {\n sound.dispose();\n }\n this._sounds.clear();\n this._initialized = false;\n this._node = null;\n this._audioManager = null;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Play a sound by name.\n *\n * @param name - The name of the sound to play. If omitted and only one sound exists, plays that sound.\n * @param time - Optional time offset in seconds to start playing from\n */\n play(name ?: string, time ?: number) : void\n {\n const sound = this._getSound(name);\n if(sound)\n {\n if(time !== undefined)\n {\n sound.play({ startOffset: time });\n }\n else\n {\n sound.play();\n }\n }\n }\n\n /**\n * Stop a sound by name.\n *\n * @param name - The name of the sound to stop. If omitted, stops all sounds.\n */\n stop(name ?: string) : void\n {\n if(name)\n {\n const sound = this._sounds.get(name);\n if(sound)\n {\n sound.stop();\n }\n }\n else\n {\n for(const sound of this._sounds.values())\n {\n sound.stop();\n }\n }\n }\n\n /**\n * Pause a sound by name.\n *\n * @param name - The name of the sound to pause. If omitted and only one sound exists, pauses that sound.\n */\n pause(name ?: string) : void\n {\n const sound = this._getSound(name);\n if(sound)\n {\n sound.pause();\n }\n }\n\n /**\n * Set the volume of a sound.\n *\n * @param volume - Volume level (0-1)\n * @param name - The name of the sound. If omitted, sets volume for all sounds.\n */\n setVolume(volume : number, name ?: string) : void\n {\n if(name)\n {\n const sound = this._sounds.get(name);\n if(sound)\n {\n sound.volume = volume;\n }\n }\n else\n {\n for(const sound of this._sounds.values())\n {\n sound.volume = volume;\n }\n }\n }\n\n /**\n * Check if a sound is currently playing.\n *\n * @param name - The name of the sound. If omitted and only one sound exists, checks that sound.\n */\n isPlaying(name ?: string) : boolean\n {\n const sound = this._getSound(name);\n return sound?.state === SoundState.Started;\n }\n\n /**\n * Get all registered sound names.\n */\n getSoundNames() : string[]\n {\n return Array.from(this._sounds.keys());\n }\n\n /**\n * Check if a sound with the given name exists.\n *\n * @param name\n */\n hasSound(name : string) : boolean\n {\n return this._sounds.has(name);\n }\n\n /**\n * Register a new sound at runtime. Replaces any existing sound with the same name.\n *\n * @param name\n * @param config\n */\n registerSound(name : string, config : SoundConfig) : void\n {\n if(!this._initialized || !this._audioManager)\n {\n throw new Error('SoundBehavior must be initialized before registering sounds');\n }\n\n const existing = this._sounds.get(name);\n if(existing)\n {\n existing.dispose();\n }\n\n this._createSound(name, config);\n }\n\n /**\n * Unregister and dispose a sound.\n *\n * @param name\n */\n unregisterSound(name : string) : void\n {\n const sound = this._sounds.get(name);\n if(sound)\n {\n sound.dispose();\n this._sounds.delete(name);\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Helper Methods\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Get a sound by name. If name is omitted and only one sound exists, returns that sound.\n */\n private _getSound(name ?: string) : StaticSound | undefined\n {\n if(name)\n {\n return this._sounds.get(name);\n }\n\n if(this._sounds.size === 1)\n {\n return this._sounds.values().next().value;\n }\n\n return undefined;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// State Machine\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { GameEventBus } from '../classes/eventBus.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface TransitionDef\n{\n guard ?: () => boolean;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A generic finite state machine with optional guard functions, lifecycle callbacks, and event bus integration.\n *\n * @template States - A string union of valid state names.\n */\nexport class StateMachine<States extends string>\n{\n private _currentState : States;\n private readonly transitions = new Map<string, TransitionDef>();\n private readonly wildcardTransitions = new Map<States, TransitionDef>();\n private readonly enterCallbacks = new Map<States, (() => void)[]>();\n private readonly exitCallbacks = new Map<States, (() => void)[]>();\n private readonly eventBus ?: GameEventBus<Record<string, unknown>>;\n\n constructor(initialState : States, eventBus ?: GameEventBus<Record<string, unknown>>)\n {\n this._currentState = initialState;\n this.eventBus = eventBus;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Properties\n //------------------------------------------------------------------------------------------------------------------\n\n get currentState() : States\n {\n return this._currentState;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Configuration\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Register a valid transition from one state to another, with an optional guard function.\n */\n addTransition(from : States, to : States, guard ?: () => boolean) : void\n {\n const key = `${ from }->${ to }`;\n this.transitions.set(key, { guard });\n }\n\n /**\n * Register a wildcard transition: allows transitioning to the given state from any state.\n */\n addTransitionFromAny(to : States, guard ?: () => boolean) : void\n {\n this.wildcardTransitions.set(to, { guard });\n }\n\n /**\n * Register a callback to run when entering a state.\n */\n onEnter(state : States, callback : () => void) : void\n {\n const callbacks = this.enterCallbacks.get(state) ?? [];\n callbacks.push(callback);\n this.enterCallbacks.set(state, callbacks);\n }\n\n /**\n * Register a callback to run when exiting a state.\n */\n onExit(state : States, callback : () => void) : void\n {\n const callbacks = this.exitCallbacks.get(state) ?? [];\n callbacks.push(callback);\n this.exitCallbacks.set(state, callbacks);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Queries\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Check if a transition to the given state is defined (ignoring guards).\n */\n canTransition(to : States) : boolean\n {\n const key = `${ this._currentState }->${ to }`;\n return this.transitions.has(key) || this.wildcardTransitions.has(to);\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Transitions\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Execute a transition to the target state.\n * Flow: validate -> run guard -> call exit callbacks -> change state -> call enter callbacks.\n *\n * @throws If no transition is defined from the current state to the target, or if a guard rejects.\n */\n transition(to : States) : void\n {\n const key = `${ this._currentState }->${ to }`;\n let transitionDef = this.transitions.get(key);\n\n // Fall back to wildcard\n if(!transitionDef)\n {\n transitionDef = this.wildcardTransitions.get(to);\n }\n\n if(!transitionDef)\n {\n throw new Error(\n `No transition defined from '${ this._currentState }' to '${ to }'.`\n );\n }\n\n // Run guard\n if(transitionDef.guard && !transitionDef.guard())\n {\n throw new Error(\n `Guard rejected transition from '${ this._currentState }' to '${ to }'.`\n );\n }\n\n const previousState = this._currentState;\n\n // Exit callbacks\n const exitCbs = this.exitCallbacks.get(previousState);\n if(exitCbs)\n {\n for(const cb of exitCbs)\n {\n cb();\n }\n }\n\n // Emit exit event -- cast needed because template literal event types can't resolve conditionally\n if(this.eventBus)\n {\n this.eventBus.publish({\n type: `state:exit:${ previousState }`,\n payload: { from: previousState, to },\n } as { type : string; payload : unknown });\n }\n\n // Change state\n this._currentState = to;\n\n // Enter callbacks\n const enterCbs = this.enterCallbacks.get(to);\n if(enterCbs)\n {\n for(const cb of enterCbs)\n {\n cb();\n }\n }\n\n // Emit enter event -- cast needed because template literal event types can't resolve conditionally\n if(this.eventBus)\n {\n this.eventBus.publish({\n type: `state:enter:${ to }`,\n payload: { from: previousState, to },\n } as { type : string; payload : unknown });\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// State Machine Behavior\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { GameEntityBehavior } from '../classes/entity.ts';\nimport type { GameEvent } from '../classes/eventBus.ts';\nimport { StateMachine } from '../utils/stateMachine.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A single transition definition for the state machine behavior configuration.\n */\nexport interface TransitionConfig<States extends string>\n{\n from : States;\n to : States;\n guard ?: () => boolean;\n}\n\n/**\n * A wildcard transition definition (from any state).\n */\nexport interface WildcardTransitionConfig<States extends string>\n{\n to : States;\n guard ?: () => boolean;\n}\n\n/**\n * Configuration for creating a StateMachineBehavior subclass.\n */\nexport interface StateMachineBehaviorConfig<States extends string, EntityState extends object>\n{\n /** The initial state of the state machine. */\n initialState : States;\n\n /** The key in the entity's state object to update when state changes. */\n stateKey : keyof EntityState & string;\n\n /** Valid transitions between states. */\n transitions : TransitionConfig<States>[];\n\n /** Wildcard transitions (from any state). */\n wildcardTransitions ?: WildcardTransitionConfig<States>[];\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * A behavior that wraps a {@link StateMachine} and ties it to entity state.\n *\n * Since behaviors are instantiated with `new BehaviorClass()` (no constructor args),\n * use the static factory `StateMachineBehavior.create(config)` to produce a configured subclass.\n *\n * @example\n * ```typescript\n * const EnemyAI = StateMachineBehavior.create<EnemyStates, EnemyState>({\n * initialState: 'idle',\n * stateKey: 'aiState',\n * transitions: [\n * { from: 'idle', to: 'patrol' },\n * { from: 'patrol', to: 'chase' },\n * { from: 'chase', to: 'attack' },\n * ],\n * wildcardTransitions: [\n * { to: 'dead' },\n * ],\n * });\n * ```\n */\nexport class StateMachineBehavior<\n States extends string = string,\n EntityState extends object = object,\n> extends GameEntityBehavior<EntityState>\n{\n name = 'stateMachine';\n eventSubscriptions : string[] = [];\n\n protected stateMachine : StateMachine<States>;\n protected stateKey : keyof EntityState & string;\n\n protected constructor()\n {\n super();\n\n // These will be overridden by the factory-produced subclass constructor.\n // Default to a dummy state machine that will be replaced.\n this.stateMachine = new StateMachine<States>('' as States);\n this.stateKey = '' as keyof EntityState & string;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Public API\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * The current state of the state machine.\n */\n get currentState() : States\n {\n return this.stateMachine.currentState;\n }\n\n /**\n * Transition to a new state. Updates the entity's state and emits a state changed event.\n *\n * @throws If the transition is not defined or a guard rejects it.\n */\n transition(to : States) : void\n {\n this.stateMachine.transition(to);\n\n // Update entity state\n if(this.entity)\n {\n (this.entity.state as Record<string, unknown>)[this.stateKey] = to;\n this.$emitStateChanged();\n }\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Behavior Implementation\n //------------------------------------------------------------------------------------------------------------------\n\n processEvent(_event : GameEvent, _state : EntityState) : boolean\n {\n return false;\n }\n\n //------------------------------------------------------------------------------------------------------------------\n // Static Factory\n //------------------------------------------------------------------------------------------------------------------\n\n /**\n * Creates a configured StateMachineBehavior subclass with the given state machine configuration baked in.\n * The returned class can be used directly as a behavior constructor.\n */\n static create<States extends string, EntityState extends object>(\n config : StateMachineBehaviorConfig<States, EntityState>\n ) : new () => StateMachineBehavior<States, EntityState>\n {\n // Capture config in closure\n const { initialState, stateKey, transitions, wildcardTransitions } = config;\n\n class ConfiguredStateMachineBehavior extends StateMachineBehavior<States, EntityState>\n {\n constructor()\n {\n super();\n\n this.stateKey = stateKey;\n this.stateMachine = new StateMachine<States>(initialState);\n\n for(const tr of transitions)\n {\n this.stateMachine.addTransition(tr.from, tr.to, tr.guard);\n }\n\n if(wildcardTransitions)\n {\n for(const wt of wildcardTransitions)\n {\n this.stateMachine.addTransitionFromAny(wt.to, wt.guard);\n }\n }\n }\n }\n\n return ConfiguredStateMachineBehavior;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Collider Property Handler\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { AbstractMesh, Mesh, PhysicsAggregate, PhysicsShapeType, type TransformNode } from '@babylonjs/core';\n\nimport type { LevelManager } from '../managers/level.ts';\nimport { SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nconst logger = new SAGELogger('ColliderHandler');\n\n/**\n * Creates a physics aggregate for a node with the given shape type.\n * Works with both Mesh and TransformNode (for box/sphere on multi-material meshes).\n */\nfunction createPhysicsAggregate(node : TransformNode, shapeType : PhysicsShapeType, mass : number) : void\n{\n const scene = node.getScene();\n new PhysicsAggregate(node, shapeType, { mass }, scene);\n}\n\n/**\n * Handles mesh collider setup, using a child mesh if marked with collider_mesh.\n */\nfunction handleMeshCollider(mesh : Mesh, mass : number) : void\n{\n const colliderChild = mesh.getChildMeshes().find(\n (child) => child.metadata?.collider_mesh === true\n );\n\n if(colliderChild instanceof Mesh)\n {\n colliderChild.isVisible = false;\n createPhysicsAggregate(colliderChild, PhysicsShapeType.MESH, mass);\n }\n else\n {\n createPhysicsAggregate(mesh, PhysicsShapeType.MESH, mass);\n }\n}\n\n/**\n * Register the collider property handler.\n *\n * Supports the following properties on scene nodes:\n * - `collider` (string): Collider type - \"box\", \"sphere\", \"mesh\", or \"none\"\n * - `collider_mesh` (boolean): On a child node, marks it as the collision source for parent's \"mesh\" collider\n * - `collider_mass` (number): Mass for the physics body. Defaults to 0 (static). Set > 0 for dynamic bodies.\n * - `collider_kinematic` (boolean): When true, sets `disablePreStep = false` so the physics body\n * tracks mesh transform each frame. Required for animated colliders (doors, elevators, platforms).\n *\n * For \"mesh\" colliders, if a child node has `collider_mesh: true`, that child's geometry\n * is used for collision and the child is made invisible.\n *\n * @param levelManager\n */\nexport function registerColliderHandler(levelManager : LevelManager) : void\n{\n levelManager.registerPropertyHandler('collider', (node, value, level, _gameEngine) =>\n {\n const colliderType = value as string;\n logger.debug(`Processing collider: ${ node.name } -> ${ colliderType } (isMesh: ${ node instanceof Mesh })`);\n\n if(!level.scene)\n {\n throw new Error('Scene not available for collider handler');\n }\n\n if(!(node instanceof Mesh) && colliderType === 'mesh')\n {\n logger.warn(`Skipping mesh collider for ${ node.name }: not a Mesh instance`);\n return;\n }\n\n const mass = (node.metadata?.collider_mass as number | undefined) ?? 0;\n\n switch (colliderType)\n {\n case 'box':\n createPhysicsAggregate(node, PhysicsShapeType.BOX, mass);\n break;\n\n case 'sphere':\n createPhysicsAggregate(node, PhysicsShapeType.SPHERE, mass);\n break;\n\n case 'mesh':\n handleMeshCollider(node as Mesh, mass);\n break;\n\n case 'none':\n if(node instanceof AbstractMesh)\n {\n node.checkCollisions = false;\n }\n break;\n\n default:\n logger.warn(`Unknown collider type: ${ colliderType }`);\n }\n\n // Enable kinematic mode -- physics body tracks mesh transform each frame\n if(node.metadata?.collider_kinematic && node instanceof AbstractMesh)\n {\n if(node.physicsBody)\n {\n node.physicsBody.disablePreStep = false;\n }\n }\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// LOD (Level of Detail) Property Handler\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { Mesh } from '@babylonjs/core';\n\nimport type { LevelManager } from '../managers/level.ts';\nimport { SAGELogger } from '../utils/logger.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nconst logger = new SAGELogger('LodHandler');\n\n/**\n * Parses a comma-separated string of distances into an array of numbers.\n * Filters out invalid (NaN) values.\n */\nfunction parseDistances(distancesStr : string) : number[]\n{\n return distancesStr\n .split(',')\n .map((str) => parseFloat(str.trim()))\n .filter((num) => !isNaN(num));\n}\n\n/**\n * Register the LOD (Level of Detail) property handler.\n *\n * Supports the following property on scene nodes:\n * - `lod_distances` (string): Comma-separated distances for LOD levels\n *\n * The mesh's children are used as LOD levels in order.\n * Example: \"10,25,50\" means:\n * - 0-10 units: use first child mesh\n * - 10-25 units: use second child mesh\n * - 25-50 units: use third child mesh\n * - 50+ units: use null (not visible)\n *\n * @param levelManager\n */\nexport function registerLodHandler(levelManager : LevelManager) : void\n{\n levelManager.registerPropertyHandler('lod_distances', (node, value, _level, _gameEngine) =>\n {\n const distancesStr = value as string;\n\n if(!(node instanceof Mesh))\n {\n return;\n }\n\n const distances = parseDistances(distancesStr);\n\n if(distances.length === 0)\n {\n logger.warn(`Invalid lod_distances value: ${ distancesStr }`);\n return;\n }\n\n const children = node.getChildMeshes(true) as Mesh[];\n const levelCount = Math.min(distances.length, children.length);\n\n // Add LOD levels for each distance/child pair\n for(let i = 0; i < levelCount; i++)\n {\n node.addLODLevel(distances[i], children[i]);\n }\n\n // Add null LOD for beyond the last distance (not visible)\n const lastDistance = distances[distances.length - 1];\n node.addLODLevel(lastDistance, null);\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Occluder Property Handler\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { Mesh } from '@babylonjs/core';\n\nimport type { LevelManager } from '../managers/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Register the occluder property handler.\n *\n * Supports the following property on scene nodes:\n * - `occluder` (boolean): If true, marks the mesh as an occluder for occlusion culling\n *\n * Occluder meshes are used by the engine to hide objects that are behind them,\n * improving rendering performance. The occluder mesh itself is made invisible.\n *\n * @param levelManager\n */\nexport function registerOccluderHandler(levelManager : LevelManager) : void\n{\n levelManager.registerPropertyHandler('occluder', (node, value, _level, _gameEngine) =>\n {\n if(!value)\n {\n return;\n }\n\n // Only meshes can be occluders\n if(!(node instanceof Mesh))\n {\n return;\n }\n\n // Mark as occluder and hide\n // Note: isOccluder exists at runtime but is missing from @babylonjs/core type definitions as of 9.0\n (node as Mesh & { isOccluder : boolean }).isOccluder = true;\n node.isVisible = false;\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Metadata Utilities\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Collect properties with a common prefix into a single object.\n * Useful for gathering related properties from node metadata.\n *\n * For example, given metadata with:\n * - sound_volume: 0.5\n * - sound_loop: true\n * - sound_spatial: false\n *\n * Calling collectPrefixedProperties(metadata, 'sound') returns:\n * { volume: 0.5, loop: true, spatial: false }\n *\n * @param metadata - The node metadata to search\n * @param prefix - The prefix to match (without trailing underscore)\n * @returns Object with sub-properties (prefix and underscore removed from keys)\n */\nexport function collectPrefixedProperties(\n metadata : Record<string, unknown>,\n prefix : string\n) : Record<string, unknown>\n{\n const result : Record<string, unknown> = {};\n const prefixWithUnderscore = `${ prefix }_`;\n\n for(const [ key, value ] of Object.entries(metadata))\n {\n if(key.startsWith(prefixWithUnderscore))\n {\n const subKey = key.slice(prefixWithUnderscore.length);\n result[subKey] = value;\n }\n }\n\n return result;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Sound Property Handler\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { collectPrefixedProperties } from '../utils/metadata.ts';\nimport { SAGELogger } from '../utils/logger.ts';\nimport type { LevelManager } from '../managers/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nconst logger = new SAGELogger('SoundHandler');\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Register the sound property handler.\n *\n * Supports the following properties on scene nodes:\n * - `sound` (string): Path to the sound file\n * - `sound_volume` (number): Volume 0-1, default 1\n * - `sound_loop` (boolean): Whether to loop, default true\n * - `sound_spatial` (boolean): Whether to use 3D spatial audio, default true\n * - `sound_distance` (number): Maximum distance for spatial audio, default 100\n * - `sound_autoplay` (boolean): Whether to autoplay, default true\n * - `sound_channel` (string): Audio channel name, default 'ambient'\n *\n * @param levelManager\n */\nexport function registerSoundHandler(levelManager : LevelManager) : void\n{\n levelManager.registerPropertyHandler('sound', (node, value, level, gameEngine) =>\n {\n const soundPath = value as string;\n const scene = level.scene;\n\n if(!scene)\n {\n throw new Error('Scene not available for sound handler');\n }\n\n // Collect prefixed properties\n const extras = collectPrefixedProperties(\n node.metadata as Record<string, unknown>,\n 'sound'\n );\n\n const volume = (extras.volume as number) ?? 1;\n const loop = (extras.loop as boolean) ?? true;\n const spatial = (extras.spatial as boolean) ?? true;\n const maxDistance = (extras.distance as number) ?? 100;\n const autoplay = (extras.autoplay as boolean) ?? true;\n const channel = (extras.channel as string) ?? 'ambient';\n\n const audioManager = gameEngine.managers.audioManager;\n\n if(!audioManager)\n {\n logger.warn(`No AudioManager available. Sound for \"${ node.name }\" skipped.`);\n return;\n }\n\n audioManager.createSound(\n `${ node.name }_sound`,\n soundPath,\n channel,\n {\n loop,\n autoplay,\n volume,\n spatialEnabled: spatial,\n spatialMaxDistance: maxDistance,\n spatialDistanceModel: 'linear',\n }\n )\n .then((sound) =>\n {\n if(spatial)\n {\n sound.spatial.attach(node);\n }\n })\n .catch((err) =>\n {\n logger.error(`Failed to create sound for node \"${ node.name }\": ${ err }`);\n });\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Trigger Property Handler\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { AbstractMesh, ActionManager, ExecuteCodeAction } from '@babylonjs/core';\n\nimport type { GameEventBus } from '../classes/eventBus.ts';\nimport type { LevelManager } from '../managers/level.ts';\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Registers an intersection action (enter or exit) for a trigger mesh against a target mesh.\n */\nfunction registerIntersectionAction(\n actionManager : ActionManager,\n triggerName : string,\n eventBus : GameEventBus,\n targetMesh : AbstractMesh,\n actionType : 'enter' | 'exit'\n) : void\n{\n const triggerType = actionType === 'enter'\n ? ActionManager.OnIntersectionEnterTrigger\n : ActionManager.OnIntersectionExitTrigger;\n\n const eventType = `trigger:${ actionType }` as const;\n\n actionManager.registerAction(\n new ExecuteCodeAction(\n {\n trigger: triggerType,\n parameter: { mesh: targetMesh },\n },\n (evt) =>\n {\n eventBus.publish({\n type: eventType,\n payload: {\n trigger: triggerName,\n other: evt.source as AbstractMesh,\n },\n });\n }\n )\n );\n}\n\n/**\n * Sets up intersection triggers between a trigger mesh and a target mesh.\n */\nfunction registerTriggersForMesh(\n actionManager : ActionManager,\n triggerName : string,\n eventBus : GameEventBus,\n targetMesh : AbstractMesh\n) : void\n{\n registerIntersectionAction(actionManager, triggerName, eventBus, targetMesh, 'enter');\n registerIntersectionAction(actionManager, triggerName, eventBus, targetMesh, 'exit');\n}\n\n/**\n * Register the trigger property handler.\n *\n * Supports the following property on scene nodes:\n * - `trigger` (string): The trigger zone name\n *\n * When an object enters or exits the trigger zone, events are published:\n * - `trigger:enter` with payload { trigger: string, other: AbstractMesh }\n * - `trigger:exit` with payload { trigger: string, other: AbstractMesh }\n *\n * The trigger mesh is made invisible but retains collision detection.\n * Intersection checks are registered against all non-trigger meshes in the scene,\n * including meshes added after the handler runs (e.g. dynamically spawned entities).\n *\n * @param levelManager\n */\nexport function registerTriggerHandler(levelManager : LevelManager) : void\n{\n levelManager.registerPropertyHandler('trigger', (node, value, level, gameEngine) =>\n {\n const triggerName = value as string;\n const scene = level.scene;\n\n if(!scene)\n {\n throw new Error('Scene not available for trigger handler');\n }\n\n if(!(node instanceof AbstractMesh))\n {\n return;\n }\n\n const eventBus = gameEngine.eventBus;\n\n // Make the trigger invisible but keep collision\n node.isVisible = false;\n node.checkCollisions = true;\n\n // Create action manager if needed\n const actionManager = (node.actionManager as ActionManager) ?? new ActionManager(scene);\n node.actionManager = actionManager;\n\n // Register intersection triggers against all existing non-trigger meshes\n for(const mesh of scene.meshes)\n {\n if(mesh !== node && !mesh.metadata?.trigger)\n {\n registerTriggersForMesh(actionManager, triggerName, eventBus, mesh);\n }\n }\n\n // Register triggers for meshes added later (e.g. dynamically spawned entities)\n const observer = scene.onNewMeshAddedObservable.add((mesh) =>\n {\n if(mesh !== node && !mesh.metadata?.trigger)\n {\n registerTriggersForMesh(actionManager, triggerName, eventBus, mesh);\n }\n });\n\n // Clean up observer when the trigger mesh is disposed\n node.onDisposeObservable.add(() =>\n {\n scene.onNewMeshAddedObservable.remove(observer);\n });\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Visible Property Handler\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { AbstractMesh, type TransformNode } from '@babylonjs/core';\nimport type { LevelManager } from '../managers/level.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Register the visible property handler.\n *\n * Supports the following property on scene nodes:\n * - `visible` (boolean | string | number): Controls mesh visibility.\n * Accepts true/false, 'true'/'false', or 1/0.\n *\n * @param levelManager\n */\nexport function registerVisibleHandler(levelManager : LevelManager) : void\n{\n levelManager.registerPropertyHandler('visible', (node : TransformNode, value : unknown, _level, _gameEngine) =>\n {\n // Only meshes have isVisible property\n if(!(node instanceof AbstractMesh))\n {\n return;\n }\n\n // Parse value as boolean\n // Supports: true/false (boolean), 'true'/'false' (string), 1/0 (number)\n let visible = true;\n\n if(typeof value === 'boolean')\n {\n visible = value;\n }\n else if(typeof value === 'string')\n {\n visible = value.toLowerCase() === 'true' || value === '1';\n }\n else if(typeof value === 'number')\n {\n visible = value !== 0;\n }\n\n node.isVisible = visible;\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Property Handlers Index\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { LevelManager } from '../managers/level.ts';\n\nimport { registerColliderHandler } from './collider.ts';\nimport { registerLodHandler } from './lod.ts';\nimport { registerOccluderHandler } from './occluder.ts';\nimport { applyPostProcessing } from './postProcessing.ts';\nimport { registerSoundHandler } from './sound.ts';\nimport { registerTriggerHandler } from './trigger.ts';\nimport { registerVisibleHandler } from './visible.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport {\n applyPostProcessing,\n registerColliderHandler,\n registerLodHandler,\n registerOccluderHandler,\n registerSoundHandler,\n registerTriggerHandler,\n registerVisibleHandler,\n};\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Register all built-in property handlers with the given LevelManager.\n * Call this during game initialization to enable default property handling.\n *\n * @param levelManager\n */\nexport function registerAllPropertyHandlers(levelManager : LevelManager) : void\n{\n registerColliderHandler(levelManager);\n registerLodHandler(levelManager);\n registerOccluderHandler(levelManager);\n registerSoundHandler(levelManager);\n registerTriggerHandler(levelManager);\n registerVisibleHandler(levelManager);\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// SkewedAspect Game Engine\n//----------------------------------------------------------------------------------------------------------------------\n\n// Interfaces\nimport type { GameCanvas, RenderEngineOptions, SageOptions } from './interfaces/game.ts';\n\n// Base Classes\nimport { GameEngine } from './classes/gameEngine.ts';\nimport { GameEventBus } from './classes/eventBus.ts';\n\n// Engines\nimport { AudioEngine } from './engines/audio.ts';\nimport { SceneEngine } from './engines/scene.ts';\n\n// Managers\nimport { AssetManager } from './managers/asset.ts';\nimport { BindingManager } from './managers/binding.ts';\nimport { GameManager } from './managers/game.ts';\nimport { GameEntityManager } from './managers/entity.ts';\nimport { LevelManager } from './managers/level.ts';\nimport { AudioManager } from './managers/audio.ts';\nimport { SaveManager } from './managers/save.ts';\nimport { UserInputManager } from './managers/input.ts';\n\n// Utils\nimport { createEngine } from './utils/graphics.ts';\nimport { createHavokPlugin, initHavok } from './utils/physics.ts';\nimport { LoggingUtility } from './utils/logger.ts';\nimport { RaycastHelper } from './utils/raycast.ts';\nimport { GameTimer } from './utils/timer.ts';\nimport { VERSION } from './utils/version.ts';\nimport type { GameEntityDefinition } from './interfaces/entity.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Creates and initializes a fully-wired GameEngine with rendering, physics, input, and entity management.\n *\n * @param canvas\n * @param entities - Entity definitions to register before the engine starts\n * @param options\n */\nexport async function createGameEngine(\n canvas : GameCanvas,\n entities : GameEntityDefinition[],\n options : SageOptions = {}\n) : Promise<GameEngine>\n{\n // Initialize logging\n const logger = new LoggingUtility(options.logLevel || 'debug');\n const engineLogger = logger.getLogger('SAGE');\n\n engineLogger.info(`Initializing SAGE Game Engine v${ VERSION }...`);\n\n // Initialize BabylonJS stuff\n engineLogger.debug('Creating rendering engine...');\n\n const renderOptions : RenderEngineOptions = {\n antialias: true,\n adaptToDeviceRatio: true,\n ...options.renderOptions,\n };\n\n const engine = await createEngine(canvas, renderOptions, options.largeWorldRendering ?? false);\n\n engineLogger.debug('Creating physics engine...');\n const havok = await initHavok();\n const physics = createHavokPlugin(havok);\n\n // Initialize internals\n engineLogger.debug('Creating event bus...');\n const eventBus = new GameEventBus(logger);\n\n // Initialize SAGE Engines\n engineLogger.debug('Creating scene engine...');\n const sceneEngine = new SceneEngine(canvas, engine, havok, logger);\n\n // Initialize SAGE Managers\n engineLogger.debug('Creating managers...');\n const assetManager = new AssetManager(eventBus, sceneEngine, logger);\n const inputManager = new UserInputManager(eventBus, canvas as HTMLElement, logger);\n const bindingManager = new BindingManager(eventBus, logger);\n const entityManager = new GameEntityManager(eventBus, logger, bindingManager);\n const levelManager = new LevelManager(eventBus, logger);\n const saveManager = new SaveManager(entityManager, levelManager, logger);\n const gameManager = new GameManager(engine, eventBus, entityManager, inputManager, levelManager, logger);\n\n // Initialize raycasting\n engineLogger.debug('Creating raycast helper...');\n const raycast = new RaycastHelper(entityManager);\n\n // Initialize timer\n engineLogger.debug('Creating game timer...');\n const timer = new GameTimer();\n gameManager.registerFrameCallback((dt) => timer.tick(dt * 1000));\n\n // Initialize audio system (opt-in)\n let audioEngine : AudioEngine | undefined;\n let audioManager : AudioManager | undefined;\n\n if(options.audioChannels && options.audioChannels.length > 0)\n {\n engineLogger.debug('Creating audio engine...');\n audioEngine = new AudioEngine(logger);\n await audioEngine.initialize();\n\n engineLogger.debug('Creating audio manager...');\n audioManager = new AudioManager(audioEngine, logger);\n await audioManager.initialize(options.audioChannels);\n }\n\n engineLogger.info(`SAGE Game Engine v${ VERSION } initialized successfully.`);\n\n // Register entities\n engineLogger.debug('Loading entities...');\n for(const entity of entities)\n {\n entityManager.registerEntityDefinition(entity);\n }\n\n // Register default input bindings\n engineLogger.debug('Registering default input bindings...');\n if(options.bindings)\n {\n for(const binding of options.bindings)\n {\n bindingManager.registerBinding(binding);\n }\n }\n\n const ge = new GameEngine(\n canvas,\n engine,\n physics,\n eventBus,\n logger,\n raycast,\n timer,\n\n // Engines\n {\n sceneEngine,\n audioEngine,\n },\n\n // Managers\n {\n assetManager,\n bindingManager,\n entityManager,\n gameManager,\n inputManager,\n levelManager,\n saveManager,\n audioManager,\n },\n\n options.largeWorldRendering ?? false\n );\n\n // Late-bind: these managers are constructed before GameEngine (because GameEngine takes them as\n // constructor args), but they need a back-reference at runtime. This must happen before any level\n // loads or entity attaches, so do it immediately after construction. Do not reorder.\n entityManager.$setGameEngine(ge);\n levelManager.$setGameEngine(ge);\n\n return ge;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n// Exports\n//----------------------------------------------------------------------------------------------------------------------\n\nexport * from './interfaces/action.ts';\nexport * from './interfaces/binding.ts';\nexport * from './interfaces/entity.ts';\nexport * from './interfaces/game.ts';\nexport * from './interfaces/input.ts';\nexport * from './interfaces/logger.ts';\n\n// Export base classes\nexport { AssetManager, AudioEngine, GameEngine, GameEventBus, SceneEngine };\n\nexport * from './classes/eventBus.ts';\nexport * from './classes/entity.ts';\nexport * from './classes/level.ts';\nexport * from './classes/gameLevel.ts';\nexport * from './behaviors/index.ts';\nexport * from './events/index.ts';\nexport * from './handlers/index.ts';\nexport * from './interfaces/level.ts';\nexport * from './interfaces/lifecycle.ts';\nexport * from './utils/logger.ts';\nexport * from './utils/metadata.ts';\nexport * from './utils/timer.ts';\nexport * from './utils/version.ts';\n\n// Export manager types\nexport type { FrameCallback } from './managers/game.ts';\nexport type { CreateEntityOptions } from './managers/entity.ts';\nexport type { TransitionOptions } from './managers/level.ts';\nexport { AudioManager } from './managers/audio.ts';\nexport type { ChannelInfo } from './managers/audio.ts';\nexport { OutlineManager } from './managers/outline.ts';\nexport { SaveManager } from './managers/save.ts';\nexport type { SaveData, SerializedEntity } from './managers/save.ts';\n\n// Export utilities\nexport { generateId } from './utils/id.ts';\nexport { RaycastHelper } from './utils/raycast.ts';\nexport type { EntityPickResult, EntityPickFilter } from './utils/raycast.ts';\nexport { StateMachine } from './utils/stateMachine.ts';\nexport {\n getCanonicalTransform,\n toQuatObject, toQuaternion, toVec3Object, toVector3,\n} from './utils/vectors.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n"],"x_google_ignoreList":[20],"mappings":"40CAMA,IAAa,EAAA,QCwEA,EAAb,KACA,CACI,OACA,aACA,QACA,SACA,QACA,SACA,OACA,QACA,MACA,oBACA,QAAiB,GAEjB,KAGA,iBAA6C,KAC7C,aAAyC,KACzC,gBAA4C,KAY5C,YACI,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EAAsB,GAE1B,CACI,KAAK,OAAS,EACd,KAAK,aAAe,EACpB,KAAK,QAAU,EACf,KAAK,SAAW,EAChB,KAAK,OAAS,EACd,KAAK,QAAU,EACf,KAAK,MAAQ,EACb,KAAK,QAAU,EACf,KAAK,SAAW,EAChB,KAAK,oBAAsB,EAE3B,KAAK,KAAO,EAAO,UAAU,aAAa,CAY9C,cAAc,EACd,CACI,GAAG,KAAK,mBAAqB,KAEzB,MAAU,MAAM,0FAA0F,CAE9G,KAAK,iBAAmB,EAQ5B,QAAQ,EACR,CACI,GAAG,KAAK,eAAiB,KAErB,MAAU,MAAM,uFAAuF,CAE3G,KAAK,aAAe,EAQxB,WAAW,EACX,CACI,GAAG,KAAK,kBAAoB,KAExB,MAAU,MAAM,0FAA0F,CAE9G,KAAK,gBAAkB,EA2B3B,UACI,EACA,EAEJ,CACI,OAAO,KAAK,SAAS,UAAU,EAAW,EAAS,CA6BvD,gBACI,EACA,EAEJ,CACI,OAAO,KAAK,SAAS,UACjB,UAAW,IACX,EACH,CAUL,MAAM,OACN,CACQ,KAAK,QA4BL,KAAK,KAAK,KAAK,kDAAkD,EA1BjE,KAAK,KAAK,KAAK,+CAAgD,EAAS,MAAM,CAG3E,KAAK,mBAEJ,KAAK,KAAK,MAAM,gCAAgC,CAChD,MAAM,KAAK,iBAAiB,KAAK,EAIrC,MAAM,KAAK,SAAS,YAAY,OAAO,CAEvC,KAAK,KAAK,KAAK,mCAAmC,CAG/C,KAAK,eAEJ,KAAK,KAAK,MAAM,4BAA4B,CAC5C,MAAM,KAAK,aAAa,KAAK,EAIjC,KAAK,QAAU,IAWvB,MAAM,MACN,CACI,GAAG,KAAK,QACR,CACI,KAAK,KAAK,KAAK,+CAAgD,EAAS,MAAM,CAE9E,IAAI,EAA+B,KAGnC,GAAG,KAAK,gBAEJ,GACA,CACI,KAAK,KAAK,MAAM,+BAA+B,CAC/C,MAAM,KAAK,gBAAgB,KAAK,OAE7B,EACP,CACI,KAAK,KAAK,MAAM,6BAA8B,IAAO,CACrD,IAAkC,EAK1C,IAAI,IAAM,KAAc,OAAO,KAAK,KAAK,SAAS,CAClD,CACI,IAAM,EAAU,KAAK,SAAS,GAC9B,GAAG,EAEC,GACA,CACI,KAAK,KAAK,MAAM,yBAA0B,IAAc,CAExD,MAAM,EAAQ,WAAW,OAEtB,EACP,CACI,KAAK,KAAK,MAAM,8BAA+B,EAAY,IAAK,IAAO,CACvE,IAAkC,GAM9C,IAAI,IAAM,KAAa,OAAO,KAAK,KAAK,QAAQ,CAChD,CACI,IAAM,EAAS,KAAK,QAAQ,GAC5B,GAAG,EAEC,GACA,CACI,KAAK,KAAK,MAAM,wBAAyB,IAAa,CAEtD,MAAM,EAAO,WAAW,OAErB,EACP,CACI,KAAK,KAAK,MAAM,6BAA8B,EAAW,IAAK,IAAO,CACrE,IAAkC,GAS9C,GAJA,KAAK,QAAU,GACf,KAAK,KAAK,KAAK,mCAAmC,CAG/C,EAEC,MAAM,OAKV,KAAK,KAAK,KAAK,6CAA6C,GCpW3D,EAAb,KACA,CACI,OAEA,aACA,CACI,KAAK,OAAS,IAAI,IAMtB,iBAAyB,EACzB,CAEI,IAAM,EAAsC,CACxC,MAAO,iBACP,MAAO,iBACP,KAAM,iBACN,KAAM,iBACN,MAAO,iBACP,MAAO,iBACP,KAAM,iBACT,CAED,OAAO,EAAO,IAAU,EAAO,KAMnC,cAAsB,EAAmB,EACzC,CAEI,IAAM,EAAM,IAAI,KAYhB,MAAO,CACH,GANc,IANJ,EAAI,UAAU,CAAG,IAAM,GAMP,GALd,EAAI,YAAY,CAAC,UAAU,CACtC,SAAS,EAAG,IAAI,CAIsB,GAH3B,EAAI,YAAY,CAAC,UAAU,CACtC,SAAS,EAAG,IAAI,CAEmC,GAD3C,EAAI,UAAU,EAAI,GAAK,KAAO,KACuB,GAM/C,KAJA,EAAM,aAAa,CAID,MAAO,EAAU,KAClD,KAAK,iBAAiB,EAAM,CAC5B,GACA,GACH,CAGL,MAAM,EAAmB,EAAkB,GAAG,EAC9C,CACI,GAAM,CAAE,EAAQ,EAAc,EAAY,GAAe,KAAK,cAAc,EAAU,QAAQ,CAC9F,QAAQ,MAAM,EAAQ,EAAc,EAAY,EAAY,EAAS,GAAG,EAAK,CAGjF,MAAM,EAAmB,EAAkB,GAAG,EAC9C,CACI,GAAM,CAAE,EAAQ,EAAc,EAAY,GAAe,KAAK,cAAc,EAAU,QAAQ,CAC9F,QAAQ,MAAM,EAAQ,EAAc,EAAY,EAAY,EAAS,GAAG,EAAK,CAGjF,KAAK,EAAmB,EAAkB,GAAG,EAC7C,CACI,GAAM,CAAE,EAAQ,EAAc,EAAY,GAAe,KAAK,cAAc,EAAU,OAAO,CAC7F,QAAQ,KAAK,EAAQ,EAAc,EAAY,EAAY,EAAS,GAAG,EAAK,CAGhF,KAAK,EAAmB,EAAkB,GAAG,EAC7C,CACI,GAAM,CAAE,EAAQ,EAAc,EAAY,GAAe,KAAK,cAAc,EAAU,OAAO,CAC7F,QAAQ,KAAK,EAAQ,EAAc,EAAY,EAAY,EAAS,GAAG,EAAK,CAGhF,MAAM,EAAmB,EAAkB,GAAG,EAC9C,CACI,GAAM,CAAE,EAAQ,EAAc,EAAY,GAAe,KAAK,cAAc,EAAU,QAAQ,CAC9F,QAAQ,MAAM,EAAQ,EAAc,EAAY,EAAY,EAAS,GAAG,EAAK,CAMjF,KAAK,EAAmB,EACxB,CACI,IAAM,EAAW,GAAI,EAAU,GAAI,IACnC,KAAK,OAAO,IAAI,EAAU,YAAY,KAAK,CAAC,CAMhD,QAAQ,EAAmB,EAC3B,CACI,IAAM,EAAW,GAAI,EAAU,GAAI,IAC7B,EAAY,KAAK,OAAO,IAAI,EAAS,CAE3C,GAAG,IAAc,IAAA,GACjB,CACI,IAAM,EAAW,YAAY,KAAK,CAAG,EACrC,KAAK,OAAO,OAAO,EAAS,CAE5B,GAAM,CAAE,EAAQ,EAAc,EAAY,GAAe,KAAK,cAAc,EAAU,QAAQ,CAC9F,QAAQ,KACJ,GAAI,EAAQ,UAAW,EAAO,iBAAkB,EAAS,QAAQ,EAAE,CAAE,IACrE,EACA,EACA,EACH,MAID,QAAQ,KAAK,IAAK,EAAU,YAAa,EAAO,kBAAkB,GCrHjE,EAAb,KACA,CACI,MAAM,EAAoB,EAAmB,GAAG,EAChD,EAIA,MAAM,EAAoB,EAAmB,GAAG,EAChD,EAIA,KAAK,EAAoB,EAAmB,GAAG,EAC/C,EAIA,KAAK,EAAoB,EAAmB,GAAG,EAC/C,EAIA,MAAM,EAAoB,EAAmB,GAAG,EAChD,EAIA,KAAK,EAAoB,EACzB,EAIA,QAAQ,EAAoB,EAC5B,IC1BS,EAAb,KACA,CACI,SACA,QACA,SAEA,YAAY,EAAmB,EAAsB,OAAQ,EAA2B,IAAI,EAC5F,CACI,KAAK,SAAW,EAChB,KAAK,QAAU,EACf,KAAK,SAAW,EAMpB,eAAsB,EAA0B,EAChD,CACI,KAAK,QAAU,EACf,KAAK,SAAW,EAGpB,MAAM,EAAkB,GAAG,EAC3B,CACO,KAAK,UAAU,QAAQ,EAEtB,KAAK,QAAQ,MAAM,KAAK,SAAU,EAAS,GAAG,EAAK,CAI3D,MAAM,EAAkB,GAAG,EAC3B,CACO,KAAK,UAAU,QAAQ,EAEtB,KAAK,QAAQ,MAAM,KAAK,SAAU,EAAS,GAAG,EAAK,CAI3D,KAAK,EAAkB,GAAG,EAC1B,CACO,KAAK,UAAU,OAAO,EAErB,KAAK,QAAQ,KAAK,KAAK,SAAU,EAAS,GAAG,EAAK,CAI1D,KAAK,EAAkB,GAAG,EAC1B,CACO,KAAK,UAAU,OAAO,EAErB,KAAK,QAAQ,KAAK,KAAK,SAAU,EAAS,GAAG,EAAK,CAI1D,MAAM,EAAkB,GAAG,EAC3B,CACO,KAAK,UAAU,QAAQ,EAEtB,KAAK,QAAQ,MAAM,KAAK,SAAU,EAAS,GAAG,EAAK,CAI3D,KAAK,EACL,CACO,KAAK,WAAa,QAEjB,KAAK,QAAQ,KAAK,KAAK,SAAU,EAAM,CAI/C,QAAQ,EACR,CACO,KAAK,WAAa,QAEjB,KAAK,QAAQ,QAAQ,KAAK,SAAU,EAAM,CAOlD,UAAkB,EAClB,CACI,GAAG,KAAK,WAAa,OAEjB,MAAO,GAGX,OAAQ,KAAK,SAAb,CAEI,IAAK,QACD,MAAO,GACX,IAAK,QACD,OAAO,IAAU,QACrB,IAAK,OACD,OAAO,IAAU,QAAU,IAAU,QAAU,IAAU,QAC7D,IAAK,OACD,OAAO,IAAU,QAAU,IAAU,QACzC,IAAK,QACD,OAAO,IAAU,QACrB,QACI,MAAO,MAUV,EAAb,KACA,CACI,QACA,MACA,QAQA,YAAY,EAAmB,QAAS,EAA2B,IAAI,EACvE,CACI,KAAK,QAAU,EACf,KAAK,MAAQ,EACb,KAAK,QAAU,IAAI,IAQvB,WAAkB,EAClB,CACI,KAAK,QAAU,EAGf,KAAK,QAAQ,QAAS,GACtB,CACO,aAAkB,GAEjB,EAAO,eAAe,KAAK,QAAS,KAAK,MAAM,EAErD,CAQN,SAAgB,EAChB,CACI,KAAK,MAAQ,EAGb,KAAK,QAAQ,QAAS,GACtB,CACO,aAAkB,GAEjB,EAAO,eAAe,KAAK,QAAS,KAAK,MAAM,EAErD,CAMN,UACA,CACI,OAAO,KAAK,MAQhB,UAAiB,EACjB,CACI,IAAI,EAAS,KAAK,QAAQ,IAAI,EAAS,CAOvC,OANI,IAEA,EAAS,IAAI,EAAW,EAAU,KAAK,MAAO,KAAK,QAAQ,CAC3D,KAAK,QAAQ,IAAI,EAAU,EAAO,EAG/B,IC5GF,EAAb,KACA,CAMI,UAAoB,IAAI,IAKxB,YAAsB,IAAI,IAK1B,KAOA,YAAY,EACZ,CACI,KAAK,KAAO,GAAQ,UAAU,WAAW,EAAI,IAAI,EAAW,WAAW,CAiB3E,UACI,EACA,EAEJ,CAgBQ,OAfJ,KAAK,KAAK,MAAM,0BAA2B,EAAmB,UAAU,GAAI,CAIxE,aAA8B,QAC1B,OAAO,GAAuB,UAAY,EAAmB,SAAS,IAAI,CAGvE,KAAK,iBACR,EACA,EACH,CAIM,KAAK,eAAe,EAAyB,EAAS,CAYrE,eACI,EACA,EAEJ,CACI,KAAK,KAAK,MAAM,kCAAmC,IAAa,CAEhE,IAAI,EAAO,KAAK,UAAU,IAAI,EAAU,CACpC,IAEA,EAAO,IAAI,IACX,KAAK,UAAU,IAAI,EAAW,EAAK,EAIvC,IAAM,EAA8B,CAChC,SAAW,GAAU,EAAS,EAAmC,CACpE,CAGD,OAFA,EAAK,IAAI,EAAa,KAGtB,CACI,KAAK,KAAK,MAAM,oCAAqC,IAAa,CAClE,EAAK,OAAO,EAAa,CACtB,EAAK,OAAS,GAEb,KAAK,UAAU,OAAO,EAAU,EAgB5C,iBACI,EACA,EAEJ,CACI,IAAI,EAEJ,GAAG,OAAO,GAAoB,SAC9B,CAEI,IAAM,EAAU,EACX,QAAQ,uBAAwB,OAAO,CACvC,QAAQ,MAAO,KAAK,CAGzB,EAAY,OAAO,IAAK,EAAS,GAAG,CACpC,KAAK,KAAK,MACN,2CAA4C,EAAiB,WAAY,EAAM,UAAU,GAC5F,MAKD,EAAQ,EACR,KAAK,KAAK,MAAM,0CAA2C,EAAM,UAAU,GAAI,CAGnF,IAAM,EAA8B,CAChC,QAAS,EACT,SAAW,GAAU,EAAS,EAAoD,CACrF,CAGD,OAFA,KAAK,YAAY,IAAI,EAAa,KAGlC,CACI,KAAK,KAAK,MAAM,kCAAmC,EAAM,UAAU,GAAI,CACvE,KAAK,YAAY,OAAO,EAAa,EAwB7C,QACI,EAEJ,CACI,KAAK,KAAK,MAAM,qBAAsB,EAAM,OAAS,EAAM,CAE3D,IAAI,EAAgB,EAGd,EAAa,KAAK,UAAU,IAAI,EAAM,KAAK,CACjD,GAAG,EACH,CACI,GAAiB,EAAW,KAC5B,IAAI,IAAM,KAAO,EAEb,QAAQ,SAAS,CACZ,SAAW,EAAI,SAAS,EAAM,CAAC,CAC/B,MAAO,GACR,CACI,KAAK,KAAK,MAAM,+BAAgC,EAAM,KAAM,IAAK,EAAI,EACvE,CAKd,IAAI,IAAM,KAAO,KAAK,YAEf,EAAI,SAAW,EAAI,QAAQ,KAAK,EAAM,KAAK,GAE1C,IACA,QAAQ,SAAS,CACZ,SAAW,EAAI,SAAS,EAAM,CAAC,CAC/B,MAAO,GACR,CACI,KAAK,KAAK,MAAM,uCAAwC,EAAM,KAAM,IAAK,EAAI,EAC/E,EAIX,IAAkB,EAEjB,KAAK,KAAK,MAAM,mCAAoC,EAAM,OAAQ,CAIlE,KAAK,KAAK,MAAM,SAAU,EAAM,KAAM,iBAAkB,EAAe,cAAc,CAY7F,MAAa,WACb,CACI,KAAK,KAAK,MAAM,wBAAwB,CACxC,KAAK,UAAU,OAAO,CACtB,KAAK,YAAY,OAAO,GC5TnB,EAAb,KACA,CACI,QAAyC,KACzC,SAAyC,KACzC,OAAiB,IAAI,IACrB,KAEA,YAAY,EACZ,CACI,KAAK,KAAO,EAAO,UAAU,cAAc,CAO/C,MAAM,YACN,CACI,KAAK,KAAK,KAAK,+BAA+B,CAE9C,KAAK,QAAU,MAAA,EAAA,EAAA,yBAA8B,CAC7C,KAAK,SAAW,MAAM,KAAK,QAAQ,mBAAmB,OAAO,CAE7D,KAAK,KAAK,KAAK,4BAA4B,CAG/C,MAAM,WACN,CACI,KAAK,KAAK,KAAK,+BAA+B,CAE9C,KAAK,OAAO,OAAO,CACnB,KAAK,SAAW,KAEhB,AAGI,KAAK,WADL,KAAK,QAAQ,SAAS,CACP,MAGnB,KAAK,KAAK,KAAK,0BAA0B,CAO7C,MAAM,UAAU,EAChB,CACI,GAAG,CAAC,KAAK,SAAW,CAAC,KAAK,SAEtB,MAAU,MAAM,8BAA8B,CAGlD,KAAK,KAAK,MAAM,uBAAwB,IAAQ,CAChD,IAAM,EAAM,MAAM,KAAK,QAAQ,eAAe,EAAM,CAAE,OAAQ,KAAK,SAAU,CAAC,CAE9E,OADA,KAAK,OAAO,IAAI,EAAM,EAAI,CACnB,EAGX,OAAO,EACP,CACI,OAAO,KAAK,OAAO,IAAI,EAAK,CAOhC,MAAM,YACF,EACA,EACA,EACA,EAEJ,CACI,GAAG,CAAC,KAAK,QAEL,MAAU,MAAM,8BAA8B,CAGlD,IAAM,EAAuC,GAAO,KAAK,UAAY,IAAA,GAI/D,EAAe,CACjB,GAAG,EACH,GAAI,EAAS,CAAE,SAAQ,CAAG,EAAE,CAC/B,CAED,OAAO,KAAK,QAAQ,iBAAiB,EAAM,EAAK,EAAa,CAOjE,gBAAgB,EAChB,CACI,GAAG,CAAC,KAAK,QAEL,MAAU,MAAM,8BAA8B,CAGlD,KAAK,QAAQ,OAAS,EAG1B,iBACA,CAMI,OALI,KAAK,QAKF,KAAK,QAAQ,OAHT,EAMf,aAAa,EAAgB,EAC7B,CACI,EAAI,OAAS,KC7GrB,EAAA,EAAA,yBAAwB,CAaxB,IAAa,EAAb,KACA,CACI,QACA,QACA,OACA,KAEA,YAAY,EAAqB,EAAyB,EAAkC,EAC5F,CACI,KAAK,QAAU,EACf,KAAK,QAAU,EACf,KAAK,OAAS,EACd,KAAK,KAAO,EAAO,UAAU,cAAc,CAM/C,aACA,CACI,OAAO,IAAI,EAAA,MAAM,KAAK,QAAQ,CAGlC,cACI,EACA,EAA0B,IAAI,EAAA,QAAQ,EAAG,KAAM,EAAE,CACjD,EAEJ,CACI,KAAK,KAAK,MACN,kCAAmC,EAAc,EAAG,IAAK,EAAc,EAAG,IAAK,EAAc,EAAG,MACnG,CAED,IAAM,EAAgB,IAA8B,IAAA,GAE9C,IAAA,GADA,CAAE,4BAA2B,CAI7B,EAAS,IAAI,EAAA,YAAY,GAAM,KAAK,OAAQ,EAAc,CAChE,EAAM,cAAc,EAAe,EAAO,CAY9C,iBACI,EACA,EACA,EACA,EAEJ,CACI,IAAM,EAAS,IAAI,EAAA,WAAW,EAAM,EAAU,EAAM,CAUpD,OATA,EAAO,UAAU,EAAA,QAAQ,MAAM,CAAC,CAEhC,IAAmB,KAAK,QAErB,GAEC,EAAO,cAAc,EAAQ,GAAK,CAG/B,EAWX,uBACI,EACA,EACA,EACA,EAAY,GAEhB,CACI,IAAM,EAAQ,IAAI,EAAA,iBAAiB,EAAM,EAAW,EAAM,CAE1D,MADA,GAAM,UAAY,EACX,EAUX,uBACI,EACA,EACA,EACA,EAAY,EAEhB,CACI,IAAM,EAAQ,IAAI,EAAA,iBAAiB,EAAM,EAAW,EAAM,CAE1D,MADA,GAAM,UAAY,EACX,EAUX,iBACI,EACA,EACA,EACA,EAAY,EAEhB,CACI,IAAM,EAAQ,IAAI,EAAA,WAAW,EAAM,EAAU,EAAM,CAEnD,MADA,GAAM,UAAY,EACX,EAaX,gBACI,EACA,EACA,EACA,EACA,EACA,EACA,EAAY,EAEhB,CACI,IAAM,EAAQ,IAAI,EAAA,UAAU,EAAM,EAAU,EAAW,EAAO,EAAU,EAAM,CAE9E,MADA,GAAM,UAAY,EACX,EAYX,oBACI,EACA,EACA,EACA,EACA,EACA,EAAY,EAEhB,CACI,IAAM,EAAQ,IAAI,EAAA,cAAc,EAAM,EAAU,EAAO,EAAQ,EAAM,CAErE,MADA,GAAM,UAAY,EACX,EAUX,aACI,EACA,EAAuD,EAAE,CACzD,EAEJ,CAEI,OAAO,EAAA,YAAY,aAAa,EAAM,CADb,SAAU,EAAG,SAAU,GACW,GAAG,EAAS,CAAE,EAAM,CASnF,UACI,EACA,EAAmF,EAAE,CACrF,EAEJ,CAEI,OAAO,EAAA,YAAY,UAAU,EAAM,CADV,KAAM,EACyB,GAAG,EAAS,CAAE,EAAM,CAShF,aACI,EACA,EAA0E,EAAE,CAC5E,EAEJ,CAEI,OAAO,EAAA,YAAY,aAAa,EAAM,CADb,MAAO,EAAG,OAAQ,EAAG,aAAc,EACD,GAAG,EAAS,CAAE,EAAM,CASnF,eACI,EACA,EAAkF,EAAE,CACpF,EAEJ,CAEI,OAAO,EAAA,YAAY,eAAe,EAAM,CADf,OAAQ,EAAG,YAAa,EAAG,eAAgB,EACP,GAAG,EAAS,CAAE,EAAM,CAWrF,WACI,EACA,EACA,EAA+E,EAAE,CACjF,EAEJ,CAEI,OAAO,IAAI,EAAA,iBAAiB,EAAM,EAAW,CADtB,KAAM,EAAG,YAAa,IAAM,SAAU,GACG,GAAG,EAAc,CAAE,EAAM,CAS7F,MAAa,UACT,EACA,EAEJ,CACI,KAAK,KAAK,MAAM,kBAAmB,IAAiB,CAEpD,GACA,CACI,IAAM,EAAS,MAAA,EAAA,EAAA,yBAA8B,GAAI,IAAkB,EAAM,CAEzE,OADA,KAAK,KAAK,MAAM,8BAA+B,IAAiB,CACzD,QAEJ,EACP,CAEI,MADA,KAAK,KAAK,MAAM,yBAA0B,IAAkB,EAAM,CAC5D,GAUd,MAAa,aACT,EACA,EACA,EAUJ,CACI,KAAK,KAAK,MAAM,0BAA2B,IAAiB,CAE5D,GACA,CAEI,IAAM,EAAU,EAAU,OAAS,EAAI,CAAE,YAAW,CAAG,IAAA,GACjD,EAAS,MAAA,EAAA,EAAA,iBAAsB,GAAI,IAAkB,EAAO,EAAQ,CAE1E,OADA,KAAK,KAAK,MAAM,iCAAkC,EAAU,OAAS,EAAI,EAAU,KAAK,KAAK,CAAG,QAAS,CAClG,QAEJ,EACP,CAEI,MADA,KAAK,KAAK,MAAM,iCAAkC,IAAkB,EAAM,CACpE,GAOd,MAAM,WACN,CACI,KAAK,KAAK,KAAK,2BAA2B,CAM1C,KAAK,KAAK,KAAK,qCAAqC,GChW/C,GAAb,KACA,CACI,UACA,aACA,KAGA,YAAsB,IAAI,IAG1B,cAAwB,IAAI,IAI5B,YAAY,EAAyB,EAA2B,EAChE,CACI,KAAK,UAAY,EACjB,KAAK,aAAe,EACpB,KAAK,KAAO,GAAQ,UAAU,eAAe,EAAI,IAAI,EAAW,eAAe,CAUnF,WAAmB,EACnB,CACI,IAAM,EAAU,EAAK,QAAQ,IAAI,CAMjC,OALG,IAAY,GAEJ,CAAE,SAAU,EAAM,CAGtB,CACH,SAAU,EAAK,UAAU,EAAG,EAAQ,CACpC,SAAU,EAAK,UAAU,EAAU,EAAE,CACxC,CAOL,MAAc,eAAe,EAC7B,CACI,IAAM,EAAS,KAAK,YAAY,IAAI,EAAS,CAC7C,GAAG,EAGC,MADA,GAAO,WACA,EAAO,UAGlB,KAAK,KAAK,MAAM,4BAA6B,IAAY,CAGzD,IAAM,EAAY,KAAK,aAAa,aAAa,CAEjD,GACA,CACI,IAAM,EAAY,MAAM,KAAK,aAAa,UAAU,EAAU,EAAU,CAKxE,OAHA,KAAK,YAAY,IAAI,EAAU,CAAE,YAAW,SAAU,EAAG,CAAC,CAC1D,KAAK,KAAK,MAAM,2BAA4B,IAAY,CAEjD,SAGX,CACI,EAAU,SAAS,EAO3B,qBAA6B,EAA4B,EAAmB,EAC5E,CACI,IAAM,EAAO,EAAU,OAAO,KAAM,GAAU,EAAM,OAAS,EAAS,CACtE,GAAG,CAAC,EACJ,CACI,IAAM,EAAY,EAAU,OAAO,IAAK,GAAU,EAAM,KAAK,CAAC,KAAK,KAAK,CACxE,MAAU,MACN,SAAU,EAAU,gCAAiC,EAAU,uBACvC,IAC3B,CAGL,OAAO,EAeX,MAAM,KAAK,EACX,CACI,GAAM,CAAE,WAAU,YAAa,KAAK,WAAW,EAAK,CAEpD,GAAG,EACH,CAEI,IAAM,EAAa,KAAK,cAAc,IAAI,EAAK,CAC/C,GAAG,EACH,CAEI,IAAM,EAAS,KAAK,YAAY,IAAI,EAAS,CAM7C,OALG,GAEC,EAAO,WAGJ,EAIX,IAAM,EAAY,MAAM,KAAK,eAAe,EAAS,CAG/C,EAAO,KAAK,qBAAqB,EAAW,EAAU,EAAK,CAGjE,OAFA,KAAK,cAAc,IAAI,EAAM,EAAK,CAE3B,EAIX,OAAO,KAAK,eAAe,EAAS,CAMxC,SAAS,EACT,CACI,IAAM,EAAa,KAAK,cAAc,IAAI,EAAK,CAC/C,GAAG,CAAC,EAEA,MAAU,MAAM,UAAW,EAAM,qCAAqC,CAG1E,OAAQ,EAAoB,eAAe,GAAI,EAAW,KAAM,WAAW,CAM/E,MAAM,EACN,CACI,IAAM,EAAa,KAAK,cAAc,IAAI,EAAK,CAC/C,GAAG,CAAC,EAEA,MAAU,MAAM,UAAW,EAAM,qCAAqC,CAG1E,OAAQ,EAAoB,MAAM,GAAI,EAAW,KAAM,QAAQ,CAMnE,MAAM,QAAQ,EACd,CACI,IAAM,EAAQ,EAAM,OAEpB,IAAI,IAAI,EAAI,EAAG,EAAI,EAAM,OAAQ,IAG7B,MAAM,KAAK,KAAK,EAAM,GAAG,CAEzB,KAAK,UAAU,QAAQ,CACnB,KAAM,iBACN,QAAS,CACL,KAAM,EAAM,GACZ,OAAQ,EAAI,EACZ,QACH,CACJ,CAAC,CAGN,KAAK,UAAU,QAAQ,CACnB,KAAM,iBACN,QAAS,CAAE,QAAO,CACrB,CAAC,CAON,QAAQ,EACR,CACI,GAAM,CAAE,YAAa,KAAK,WAAW,EAAK,CACpC,EAAS,KAAK,YAAY,IAAI,EAAS,CACzC,OAKJ,EAAO,WAEJ,EAAO,UAAY,GACtB,CACI,KAAK,KAAK,MAAM,8BAA+B,EAAU,uBAAuB,CAGhF,IAAI,GAAM,CAAE,EAAc,KAAU,KAAK,cAElC,EAAa,WAAW,GAAI,EAAU,GAAG,GAExC,EAAK,SAAS,CACd,KAAK,cAAc,OAAO,EAAa,EAI/C,EAAO,UAAU,SAAS,CAC1B,KAAK,YAAY,OAAO,EAAS,EAOzC,YACA,CACI,IAAI,IAAM,KAAU,KAAK,YAAY,QAAQ,CAEzC,EAAO,UAAU,SAAS,CAG9B,IAAI,IAAM,KAAQ,KAAK,cAAc,QAAQ,CAEzC,EAAK,SAAS,CAGlB,KAAK,YAAY,OAAO,CACxB,KAAK,cAAc,OAAO,CAM9B,MAAM,WACN,CACI,KAAK,YAAY,GCvQZ,EAA+B,CACxC,UACA,SACA,QACH,CCfY,EAAmB,CAAE,WAAY,QAAS,UAAW,CAyHlE,SAAgB,EAAgB,EAChC,CACI,OAAO,EAAM,OAAS,WAM1B,SAAgB,EAAa,EAC7B,CACI,OAAO,EAAM,OAAS,QAM1B,SAAgB,EAAe,EAC/B,CACI,OAAO,EAAM,OAAS,UCzF1B,IAAa,GAAb,KACA,CACI,KAAuB,UACvB,OACA,QACA,SACA,WACA,OAGA,UACA,WAGA,kBAA4B,GAS5B,IAAI,SACJ,CACI,MAAO,CACH,SAAU,KAAK,UACf,UAAW,KAAK,WACnB,CAcL,YACI,EACA,EACA,EACA,EACA,EAAkC,EAAE,CAExC,CACI,KAAK,OAAS,EACd,KAAK,SAAW,EAChB,KAAK,WAAa,EAClB,KAAK,OAAS,EACd,KAAK,QAAU,EAAQ,QAGvB,KAAK,UAAY,EAAQ,UAAY,SACrC,KAAK,WAAa,EAAQ,WAAa,GAa3C,QAAe,EAAoB,EACnC,CAEI,IAAM,EAAW,KAAK,OAAO,SAAS,EAAM,EAAI,GAG1C,EAAe,OAAO,GAAa,UACnC,EACA,GAAY,KAAK,WAGnB,EAAgB,GAEpB,OAAQ,KAAK,UAAb,CAEI,IAAK,SACD,EAAgB,GAAgB,CAAC,KAAK,kBACtC,MACJ,IAAK,UACD,EAAgB,CAAC,GAAgB,KAAK,kBACtC,MACJ,IAAK,OACD,EAAgB,IAAiB,KAAK,kBACtC,MAOR,GAHA,KAAK,kBAAoB,EAGtB,EACH,CACI,IAAI,EACJ,GAAG,KAAK,OAAO,OAAS,SACxB,CAEI,IAAI,EAAa,OAAO,GAAa,SAAW,EAAY,EAAW,EAAM,EAG7E,GAAG,KAAK,OAAO,WAAa,IAAA,IAAa,KAAK,OAAO,WAAa,IAAA,GAClE,CACI,IAAM,EAAM,KAAK,OAAO,UAAY,EAC9B,EAAM,KAAK,OAAO,UAAY,EACpC,EAAa,EAAO,GAAc,EAAM,GAE5C,EAAc,OAId,EAAc,GAElB,IAAM,EAAc,CAChB,KAAM,UAAW,KAAK,OAAO,OAC7B,QAAS,CACL,MAAO,EACP,SAAU,KAAK,SACf,QAAS,KAAK,QACjB,CACJ,CACD,EAAS,QAAQ,EAAY,EAQrC,eAAsB,EACtB,CACI,GAAG,EACH,CACI,IAAM,EAAW,KAAK,OAAO,SAAS,EAAM,EAAI,GAChD,KAAK,kBAAoB,OAAO,GAAa,UACvC,EACA,GAAY,KAAK,gBAIvB,KAAK,kBAAoB,GAOjC,QACA,CACI,MAAO,CACH,KAAM,KAAK,KACX,OAAQ,KAAK,OAAO,KACpB,MAAO,CACH,SAAU,KAAK,SACf,GAAG,KAAK,OAAO,QAAQ,CAC1B,CACD,QAAS,KAAK,QACd,QAAS,KAAK,QACjB,GClKI,GAAb,KACA,CACI,KAAuB,SACvB,OACA,QACA,SACA,WACA,OAGA,QACA,WACA,cACA,SACA,UAGA,kBAA4B,GAC5B,aASA,IAAW,OACX,CACI,OAAO,KAAK,aAMhB,IAAW,MAAM,EACjB,CACI,KAAK,aAAe,EAMxB,IAAW,SACX,CACI,OAAO,KAAK,SAMhB,IAAW,UACX,CACI,OAAO,KAAK,UAOhB,IAAW,OACX,CASQ,OARD,KAAK,OAAO,OAAS,SAEb,KAAK,aACL,KAAK,OAAO,UAAY,EACxB,KAAK,OAAO,UAAY,EAIxB,KAAK,aAAe,KAAK,SAAW,KAAK,UAOxD,IAAW,SACX,CACI,MAAO,CACH,OAAQ,KAAK,QACb,UAAW,KAAK,WAChB,aAAc,KAAK,cACnB,QAAS,KAAK,SACd,SAAU,KAAK,UAClB,CAcL,YACI,EACA,EACA,EACA,EACA,EAAiC,EAAE,CAEvC,CACI,KAAK,OAAS,EACd,KAAK,SAAW,EAChB,KAAK,WAAa,EAClB,KAAK,OAAS,EACd,KAAK,QAAU,EAAQ,QAGvB,KAAK,QAAU,EAAQ,QAAU,GACjC,KAAK,WAAa,EAAQ,WAAa,GACvC,KAAK,cAAgB,EAAQ,cAAgB,GAC7C,KAAK,aAAe,KAAK,cAIzB,KAAK,SAAW,EAAQ,SAAW,GACnC,KAAK,UAAY,EAAQ,UAAY,GAazC,QAAe,EAAoB,EACnC,CACI,IAAM,EAAW,KAAK,OAAO,SAAS,EAAM,EAAI,GAC1C,EAAe,OAAO,GAAa,UACnC,EACA,GAAY,KAAK,WAEjB,EAAe,KAAK,QACpB,CAAC,GAAgB,KAAK,kBACtB,GAAgB,CAAC,KAAK,kBAE5B,KAAK,kBAAoB,EAErB,IAEJ,KAAK,aAAe,CAAC,KAAK,aAE1B,EAAS,QAAQ,CACb,KAAM,UAAW,KAAK,OAAO,OAC7B,QAAS,CACL,MAAO,KAAK,MACZ,SAAU,KAAK,SACf,QAAS,KAAK,QACjB,CACJ,CAAC,EAON,eAAsB,EACtB,CACI,GAAG,EACH,CACI,IAAM,EAAW,KAAK,OAAO,SAAS,EAAM,EAAI,GAChD,KAAK,kBAAoB,OAAO,GAAa,UACvC,EACA,GAAY,KAAK,gBAIvB,KAAK,kBAAoB,GAOjC,OACA,CACI,KAAK,aAAe,KAAK,cAM7B,QACA,CACI,MAAO,CACH,KAAM,KAAK,KACX,OAAQ,KAAK,OAAO,KACpB,MAAO,CACH,SAAU,KAAK,SACf,GAAG,KAAK,OAAO,QAAQ,CAC1B,CACD,MAAO,KAAK,aACZ,QAAS,KAAK,QACd,QAAS,KAAK,QACjB,GC5LI,GAAb,KACA,CACI,KAAuB,QACvB,OACA,QACA,SACA,WACA,OAGA,OACA,QACA,QACA,cACA,UACA,SACA,UACA,KACA,KAGA,WAOA,IAAI,SACJ,CACI,MAAO,CACH,MAAO,KAAK,OACZ,OAAQ,KAAK,QACb,OAAQ,KAAK,QACb,aAAc,KAAK,cACnB,SAAU,KAAK,UACf,QAAS,KAAK,SACd,SAAU,KAAK,UACf,IAAK,KAAK,KACV,IAAK,KAAK,KACb,CAaL,YACI,EACA,EACA,EACA,EACA,EAAgC,EAAE,CAEtC,CACI,KAAK,OAAS,EACd,KAAK,SAAW,EAChB,KAAK,WAAa,EAClB,KAAK,OAAS,EACd,KAAK,QAAU,EAAQ,QAGvB,KAAK,OAAS,EAAQ,OAAS,EAC/B,KAAK,QAAU,EAAQ,QAAU,EACjC,KAAK,QAAU,EAAQ,QAAU,GACjC,KAAK,cAAgB,EAAQ,cAAgB,GAC7C,KAAK,UAAY,EAAQ,UAAY,EAIrC,KAAK,SAAW,EAAQ,SAAW,GACnC,KAAK,UAAY,EAAQ,UAAY,GAGrC,IAAM,EAAa,KAAK,OAAO,OAAS,SAClC,KAAK,OAAO,UAAY,KACxB,KACN,KAAK,KAAO,EAAQ,KAAO,EAE3B,IAAM,EAAa,KAAK,OAAO,OAAS,SAClC,KAAK,OAAO,UAAY,IACxB,IACN,KAAK,KAAO,EAAQ,KAAO,EAa/B,QAAe,EAAoB,EACnC,CACI,IAAM,EAAW,KAAK,OAAO,SAAS,EAAM,CAC5C,GAAG,IAAa,IAAA,GAAa,OAE7B,IAAM,EAAe,OAAO,GAAa,UAAa,EAAW,EAAM,EAAO,EAE1E,EAAQ,KAAK,UAAY,GAAK,KAAK,IAAI,EAAa,CAAG,KAAK,UAAY,EAAI,EAChF,GAAU,KAAK,QAAU,CAAC,EAAQ,GAAS,KAAK,OAAU,KAAK,QAG/D,EAAQ,KAAK,IAAI,KAAK,KAAM,KAAK,IAAI,KAAK,KAAM,EAAM,CAAC,CAEpD,OAAK,eAAiB,KAAK,aAAe,KAC7C,KAAK,WAAa,EAElB,EAAS,QAAQ,CACb,KAAM,UAAW,KAAK,OAAO,OAC7B,QAAS,CACL,QACA,SAAU,KAAK,SACf,QAAS,KAAK,QACjB,CACJ,CAAC,EAMN,eAAsB,EACtB,EAOA,QACA,CACI,MAAO,CACH,KAAM,KAAK,KACX,OAAQ,KAAK,OAAO,KACpB,MAAO,CACH,SAAU,KAAK,SACf,GAAG,KAAK,OAAO,QAAQ,CAC1B,CACD,QAAS,KAAK,QACd,QAAS,KAAK,QACjB,GChMI,GAAb,MAAa,CACb,CAII,WAAkD,MAKlD,UAKA,SAQA,YAAY,EAAkB,EAAkC,EAAE,CAClE,CACI,KAAK,UAAY,EACjB,KAAK,SAAW,EAAQ,UAAY,GAQxC,SAAgB,EAChB,CAEI,GAAG,EAAM,OAAS,WAEd,OAGJ,IAAM,EAAgB,EASlB,OAND,KAAK,SAEG,EAAc,MAAM,KAAK,WAIzB,EAAc,KAAK,KAAK,WAUvC,OAAc,WAAW,EAAoB,EAAkC,EAAE,CACjF,CACI,OAAO,IAAI,EAAoB,EAAW,EAAQ,CAMtD,QACA,CACI,MAAO,CAEH,KAAM,WACN,WAAY,KAAK,WACjB,UAAW,KAAK,UAChB,QAAS,CACL,SAAU,KAAK,SAClB,CACJ,GChGI,GAAb,MAAa,CACb,CAII,WAKA,UAQA,YAAY,EAA8B,EAC1C,CACI,KAAK,WAAa,EAClB,KAAK,UAAY,EAQrB,SAAgB,EAChB,CAEO,KAAM,OAAS,QAKlB,OAAQ,KAAK,WAAb,CAEI,IAAK,SACL,CACI,IAAM,EAAc,EAAM,QAAQ,KAAK,WACvC,OAAO,EAAc,EAAY,QAAU,IAAA,GAG/C,IAAK,WACL,CAEI,GAAM,CAAE,EAAS,GAAS,KAAK,UAAU,MAAM,IAAI,CAQnD,MANG,CAAC,GAAW,CAAC,GAAS,IAAY,YAAc,IAAY,YACvD,IAAS,KAAO,IAAS,IAE7B,OAGG,EAAM,SAAS,GAAS,GAGnC,IAAK,QACL,CACI,IAAM,EAAkB,KAAK,YAAc,UAAY,KAAK,YAAc,UACnE,KAAK,YAAc,SAK1B,OAJG,EAAM,OAAS,EAEP,EAAM,MAAM,KAAK,WAE5B,OAGJ,QACI,QASZ,OAAc,WAAW,EACzB,CACI,GAAM,CAAE,EAAY,GAAG,GAAa,EAAiB,MAAM,IAAI,CACzD,EAAY,EAAS,KAAK,IAAI,CAEpC,GAAG,CAAC,GAAc,CAAC,EAEf,MAAU,MAAM,gCAAiC,IAAoB,CAGzE,OAAO,IAAI,EACP,EACA,EACH,CAML,QACA,CAEI,MAAO,CACH,KAAM,QACN,WAAY,KAAK,WACjB,UAAW,KAAK,UACnB,GCpFI,GAAb,MAAa,CACb,CAII,WAKA,UAKA,eAKA,SAKA,OASA,YAAY,EAAgC,EAAoB,EAAiC,EAAE,CACnG,CACI,KAAK,WAAa,EAClB,KAAK,UAAY,EACjB,KAAK,eAAiB,EAAQ,gBAAkB,GAChD,KAAK,SAAW,EAAQ,UAAY,GACpC,KAAK,OAAS,EAAQ,QAAU,GAQpC,SAAgB,EAChB,CAEO,KAAM,OAAS,UAKlB,OAAQ,KAAK,WAAb,CAEI,IAAK,SACL,CACI,IAAM,EAAc,EAAM,QAAQ,KAAK,WAOvC,OALI,EAKG,KAAK,eAAiB,EAAY,MAAQ,EAAY,QAHzD,OAMR,IAAK,OACL,CACI,IAAI,EAAQ,EAAM,KAAK,KAAK,WAc5B,OAZG,IAAU,IAAA,GAET,QAID,KAAK,IAAI,EAAM,CAAG,KAAK,WAEtB,EAAQ,GAIL,KAAK,OAAS,CAAC,EAAQ,GAGlC,QACI,QAUZ,OAAc,WAAW,EAA2B,EAAiC,EAAE,CACvF,CACI,GAAM,CAAE,EAAY,GAAc,EAAiB,MAAM,IAAI,CAE7D,GAAG,CAAC,GAAc,CAAC,EAEf,MAAU,MAAM,kCAAmC,IAAoB,CAG3E,OAAO,IAAI,EACP,EACA,EACA,EACH,CAML,QACA,CACI,MAAO,CACH,KAAM,UACN,WAAY,KAAK,WACjB,UAAW,KAAK,UAChB,QAAS,CACL,eAAgB,KAAK,eACrB,SAAU,KAAK,SACf,OAAQ,KAAK,OAChB,CACJ,GCtGH,GAA4B,IAM5B,GAAwB,GAmBjB,GAAb,KACA,CAEI,UAAoB,IAAI,IAGxB,SAAmB,IAAI,IAGvB,UAAoB,IAAI,IAKxB,gBAA0B,IAAI,IAG9B,sBAAoD,KAGpD,gBAA0B,IAAI,IAG9B,cAA8C,KAG9C,UAGA,kBAAkD,KAGlD,KASA,YAAY,EAAyB,EACrC,CACI,KAAK,UAAY,EAGjB,KAAK,kBAAoB,KAAK,UAAU,UACpC,gBACC,GACD,CACI,IAAM,EAAU,EAAM,QACnB,GAEC,KAAK,aAAa,EAAQ,OAAQ,EAAQ,MAAM,EAG3D,CAED,KAAK,KAAO,GAAQ,UAAU,iBAAiB,EAAI,IAAI,EAAW,iBAAiB,CACnF,KAAK,KAAK,MAAM,6BAA6B,CAajD,wBAAgC,EAChC,CAQI,OANI,EAAQ,QAML,KAAK,gBAAgB,IAAI,EAAQ,QAAQ,CAJrC,GAaf,oBAA4B,EAAsB,EAAY,GAC9D,CACI,IAAI,EAAU,KAAK,UAAU,IAAI,EAAY,CAa7C,OAXI,IAEA,EAAU,CACN,KAAM,EACN,YACH,CAED,KAAK,UAAU,IAAI,EAAa,EAAQ,CACxC,KAAK,KAAK,MAAM,yBAA0B,EAAa,gBAAiB,EAAW,GAAG,EAGnF,EASX,6BAAqC,EACrC,CACI,IAAM,EAAiC,EAAE,CAGzC,IAAI,IAAM,KAAe,KAAK,gBAGvB,IAAgB,GAKH,KAAK,UAAU,IAAI,EAAY,EAGnC,YAER,KAAK,gBAAgB,OAAO,EAAY,CACxC,EAAoB,KAAK,EAAY,EAI7C,OAAO,EASX,0BAAkC,EAClC,CAeI,OAdG,EAAa,EAAM,CAGX,OAAO,OAAO,EAAM,QAAQ,CAAC,KAAM,GAAQ,EAAI,QAAQ,EAAI,EAAM,QAAU,IAAA,GAGnF,EAAe,EAAM,CAGb,OAAO,OAAO,EAAM,QAAQ,CAAC,KAAM,GAAQ,EAAI,QAAQ,EACvD,OAAO,OAAO,EAAM,KAAK,CAAC,KAAM,GAAQ,KAAK,IAAI,EAAI,CAAG,GAA0B,CAItF,GASX,6BAAqC,EACrC,CAEI,IAAM,EAAS,KAAK,SAAS,IAAI,EAAW,OAAO,CAGnD,GAAG,CAAC,EAGA,OADA,KAAK,KAAK,KAAK,kCAAmC,EAAW,OAAQ,cAAc,CAC5E,KAIX,GAAM,CAAE,WAAU,GAAG,GAAc,EAAW,MAE9C,OAAQ,EAAW,KAAnB,CAEI,IAAK,UAED,OAAO,IAAI,GACP,EACA,EACA,EAAW,MAAM,KACjB,KAAK,iCAAiC,EAAU,CAChD,CACI,GAAI,EAAW,SAAW,EAAE,CAC5B,QAAS,EAAW,QACvB,CACJ,CAGL,IAAK,SAED,OAAO,IAAI,GACP,EACA,EACA,EAAW,MAAM,KACjB,KAAK,iCAAiC,EAAU,CAChD,CACI,GAAI,EAAW,SAAW,EAAE,CAC5B,QAAS,EAAW,QACvB,CACJ,CAGL,IAAK,QAED,OAAO,IAAI,GACP,EACA,EACA,EAAW,MAAM,KACjB,KAAK,iCAAiC,EAAU,CAChD,CACI,GAAI,EAAW,SAAW,EAAE,CAC5B,QAAS,EAAW,QACvB,CACJ,CAGL,QAEI,OADA,KAAK,KAAK,MAAM,iCAAmC,EAAiC,OAAQ,CACrF,MAUnB,iCAAyC,EACzC,CAEI,OAAQ,EAAW,KAAnB,CAEI,IAAK,WACD,OAAO,IAAI,GACP,EAAW,UACX,EAAW,QACd,CAEL,IAAK,QACL,CAEI,IAAM,EAAa,EAAW,WAC9B,GAAG,EAAE,IAAe,UAAY,IAAe,YAAc,IAAe,SAExE,MAAU,MAAM,8BAA+B,IAAc,CAGjE,OAAO,IAAI,GACP,EACA,EAAW,UACd,CAGL,IAAK,UACL,CAEI,IAAM,EAAa,EAAW,WAC9B,GAAG,EAAE,IAAe,UAAY,IAAe,QAE3C,MAAU,MAAM,gCAAiC,IAAc,CAEnE,OAAO,IAAI,GACP,EACA,EAAW,UACX,EAAW,QACd,CAGL,QACI,MAAU,MAAM,kCAAoC,EAA2C,OAAQ,EAWnH,qBAA6B,EAAsB,EACnD,CACI,IAAM,EAAU,KAAK,cACrB,GAAG,CAAC,EAEA,OAGJ,GAAM,CAAE,WAAY,EAGpB,GAAG,CAAC,EAAQ,YAAY,SAAS,EAAO,KAAK,CAEzC,OAGJ,IAAI,EAAkC,KAEtC,GAAG,EAAgB,EAAM,CACzB,CAEI,GAAG,EAAM,OAAO,SAAW,GAEvB,OAIJ,IAAI,IAAM,KAAO,OAAO,KAAK,EAAM,MAAM,CAErC,GAAG,EAAM,MAAM,KAAS,GACxB,CACI,EAAS,CAAE,KAAM,WAAY,WAAY,MAAO,UAAW,EAAK,SAAU,EAAO,GAAI,CACrF,eAIJ,EAAa,EAAM,MAGnB,IAAM,KAAO,OAAO,KAAK,EAAM,QAAQ,CAEvC,GAAG,EAAM,QAAQ,GAAK,QACtB,CACI,EAAS,CAAE,KAAM,QAAS,WAAY,SAAU,UAAW,EAAK,SAAU,EAAO,GAAI,CACrF,eAIJ,EAAe,EAAM,CAC7B,CAEI,IAAI,IAAM,KAAO,OAAO,KAAK,EAAM,QAAQ,CAEvC,GAAG,EAAM,QAAQ,GAAK,QACtB,CACI,EAAS,CAAE,KAAM,UAAW,WAAY,SAAU,UAAW,EAAK,SAAU,EAAO,GAAI,CACvF,MAKR,GAAG,CAAC,OAEI,IAAM,KAAO,OAAO,KAAK,EAAM,KAAK,CAEpC,GAAG,KAAK,IAAI,EAAM,KAAK,GAAK,CAAG,GAC/B,CACI,EAAS,CAAE,KAAM,UAAW,WAAY,OAAQ,UAAW,EAAK,SAAU,EAAO,GAAI,CACrF,QAahB,GANG,CAAC,GAMD,EAAQ,aACJ,CAAC,EAAQ,YAAY,SAAS,EAAO,WAAiD,CAGzF,OAIJ,GAAM,CAAE,WAAY,EACpB,KAAK,iBAAiB,CACtB,EAAQ,EAAO,CAOnB,iBACA,CACI,IAAM,EAAU,KAAK,cACrB,GAAG,CAAC,EAEA,OAID,EAAQ,cAEP,EAAQ,cAAc,CAG1B,GAAM,CAAE,YAAa,EACrB,KAAK,cAAgB,KAGrB,KAAK,kBAAkB,mBAAmB,CAC1C,KAAK,UAAU,OAAO,mBAAmB,CAEzC,IAAI,IAAM,KAAe,EAErB,KAAK,gBAAgB,EAAY,CAoBzC,aAAoB,EAAsB,EAC1C,CAMI,GAJA,KAAK,gBAAgB,IAAI,EAAO,GAAI,EAAM,CAIvC,KAAK,0BAA0B,EAAM,EAAI,EAAO,OAAS,KAAK,sBACjE,CACI,IAAM,EAAqB,KAAK,sBAChC,KAAK,sBAAwB,EAAO,KACpC,KAAK,UAAU,QAAQ,CACnB,KAAM,4BACN,QAAS,CAAE,WAAY,EAAO,KAAM,qBAAoB,CAC3D,CAAC,CAIN,GAAG,KAAK,cACR,CACI,KAAK,qBAAqB,EAAQ,EAAM,CACxC,OAGJ,IAAM,EAAW,KAAK,UAAU,IAAI,EAAO,GAAG,CAC3C,MAAC,GAAY,EAAS,SAAW,IAKjC,OAAK,gBAAgB,OAAS,GAAK,EAAS,KAAM,GAAY,EAAQ,QAAQ,EAKjF,IAAI,IAAM,KAAW,EAEb,KAAK,wBAAwB,EAAQ,EAKzC,EAAQ,QAAQ,EAAO,KAAK,UAAU,CAc9C,eAAsB,EACtB,CAGI,GAFA,KAAK,KAAK,MAAM,uBAAwB,EAAO,KAAM,GAAG,CAErD,KAAK,SAAS,IAAI,EAAO,KAAK,CACjC,CACI,IAAM,EAAW,WAAY,EAAO,KAAM,uBAE1C,MADA,KAAK,KAAK,MAAM,EAAS,CACf,MAAM,EAAS,CAG7B,KAAK,SAAS,IAAI,EAAO,KAAM,EAAO,CACtC,KAAK,KAAK,MAAM,WAAY,EAAO,KAAM,2BAA2B,CAQxE,UAAiB,EACjB,CACI,KAAK,KAAK,MAAM,mBAAoB,EAAY,GAAG,CAEnD,IAAM,EAAS,KAAK,SAAS,IAAI,EAAW,EAAI,KAMhD,OALI,GAEA,KAAK,KAAK,MAAM,WAAY,EAAY,aAAa,CAGlD,EAWX,IAAW,sBACX,CACI,OAAO,KAAK,sBAehB,aAAoB,EACpB,CACI,GAAG,KAAK,cAEJ,MAAU,MAAM,qCAAqC,CAIzD,IAAM,EAAW,CAAE,GAAG,KAAK,gBAAiB,CAM5C,OAHA,KAAK,gBAAgB,mBAAoB,GAAK,CAC9C,KAAK,gBAAgB,mBAAmB,CAEjC,IAAI,SAA0B,EAAS,IAC9C,CACI,IAAM,EAA8B,CAAE,UAAS,SAAQ,UAAS,WAAU,CAI1E,GAHA,KAAK,cAAgB,EAGlB,EAAQ,OACX,CACI,GAAG,EAAQ,OAAO,QAClB,CACI,KAAK,iBAAiB,CACtB,EAAO,EAAQ,OAAO,QAAc,MAAM,yBAAyB,CAAC,CACpE,OAGJ,IAAM,MACN,CACI,IAAM,EAAS,EAAQ,QAAQ,QAAc,MAAM,yBAAyB,CAC5E,KAAK,iBAAiB,CACtB,EAAO,EAAO,EAGlB,EAAQ,OAAO,iBAAiB,QAAS,EAAS,CAAE,KAAM,GAAM,CAAC,CAEjE,EAAa,iBACb,CACI,EAAQ,QAAQ,oBAAoB,QAAS,EAAQ,IAG/D,CAcN,gBAAuB,EAAsB,EAAY,GACzD,CACI,IAAM,EAAU,KAAK,oBAAoB,EAAa,EAAU,CAShE,OANG,EAAQ,YAAc,IAErB,EAAQ,UAAY,EACpB,KAAK,KAAK,KAAK,oBAAqB,EAAa,iBAAkB,IAAa,EAG7E,EASX,gBAAuB,EACvB,CAKI,IAAM,EAHU,KAAK,oBAAoB,EAAY,CAGzB,UAE5B,KAAK,KAAK,MAAM,uBAAwB,EAAa,gBAAiB,EAAa,GAAG,CAGtF,IAAM,EAAkB,KAAK,gBAAgB,IAAI,EAAY,CAE7D,GAAG,EACH,CAEI,IAAM,EAAc,KAAK,6BAA6B,EAAY,CAE/D,EAAY,OAAS,GAEpB,KAAK,KAAK,KAAK,mCAAoC,EAAY,KAAK,KAAK,GAAI,CAKrF,GAAG,CAAC,EACJ,CACI,KAAK,gBAAgB,IAAI,EAAY,CAKrC,IAAI,GAAM,CAAE,EAAU,KAAoB,KAAK,UAAU,SAAS,CAClE,CACI,IAAM,EAAY,KAAK,gBAAgB,IAAI,EAAS,CACpD,IAAI,IAAM,KAAW,EAEd,EAAQ,UAAY,GAEnB,EAAQ,eAAe,EAAU,CAK7C,KAAK,KAAK,KAAK,YAAa,EAAa,aAAc,EAAc,gBAAkB,KAAM,EASrG,kBAAyB,EACzB,CACI,KAAK,KAAK,MAAM,yBAA0B,EAAa,GAAG,CAEvD,KAAK,gBAAgB,IAAI,EAAY,EAEpC,KAAK,gBAAgB,OAAO,EAAY,CACxC,KAAK,KAAK,KAAK,YAAa,EAAa,eAAe,EAIxD,KAAK,KAAK,MAAM,YAAa,EAAa,kBAAkB,CAOpE,mBACA,CACI,MAAO,CAAE,GAAG,KAAK,gBAAiB,CAQtC,gBAAuB,EACvB,CACI,OAAO,KAAK,gBAAgB,IAAI,EAAY,CAQhD,WAAkB,EAClB,CACI,OAAO,KAAK,UAAU,IAAI,EAAY,EAAI,KAc9C,iBAAwB,EACxB,CACI,GAAG,CAAC,EAAa,SAAS,EAAQ,KAAK,CAEnC,MAAU,MAAM,yBAA0B,EAAQ,OAAQ,CAI3D,EAAQ,SAAW,CAAC,KAAK,UAAU,IAAI,EAAQ,QAAQ,EAEtD,KAAK,gBAAgB,EAAQ,QAAQ,CAIrC,KAAK,UAAU,IAAI,EAAQ,SAAS,EAEpC,KAAK,UAAU,IAAI,EAAQ,SAAU,EAAE,CAAC,CAI5C,KAAK,UAAU,IAAI,EAAQ,SAAS,EAAE,KAAK,EAAQ,CACnD,KAAK,KAAK,MAAM,cAAe,EAAQ,KAAM,gBAAiB,EAAQ,OAAO,KAAM,gBACxE,EAAQ,SAAW,KAAM,GAAG,CAS3C,gBAAuB,EACvB,CACI,IAAM,EAAU,KAAK,6BAA6B,EAAW,CAE1D,EAEC,KAAK,iBAAiB,EAAQ,CAI9B,KAAK,KAAK,MAAM,wCAAyC,EAAW,OAAQ,eACjE,EAAW,KAAM,GAAG,CAUvC,mBAA0B,EAAqB,EAA0B,KACzE,CACI,KAAK,KAAK,MAAM,0CAA2C,EAAY,gBAAiB,EAAS,GAAG,CAEpG,IAAI,EAAkB,EAGtB,IAAI,GAAM,CAAE,EAAU,KAAoB,KAAK,UAAU,SAAS,CAClE,CACI,IAAM,EAAc,EAAe,OAAQ,GAC3C,CACI,IAAM,EAAiB,EAAQ,SAAW,KACpC,EAAa,EAAQ,OAAO,OAAS,GAAc,IAAmB,EAK5E,OAJI,GAEA,IAEG,GACT,CAGC,EAAY,SAAW,EAAe,QAErC,KAAK,UAAU,IAAI,EAAU,EAAY,CAIjD,KAAK,KAAK,KAAK,WAAY,EAAiB,wBAAyB,EAAY,gBAAiB,EAAS,GAAG,CAUlH,qBAA4B,EAAqB,EAA0B,EAC3E,CACI,IAAM,EAAqB,EAAE,CAE7B,IAAI,IAAM,KAAkB,KAAK,UAAU,QAAQ,CAE/C,IAAI,IAAM,KAAW,EACrB,CAEI,GADG,EAAQ,OAAO,OAAS,GACxB,IAAe,IAAA,IAAa,EAAQ,aAAe,EAAc,SAEpE,IAAM,EAAiB,EAAQ,SAAW,KACvC,IAAY,IAAA,IAAa,IAAmB,GAE/C,EAAO,KAAK,EAAQ,CAI5B,OAAO,EAWX,qBACA,CACI,KAAK,KAAK,MAAM,kCAAkC,CAGlD,IAAM,EAAU,CAAE,GAAG,KAAK,SAAS,QAAQ,CAAE,CAAC,IAAK,GAG5C,EAAO,OAAS,SAER,CACH,KAAM,EAAO,KACb,KAAM,EAAO,KACb,MAAO,EAAO,MACd,SAAU,EAAO,UAAY,EAC7B,SAAU,EAAO,UAAY,EAChC,CAEE,EACT,CAGI,EAAiC,EAAE,CAGzC,IAAI,IAAM,KAAkB,KAAK,UAAU,QAAQ,CAE/C,IAAI,IAAM,KAAW,EAGjB,EAAS,KAAK,EAAQ,QAAQ,CAAC,CAKvC,IAAM,EAAW,CAAE,GAAG,KAAK,UAAU,QAAQ,CAAE,CAAC,IAAK,IAAa,CAC9D,KAAM,EAAQ,KACd,UAAW,EAAQ,UACnB,OAAQ,KAAK,gBAAgB,IAAI,EAAQ,KAAK,CACjD,EAAE,CAKH,OAHA,KAAK,KAAK,KAAK,2BAA4B,EAAQ,OAAQ,YAAa,EAAS,OAAQ,aAC/E,EAAS,OAAQ,WAAW,CAE/B,CACH,UACA,WACA,WACH,CAQL,oBAA2B,EAC3B,CACI,KAAK,KAAK,MAAM,kCAAkC,CAGlD,KAAK,UAAU,OAAO,CACtB,KAAK,SAAS,OAAO,CACrB,KAAK,UAAU,OAAO,CACtB,KAAK,gBAAgB,OAAO,CAG5B,IAAI,IAAM,KAAU,EAAO,QAEvB,KAAK,eAAe,EAAO,CAI/B,IAAI,IAAM,KAAe,EAAO,SAE5B,KAAK,gBAAgB,EAAY,KAAM,EAAY,UAAU,CAG1D,EAAY,QAEX,KAAK,gBAAgB,EAAY,KAAK,CAK9C,IAAI,IAAM,KAAc,EAAO,SAE3B,GACA,CACI,KAAK,gBAAgB,EAAW,OAE7B,EACP,CACO,aAAe,OAEd,KAAK,KAAK,MAAM,wCAAyC,EAAW,OAAQ,KAAM,EAAI,UAAW,CAK7G,KAAK,KAAK,KAAK,2BAA4B,EAAO,QAAQ,OAAQ,YACxD,EAAO,SAAS,OAAQ,aAAc,EAAO,SAAS,OAAQ,WAAW,CAMvF,MAAM,WACN,CAII,GAHA,KAAK,KAAK,KAAK,8BAA8B,CAG1C,KAAK,cACR,CACI,GAAM,CAAE,UAAW,KAAK,cACxB,KAAK,iBAAiB,CACtB,EAAW,MAAM,iDAAiD,CAAC,CAIvE,AAGI,KAAK,qBADL,KAAK,mBAAmB,CACC,MAI7B,KAAK,UAAU,OAAO,CACtB,KAAK,gBAAgB,OAAO,CAG5B,KAAK,SAAS,OAAO,CAGrB,KAAK,UAAU,OAAO,CACtB,KAAK,gBAAgB,OAAO,CAG5B,KAAK,sBAAwB,KAE7B,KAAK,KAAK,KAAK,wCAAwC,GCzjC/D,SAAgB,GAChB,CACI,OAAO,OAAO,OAAW,KAAsB,OAAO,WAAa,OAGvE,SAAgB,GAChB,CACI,OAAO,GAAW,EACX,CAAC,CAAC,OAAO,UAAU,IEZ9B,IAAI,IDyCS,GAAb,KACA,CACI,QACA,UACA,eACA,cACA,cACA,KACA,iBACA,oBAGA,gBAAsD,EAAE,CAGxD,gBAA0B,EAG1B,QAAkB,GAElB,QAAiB,GAIjB,YACI,EACA,EACA,EACA,EACA,EACA,EAEJ,CACI,KAAK,QAAU,EACf,KAAK,UAAY,EACjB,KAAK,eAAiB,EACtB,KAAK,cAAgB,EACrB,KAAK,cAAgB,EACrB,KAAK,KAAO,GAAQ,UAAU,cAAc,EAAI,IAAI,EAAW,cAAc,CAG7E,KAAK,iBAAmB,KAAK,YAAY,KAAK,KAAK,CACnD,KAAK,oBAAsB,KAAK,eAAe,KAAK,KAAK,CAEtD,GAAW,EAGV,OAAO,iBAAiB,SAAU,KAAK,oBAAoB,CAInE,IAAI,cACJ,CACI,OAAO,KAAK,cAAc,cAAc,OAAS,KAOrD,IAAI,UACJ,CACI,OAAO,KAAK,QA6BhB,sBAA6B,EAC7B,CACI,IAAM,EAAK,KAAK,kBAMhB,OALA,KAAK,gBAAgB,KAAK,CAAE,KAAI,WAAU,CAAC,CAE3C,KAAK,KAAK,MAAM,kCAAmC,EAAI,GAAG,KAI1D,CACI,IAAM,EAAQ,KAAK,gBAAgB,UAAW,GAAO,EAAG,KAAO,EAAG,CAC/D,IAAU,KAET,KAAK,gBAAgB,OAAO,EAAO,EAAE,CACrC,KAAK,KAAK,MAAM,oCAAqC,EAAI,GAAG,GAOxE,aACA,CAEI,IAAM,EADc,KAAK,QAAQ,cAAc,CACf,IAMhC,GAHA,KAAK,cAAc,cAAc,CAG9B,CAAC,KAAK,QACT,CAEI,KAAK,eAAe,aAAa,EAAU,CAG3C,IAAI,GAAM,CAAE,cAAc,KAAK,gBAE3B,GACA,CACI,EAAS,EAAU,OAEhB,EACP,CACI,KAAK,KAAK,MAAM,2BAA4B,EAAI,EAMzD,KAAK,cAEJ,KAAK,aAAa,QAAQ,CAIlC,gBACA,CACO,KAAK,SAEJ,KAAK,QAAQ,QAAQ,CAQ7B,MAAM,OACN,CAEI,KAAK,QAAQ,cAAc,KAAK,iBAAiB,CAEjD,KAAK,QAAU,GAEf,KAAK,KAAK,KAAK,gDAAgD,CAGnE,MAAM,MACN,CACI,KAAK,QAAU,GACf,KAAK,QAAQ,gBAAgB,CAE7B,KAAK,KAAK,KAAK,gDAAgD,CAYnE,MAAM,EACN,CACI,GAAG,KAAK,QACR,CACI,KAAK,KAAK,MAAM,yBAAyB,CACzC,OAGJ,KAAK,QAAU,GACf,KAAK,KAAK,KAAK,cAAe,EAAS,YAAa,EAAQ,GAAK,GAAG,CAEpE,KAAK,UAAU,QAAQ,CACnB,KAAM,cACN,QAAS,CAAE,SAAQ,CACtB,CAAC,CAWN,OAAO,EACP,CACI,GAAG,CAAC,KAAK,QACT,CACI,KAAK,KAAK,MAAM,qBAAqB,CACrC,OAGJ,KAAK,QAAU,GACf,KAAK,KAAK,KAAK,eAAgB,EAAS,YAAa,EAAQ,GAAK,GAAG,CAErE,KAAK,UAAU,QAAQ,CACnB,KAAM,eACN,QAAS,CAAE,SAAQ,CACtB,CAAC,CAON,MAAM,WACN,CACI,KAAK,KAAK,KAAK,2BAA2B,CAGvC,KAAK,SAEJ,MAAM,KAAK,MAAM,CAIlB,GAAW,EAEV,OAAO,oBAAoB,SAAU,KAAK,oBAAoB,CAIlE,KAAK,gBAAkB,EAAE,CAEzB,KAAK,KAAK,KAAK,qCAAqC,GCpSpD,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,SAAS,GAAG,CAAC,UAAU,EAAE,CAAC,SAAgB,GAAO,EAAE,CAAC,IAAK,GAAG,IAAI,EAAE,GAAG,EAAE,EAAE,OAAO,UAAU,CAAC,GAAG,CAAC,GAAS,IAAN,IAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,GAAG,EAAE,IAAI,KAAK,QAAQ,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE,MCkBtO,IAAa,EAAa,GAAO,GAAG,CCsEd,EAAtB,KACA,CAII,OAA4D,KAM5D,MAAM,EACN,CACI,GAAG,CAAC,KAAK,OAEL,MAAU,MAAM,uCAAuC,CAG3D,EAAM,SAAW,KAAK,OAAO,GAE5B,KAAK,OAAO,SAA8D,QAAQ,EAAM,CAU7F,kBAAkB,EAAiB,EACnC,CACI,GAAG,CAAC,KAAK,OAEL,MAAU,MAAM,uCAAuC,CAG3D,KAAK,MAAM,CACP,KAAM,uBACN,QAAS,CACL,SAAU,KAAK,OAAO,GACtB,WAAY,KAAK,OAAO,KACxB,MAAO,GAAS,KAAK,OAAO,MAC5B,UACH,CACJ,CAAC,CAON,WAAW,EACX,CACI,KAAK,OAAS,IA2DT,EAAb,KACA,CAEI,GAEA,KAGA,KAOA,KAGA,SAA0C,EAAE,CAG5C,OAA6C,KAE7C,MAGA,UAA0C,EAAE,CAE5C,SAEA,YAA0C,KAC1C,eAAkD,KAClD,MAA8B,IAAI,IAClC,cAA6D,IAAI,IAUjE,YACI,EACA,EACA,EACA,EACA,EAEJ,CACI,KAAK,GAAK,GAAY,CACtB,KAAK,KAAO,EACZ,KAAK,KAAO,EACZ,KAAK,MAAQ,EACb,KAAK,SAAW,EAGhB,IAAI,IAAM,KAAgB,EAEtB,KAAK,eAAe,IAAI,EAAe,CAW/C,IAAI,MACJ,CACI,OAAO,KAAK,MAOhB,OAAO,EACP,CACI,OAAO,KAAK,MAAM,IAAI,EAAI,CAO9B,QAAQ,EACR,CACI,KAAK,MAAM,IAAI,EAAI,CAOvB,WAAW,EACX,CACI,OAAO,KAAK,MAAM,OAAO,EAAI,CAWjC,aAAa,EACb,CACQ,QAAK,KAKT,OAAO,KAAK,KAAK,YACZ,GAAU,EAAM,OAAS,EAC1B,GACH,CAAC,GAON,SAAS,EACT,CACI,GAAG,CAAC,KAAK,KAEL,OAGJ,IAAI,EAAsC,KAAK,KAE/C,IAAI,IAAM,KAAQ,EAAK,MAAM,IAAI,CAE7B,EAAU,GAAS,YACd,GAAU,EAAM,OAAS,EAC1B,GACH,CAAC,GAGN,OAAO,EAUX,cAAc,EAAsB,EACpC,CACK,KAA6B,KAAO,EACrC,KAAK,YAAc,EAGnB,IAAI,IAAM,KAAY,KAAK,UAEvB,EAAS,iBAAiB,EAAM,EAAW,CAI/C,IAAI,IAAM,KAAS,KAAK,SAEjB,EAAM,OAEJ,EAAM,KAA8B,OAAS,GAU1D,iBACA,CAEI,KAAK,aAAa,CAGlB,IAAI,IAAM,KAAS,KAAK,SAEjB,EAAM,OAEJ,EAAM,KAA8B,OAAS,MAKtD,IAAI,IAAM,KAAY,KAAK,UAEvB,EAAS,kBAAkB,CAG9B,KAA6B,KAAO,IAAA,GACrC,KAAK,YAAc,KASvB,UAAU,EACV,CACI,IAAM,EAAW,KAAK,aAAa,UAAU,cAAc,cAAc,SACtE,GAEC,EAAS,gBAAgB,EAAW,KAAK,CASjD,YAAY,EACZ,CACI,IAAM,EAAW,KAAK,aAAa,UAAU,cAAc,cAAc,SACtE,IAEI,EAEC,EAAS,kBAAkB,EAAW,KAAK,CAI3C,EAAS,qBAAqB,KAAK,EAc/C,UAAU,EACV,CACI,KAAK,SAAS,KAAK,EAAM,CACxB,EAA8B,OAAS,KAGrC,KAAK,MAAQ,EAAM,OAEjB,EAAM,KAA8B,OAAS,KAAK,MAS3D,aAAa,EACb,CACI,IAAM,EAAQ,KAAK,SAAS,QAAQ,EAAM,CACvC,IAAU,KAET,KAAK,SAAS,OAAO,EAAO,EAAE,CAC7B,EAA8B,OAAS,KAGrC,EAAM,OAEJ,EAAM,KAA8B,OAAS,OAe1D,kBAAkB,EAClB,CACI,KAAK,eAAiB,EAW1B,MAAM,KAAK,EAAiB,EAAe,EAC3C,CACI,GAAG,IAAW,KAAK,GAEf,OAAO,KAAK,MAAM,EAAM,EAAS,KAAK,GAAG,CAG7C,GAAG,CAAC,KAAK,eAEL,MAAU,MAAM,kDAAkD,CAGtE,OAAO,KAAK,eAAe,KAAK,EAAQ,EAAM,EAAS,KAAK,GAAG,CAWnE,MAAM,QAAW,EAAiB,EAAe,EACjD,CACI,GAAG,IAAW,KAAK,GAEf,OAAO,KAAK,SAAY,EAAM,EAAS,KAAK,GAAG,CAGnD,GAAG,CAAC,KAAK,eAEL,MAAU,MAAM,kDAAkD,CAGtE,OAAO,KAAK,eAAe,QAAW,EAAQ,EAAM,EAAS,KAAK,GAAG,CAUzE,MAAM,MAAM,EAAe,EAAoB,EAC/C,CACI,MAAM,KAAK,cAAc,CAAE,OAAM,SAAU,KAAK,GAAI,WAAmB,UAAkB,CAAC,CAU9F,MAAM,SAAY,EAAe,EAAoB,EACrD,CACI,IAAM,EAAoB,CAAE,OAAM,SAAU,KAAK,GAAI,WAAmB,UAAkB,CACtF,EAEJ,IAAI,IAAM,KAAY,KAAK,UAEvB,GACA,CAEI,IAAM,EAAS,MAAM,EAAS,iBAAiB,EAAO,KAAK,MAAM,CACjE,GAAG,IAAW,IAAA,GAEV,MAAO,CAAE,QAAS,GAAM,MAAO,EAAa,OAG7C,EACP,CAEI,EAAY,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,CAIpE,MAAO,CACH,QAAS,GACT,MAAO,GAAa,2BAA4B,EAAM,eAAgB,KAAK,MAAQ,KAAK,GAAI,GAC/F,CAaL,OAAO,EAAuB,EAC9B,CAEI,KAAK,MAAQ,CAAE,GAAG,EAAc,CAGhC,KAAK,MAAM,OAAO,CAClB,IAAI,IAAM,KAAO,EAEb,KAAK,MAAM,IAAI,EAAI,CAIpB,KAAK,MAEJ,KAAK,iBAAiB,CAI1B,IAAI,IAAM,KAAY,KAAK,UAEvB,EAAS,UAAU,KAAK,MAAM,CAIlC,IAAI,IAAM,KAAS,KAAK,SAEpB,EAAM,OAAO,EAAE,CAAE,EAAE,CAAC,CAIvB,KAA6B,OAAS,KAO3C,MAAM,cAAc,EACpB,CACI,IAAI,IAAM,KAAY,KAAK,UAIvB,GADe,MAAM,EAAS,aAAa,EAAO,KAAK,MAAM,CAIzD,MASZ,QAAQ,EACR,CACI,IAAI,IAAM,KAAY,KAAK,UAEvB,EAAS,SAAS,EAAI,KAAK,MAAM,CAIrC,IAAI,IAAM,KAAS,KAAK,SAEpB,EAAM,QAAQ,EAAG,CAOzB,MAAM,UACN,CAEI,IAAI,IAAM,IAAS,CAAE,GAAG,KAAK,SAAU,CAGnC,MAAM,EAAM,UAAU,CAG1B,KAAK,SAAS,OAAS,EAEvB,IAAI,IAAM,KAAY,KAAK,UAGvB,MAAM,EAAS,WAAW,CAI9B,IAAI,IAAM,KAAgB,KAAK,cAAc,QAAQ,CAEjD,EAAa,aAAa,CAG9B,KAAK,UAAY,EAAE,CACnB,KAAK,cAAc,OAAO,CAO9B,kBAA0B,EAC1B,CACI,OAAO,KAAK,UAAU,UAAW,GAAO,EAAG,cAAgB,EAAc,CAO7E,YAAY,EACZ,CACI,OAAO,KAAK,kBAAkB,EAAc,GAAK,GAOrD,YAA0C,EAC1C,CACI,OAAO,KAAK,UAAU,KAAM,GAAO,EAAG,cAAgB,EAAc,CAkBxE,eAAe,EAA+B,EAC9C,CAEI,GAAG,KAAK,YAAY,EAAS,YAA6C,CAEtE,MAAU,MAAM,YAAa,EAAS,YAAY,KAAM,sCAAsC,CAIlG,IAAI,IAAM,KAAS,EAAS,mBAC5B,CAEI,IAAM,EAAuB,KAAK,cAAc,IAAI,EAAM,CAC1D,GAAG,EAEC,EAAqB,YAGzB,CAII,IAAM,EADM,KAAK,SACO,UAAU,EAAO,KAAK,cAAc,KAAK,KAAK,CAAC,CACvE,KAAK,cAAc,IAAI,EAAO,CAAE,MAAO,EAAG,cAAa,CAAC,EAKhE,GAAG,CAAC,EAGA,KAAK,UAAU,KAAK,EAAS,KAGjC,CAKI,GAH2B,CAAE,EAAQ,OAAQ,EAAQ,MAAO,EAAQ,GAAI,CACnE,OAAQ,GAAQ,IAAQ,IAAA,GAAU,CAEjB,OAAS,EAE3B,MAAU,MAAM,2DAA2D,CAG/E,GAAG,EAAQ,OACX,CACI,IAAM,EAAQ,KAAK,kBAAkB,EAAQ,OAAO,CACpD,GAAG,IAAU,GAET,MAAU,MAAM,uCAAwC,EAAQ,OAAO,KAAM,QAAQ,CAEzF,KAAK,UAAU,OAAO,EAAO,EAAG,EAAS,SAErC,EAAQ,MAChB,CACI,IAAM,EAAQ,KAAK,kBAAkB,EAAQ,MAAM,CACnD,GAAG,IAAU,GAET,MAAU,MAAM,uCAAwC,EAAQ,MAAM,KAAM,QAAQ,CAExF,KAAK,UAAU,OAAO,EAAQ,EAAG,EAAG,EAAS,SAEzC,EAAQ,KAAO,IAAA,GACvB,CAEI,IAAM,EAAe,KAAK,IAAI,EAAG,KAAK,IAAI,EAAQ,GAAI,KAAK,UAAU,OAAO,CAAC,CAC7E,KAAK,UAAU,OAAO,EAAc,EAAG,EAAS,MAKhD,KAAK,UAAU,KAAK,EAAS,CAIrC,EAAS,WAAW,KAAK,CAGtB,KAAK,MAAQ,KAAK,aAEjB,EAAS,iBAAiB,KAAK,KAAM,KAAK,YAAY,CAQ9D,eAAe,EACf,CACI,IAAM,EAAQ,KAAK,kBAAkB,EAAc,CACnD,GAAG,IAAU,GAET,MAAO,GAGX,IAAM,EAAW,KAAK,UAAU,GAG7B,KAAK,MAEJ,EAAS,kBAAkB,CAI/B,IAAI,IAAM,KAAS,EAAS,mBAC5B,CACI,IAAM,EAAe,KAAK,cAAc,IAAI,EAAM,CAC/C,IAEC,EAAa,QACV,EAAa,OAAS,IAErB,EAAa,aAAa,CAC1B,KAAK,cAAc,OAAO,EAAM,GAS5C,OAHA,KAAK,UAAU,OAAO,EAAO,EAAE,CAC/B,EAAS,WAAW,KAAK,CAElB,KC1zBF,GAAb,KACA,CAEI,SAGA,SAA6C,IAAI,IAGjD,kBAAgE,IAAI,IAGpE,YAA0C,KAG1C,eAGA,KAOA,gBAAyD,IAAI,IAG7D,gBAAyD,IAAI,IAG7D,eAAwD,IAAI,IAG5D,gBAA2D,IAAI,IAG/D,OAA6C,IAAI,IAQjD,YAAY,EAAyB,EAAqC,EAC1E,CACI,KAAK,SAAW,EAChB,KAAK,eAAiB,EACtB,KAAK,KAAO,GAAQ,UAAU,gBAAgB,EAAI,IAAI,EAAW,gBAAgB,CACjF,KAAK,KAAK,KAAK,4BAA4B,CAW/C,eAAe,EACf,CACI,KAAK,YAAc,EAGvB,IAAY,SACZ,CACI,GAAG,CAAC,KAAK,YAEL,MAAU,MAAM,kEAAkE,CAEtF,OAAO,KAAK,YAUhB,gBAA2B,EAA+B,EAC1D,CACI,IAAI,EAAM,EAAI,IAAI,EAAI,CAMtB,OALI,IAEA,EAAM,IAAI,IACV,EAAI,IAAI,EAAK,EAAI,EAEd,EAMX,aAAqB,EACrB,CAEI,KAAK,gBAAgB,KAAK,gBAAiB,EAAO,KAAK,CAAC,IAAI,EAAO,CAGhE,EAAO,MAEN,KAAK,gBAAgB,KAAK,gBAAiB,EAAO,KAAK,CAAC,IAAI,EAAO,CAIvE,IAAI,IAAM,KAAO,EAAO,KAEpB,KAAK,gBAAgB,KAAK,eAAgB,EAAI,CAAC,IAAI,EAAO,CAI3D,EAAO,MAEN,KAAK,gBAAgB,IAAI,EAAO,KAAM,EAAO,CAOrD,eAAuB,EACvB,CAEI,IAAM,EAAU,KAAK,gBAAgB,IAAI,EAAO,KAAK,CAWrD,GAVG,IAEC,EAAQ,OAAO,EAAO,CACnB,EAAQ,OAAS,GAEhB,KAAK,gBAAgB,OAAO,EAAO,KAAK,EAK7C,EAAO,KACV,CACI,IAAM,EAAU,KAAK,gBAAgB,IAAI,EAAO,KAAK,CAClD,IAEC,EAAQ,OAAO,EAAO,CACnB,EAAQ,OAAS,GAEhB,KAAK,gBAAgB,OAAO,EAAO,KAAK,EAMpD,IAAI,IAAM,KAAO,EAAO,KACxB,CACI,IAAM,EAAS,KAAK,eAAe,IAAI,EAAI,CACxC,IAEC,EAAO,OAAO,EAAO,CAClB,EAAO,OAAS,GAEf,KAAK,eAAe,OAAO,EAAI,EAMxC,EAAO,MAEN,KAAK,gBAAgB,OAAO,EAAO,KAAK,CAYhD,oBAA4B,EAC5B,CACI,IAAM,EAAW,EAAS,QAC1B,GAAG,CAAC,EAEA,OAAO,EAGX,IAAM,EAAU,KAAK,kBAAkB,IAAI,EAAS,CACpD,GAAG,CAAC,EACJ,CACI,IAAM,EAAW,kBAAmB,EAAU,8CAE9C,MADA,KAAK,KAAK,MAAM,EAAS,CACf,MAAM,EAAS,CAI7B,IAAI,EAqCJ,OApCG,EAAQ,MAAQ,EAAS,QAExB,EAAa,CAAE,GAAG,IAAI,IAAI,CAAE,GAAI,EAAQ,MAAQ,EAAE,CAAG,GAAI,EAAS,MAAQ,EAAE,CAAG,CAAC,CAAE,EAI9C,CAEpC,KAAM,EAAS,KACf,KAAM,EAAS,MAAQ,EAAQ,KAC/B,KAAM,EAAS,MAAQ,EAAQ,KAG/B,aAAc,CAAE,GAAG,EAAQ,aAAc,GAAG,EAAS,aAAc,CAGnE,UAAW,EAAS,WAAa,EAAQ,UACzC,QAAS,EAAS,SAAW,EAAQ,QAGrC,KAAM,EAGN,SAAU,EAAS,UAAY,EAAQ,SACvC,SAAU,EAAS,UAAY,EAAQ,SAGvC,SAAU,EAAS,UAAY,EAAQ,SAGvC,eAAgB,EAAS,gBAAkB,EAAQ,eACnD,SAAU,EAAS,UAAY,EAAQ,SACvC,gBAAiB,EAAS,iBAAmB,EAAQ,gBACrD,UAAW,EAAS,WAAa,EAAQ,UAC5C,CAcL,sBAA8B,EAAyB,EACvD,CAmBI,MAXA,EANG,EAAe,OAAS,EAAU,MAMlC,EAAe,OAAS,UAAY,EAAU,OAAS,WAGlD,EAAU,WAAa,IAAA,IAAa,EAAe,WAAa,EAAU,UACtE,EAAU,WAAa,IAAA,IAAa,EAAe,WAAa,EAAU,WAe1F,uBAA+B,EAC/B,CACQ,KAAU,QAKd,IAAI,IAAM,KAAU,EAAU,QAE1B,GACA,CAEI,IAAM,EAAiB,KAAK,eAAe,UAAU,EAAO,KAAK,CAE7D,EAMK,KAAK,sBAAsB,EAAgB,EAAO,CAUvD,KAAK,KAAK,MACN,WAAY,EAAO,KAAM,qEAC5B,CAVD,KAAK,KAAK,KACN,WAAY,EAAO,KAAM,uDACX,EAAU,KAAM,cAAe,KAAK,UAAU,EAAO,CAAE,eACpD,KAAK,UAAU,EAAe,GAClD,EATD,KAAK,KAAK,MAAM,uBAAwB,EAAO,KAAM,sBAAuB,EAAU,KAAM,GAAG,CAC/F,KAAK,eAAe,eAAe,EAAO,QAiB3C,EACP,CACI,KAAK,KAAK,MAAM,8BAA+B,EAAO,KAAM,KAClD,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,GAAI,EAK1E,aAAa,EACb,CACI,KAAK,KAAK,MAAM,YAAa,KAAK,SAAS,KAAM,oBAAqB,IAAM,CAC5E,IAAI,IAAM,KAAU,KAAK,SAAS,QAAQ,CAGlC,EAAO,QAEP,EAAO,QAAQ,EAAG,CAc9B,yBAAyB,EACzB,CACI,KAAK,KAAK,MAAM,kCAAmC,EAAU,OAAQ,CAGrE,IAAM,EAAW,EAAU,QACrB,KAAK,oBAAoB,EAAU,CACnC,EAGN,KAAK,uBAAuB,EAAS,CAGrC,KAAK,kBAAkB,IAAI,EAAS,KAAM,EAAS,CAGhD,EAAS,UAAY,EAAS,SAAW,GAInC,KAAK,QAAQ,EAAS,KAAM,EAAS,SAAS,CAQ3D,cAAc,EACd,CACI,OAAO,KAAK,kBAAkB,IAAI,EAAK,CAS3C,MAAM,aAIF,EACA,EAAuC,EAAE,CAE7C,CACI,KAAK,KAAK,MAAM,4BAA6B,IAAQ,CACrD,IAAM,EAAY,KAAK,kBAAkB,IAAI,EAAK,CAClD,GAAG,CAAC,EACJ,CACI,IAAM,EAAW,eAAgB,EAAM,qBAEvC,MADA,KAAK,KAAK,MAAM,EAAS,CACf,MAAM,EAAS,CAI7B,GAAG,EAAU,SACb,CACI,IAAM,EAAO,KAAK,OAAO,IAAI,EAAK,CAClC,GAAG,GAAQ,EAAK,OAAS,EACzB,CACI,KAAK,KAAK,MAAM,wCAAyC,IAAQ,CACjE,IAAM,EAAS,EAAK,KAAK,CAkBzB,GAfA,EAAO,OACH,EAAU,aACV,EAAU,MAAQ,EAAE,CACvB,CAGD,EAAO,kBAAkB,KAAK,CAG3B,EAAQ,eAEP,EAAO,MAAQ,CAAE,GAAG,EAAO,MAAO,GAAG,EAAQ,aAAc,EAI5D,EAAQ,KAEP,IAAI,IAAM,KAAO,EAAQ,KAErB,EAAO,QAAQ,EAAI,CAe3B,OAVG,EAAQ,MAEP,EAAO,cAAc,EAAQ,KAAM,KAAK,QAAQ,CAIpD,KAAK,SAAS,IAAI,EAAO,GAAI,EAAO,CACpC,KAAK,aAAa,EAAO,CAEzB,KAAK,KAAK,MAAM,mBAAoB,EAAO,GAAI,YAAY,CACpD,GAIf,KAAK,KAAK,MAAM,gCAAiC,EAAU,WAAW,QAAU,EAAG,YAAY,CAC/F,IAAI,EAAc,CAAE,GAAG,EAAU,aAAc,GAAG,EAAQ,aAAc,CAGxE,GAAG,EAAU,eACb,CACI,KAAK,KAAK,MAAM,gDAAiD,IAAQ,CACzE,IAAM,EAAS,MAAM,EAAU,eAAe,EAAY,CAEvD,IAAW,IAAA,KAEV,EAAc,GAKtB,IAAM,EAAa,EAAQ,MAAQ,EAAU,KAGvC,EAAS,IAAI,EACf,EAAU,KACV,KAAK,SACL,EACA,EAAU,UACV,EACH,CAYD,GATA,EAAO,kBAAkB,KAAK,CAG3B,EAAQ,MAEP,EAAO,cAAc,EAAQ,KAAM,KAAK,QAAQ,CAIjD,EAAU,KAET,IAAI,IAAM,KAAO,EAAU,KAEvB,EAAO,QAAQ,EAAI,CAK3B,GAAG,EAAQ,KAEP,IAAI,IAAM,KAAO,EAAQ,KAErB,EAAO,QAAQ,EAAI,CAU3B,GALA,KAAK,SAAS,IAAI,EAAO,GAAI,EAAO,CACpC,KAAK,aAAa,EAAO,CACzB,KAAK,KAAK,MAAM,2BAA4B,EAAO,KAAO,EAAa,WAAY,EAAY,GAAK,KAAM,CAGvG,EAAU,SACb,CACI,KAAK,KAAK,MAAM,0CAA2C,IAAQ,CACnE,IAAM,EAAS,MAAM,EAAU,SAAS,EAAO,MAAM,CAElD,IAAW,IAAA,KAEV,EAAO,MAAQ,GAKvB,GAAG,EAAU,SAET,IAAI,IAAM,KAAY,EAAU,SAChC,CACI,IAAM,EAA8C,CAAE,GAAG,EAAS,aAAc,CAG7E,EAAS,WAER,EAAkB,UAAY,EAAS,UAExC,EAAS,WAER,EAAkB,UAAY,EAAS,UAI3C,IAAM,EAAc,MAAM,KAAK,aAAa,EAAS,KAAM,CACvD,KAAM,EAAS,KACf,aAAc,EACjB,CAAC,CAEF,EAAO,UAAU,EAAY,CAIrC,OAAO,EAQX,MAAM,cAAc,EACpB,CACI,KAAK,KAAK,MAAM,sBAAuB,IAAY,CACnD,IAAM,EAAS,KAAK,SAAS,IAAI,EAAS,CAC1C,GAAG,EACH,CAEI,IAAM,EAAY,KAAK,kBAAkB,IAAI,EAAO,KAAK,CAGzD,GAAG,GAAW,SACd,CACI,KAAK,KAAK,MAAM,kBAAmB,EAAU,wBAAwB,CAGrE,IAAI,IAAM,IAAS,CAAE,GAAG,EAAO,SAAU,CAGrC,MAAM,KAAK,cAAc,EAAM,GAAG,CAInC,EAAO,QAEN,EAAO,OAAO,aAAa,EAAO,CAItC,KAAK,eAAe,EAAO,CAG3B,EAAO,OAAO,EAAU,aAAc,EAAU,MAAQ,EAAE,CAAC,CAG3D,IAAI,EAAO,KAAK,OAAO,IAAI,EAAO,KAAK,CACnC,IAEA,EAAO,EAAE,CACT,KAAK,OAAO,IAAI,EAAO,KAAM,EAAK,EAEtC,EAAK,KAAK,EAAO,CAGjB,KAAK,SAAS,OAAO,EAAS,CAE9B,KAAK,KAAK,MAAM,UAAW,EAAU,SAAS,CAC9C,OAMJ,IAAI,IAAM,IAAS,CAAE,GAAG,EAAO,SAAU,CAGrC,MAAM,KAAK,cAAc,EAAM,GAAG,CAUtC,GANG,EAAO,QAEN,EAAO,OAAO,aAAa,EAAO,CAInC,GAAW,gBACd,CACI,KAAK,KAAK,MAAM,4CAA6C,IAAY,CACzE,IAAM,EAAS,MAAM,EAAU,gBAAgB,EAAO,MAAM,CAEzD,IAAW,IAAA,KAEV,EAAO,MAAQ,GAKvB,KAAK,eAAe,EAAO,CAG3B,MAAM,EAAO,UAAU,CAGpB,GAAW,YAEV,KAAK,KAAK,MAAM,sCAAuC,IAAY,CACnE,MAAM,EAAU,UAAU,EAAO,MAAM,EAI3C,KAAK,SAAS,OAAO,EAAS,CAE9B,KAAK,KAAK,MAAM,UAAW,EAAU,YAAY,MAIjD,KAAK,KAAK,KAAK,6CAA8C,IAAY,CAQjF,UAAU,EACV,CAEI,OADA,KAAK,KAAK,MAAM,mBAAoB,IAAY,CACzC,KAAK,SAAS,IAAI,EAAS,EAAI,KAO1C,UAAU,EACV,CACI,KAAK,KAAK,MAAM,2BAA4B,EAAO,GAAI,UAAW,EAAO,KAAM,GAAG,CAClF,EAAO,kBAAkB,KAAK,CAC9B,KAAK,SAAS,IAAI,EAAO,GAAI,EAAO,CACpC,KAAK,aAAa,EAAO,CAO7B,aAAa,EACb,CACI,KAAK,KAAK,MAAM,oBAAqB,IAAY,CACjD,IAAM,EAAS,KAAK,SAAS,IAAI,EAAS,CACvC,GAEC,KAAK,eAAe,EAAO,CAC3B,KAAK,SAAS,OAAO,EAAS,CAC9B,KAAK,KAAK,MAAM,UAAW,EAAU,UAAU,EAI/C,KAAK,KAAK,KAAK,4CAA6C,IAAY,CAchF,SAAS,EAAmB,EAC5B,CACI,IAAM,EAAS,KAAK,SAAS,IAAI,EAAS,CACpC,EAAQ,KAAK,SAAS,IAAI,EAAQ,CAgBxC,OAdI,EAMA,GAMJ,EAAO,UAAU,EAAM,CACvB,KAAK,KAAK,MAAM,eAAgB,EAAS,aAAc,IAAY,CAC5D,KANH,KAAK,KAAK,KAAK,2BAA4B,EAAS,aAAa,CAC1D,KAPP,KAAK,KAAK,KAAK,4BAA6B,EAAU,aAAa,CAC5D,IAoBf,YAAY,EAAmB,EAC/B,CACI,IAAM,EAAS,KAAK,SAAS,IAAI,EAAS,CACpC,EAAQ,KAAK,SAAS,IAAI,EAAQ,CAgBxC,OAdI,EAMA,GAMJ,EAAO,aAAa,EAAM,CAC1B,KAAK,KAAK,MAAM,iBAAkB,EAAS,eAAgB,IAAY,CAChE,KANH,KAAK,KAAK,KAAK,8BAA+B,EAAS,aAAa,CAC7D,KAPP,KAAK,KAAK,KAAK,+BAAgC,EAAU,aAAa,CAC/D,IAuBf,UAAU,EACV,CACI,IAAM,EAAU,KAAK,gBAAgB,IAAI,EAAK,CAC9C,GAAG,GAAW,EAAQ,KAAO,EAEzB,OAAO,EAAQ,QAAQ,CAAC,MAAM,CAAC,MASvC,kBAAkB,EAClB,CACI,IAAM,EAAU,KAAK,gBAAgB,IAAI,EAAK,CAC9C,OAAO,EAAU,MAAM,KAAK,EAAQ,CAAG,EAAE,CAO7C,UAAU,EACV,CACI,IAAM,EAAU,KAAK,gBAAgB,IAAI,EAAK,CAC9C,OAAO,EAAU,MAAM,KAAK,EAAQ,CAAG,EAAE,CAO7C,SAAS,EACT,CACI,IAAM,EAAS,KAAK,eAAe,IAAI,EAAI,CAC3C,OAAO,EAAS,MAAM,KAAK,EAAO,CAAG,EAAE,CAQ3C,UAAU,EAAiB,EAAuB,MAClD,CACI,GAAG,EAAK,SAAW,EAEf,MAAO,EAAE,CAGb,GAAG,IAAS,MACZ,CAEI,IAAM,EAAS,IAAI,IACnB,IAAI,IAAM,KAAO,EACjB,CACI,IAAM,EAAS,KAAK,eAAe,IAAI,EAAI,CAC3C,GAAG,EAEC,IAAI,IAAM,KAAU,EAEhB,EAAO,IAAI,EAAO,CAI9B,OAAO,MAAM,KAAK,EAAO,KAG7B,CAGI,IAAM,EAAO,EACR,IAAK,GAAQ,KAAK,eAAe,IAAI,EAAI,CAAC,CAC1C,OAAQ,GAAiC,IAAQ,IAAA,GAAU,CAGhE,GAAG,EAAK,SAAW,EAAK,OAEpB,MAAO,EAAE,CAIb,EAAK,MAAM,EAAM,IAAS,EAAK,KAAO,EAAK,KAAK,CAChD,GAAM,CAAE,EAAU,GAAG,GAAS,EAE9B,OAAO,MAAM,KAAK,EAAS,CAAC,OAAQ,GAAW,EAAK,MAAO,GAAQ,EAAI,IAAI,EAAO,CAAC,CAAC,EAQ5F,gBACA,CACI,OAAO,KAAK,SAAS,QAAQ,CAMjC,IAAI,aACJ,CACI,OAAO,KAAK,SAAS,KAezB,MAAM,KAAK,EAAiB,EAAe,EAAoB,EAC/D,CACI,IAAM,EAAS,KAAK,SAAS,IAAI,EAAO,EAAI,KAAK,UAAU,EAAO,CAClE,GAAG,CAAC,EACJ,CACI,KAAK,KAAK,KAAK,iBAAkB,EAAQ,aAAa,CACtD,OAGJ,MAAM,EAAO,MAAM,EAAM,EAAS,EAAS,CAY/C,MAAM,QACF,EACA,EACA,EACA,EAEJ,CACI,IAAM,EAAS,KAAK,SAAS,IAAI,EAAO,EAAI,KAAK,UAAU,EAAO,CAMlE,OALI,EAKG,EAAO,SAAY,EAAM,EAAS,EAAS,CAHvC,CAAE,QAAS,GAAO,MAAO,WAAY,EAAQ,aAAc,CAgB1E,OAAO,EAA8B,EACrC,CACI,IAAM,EAAe,OAAO,GAAW,SAAW,KAAK,SAAS,IAAI,EAAO,CAAG,EAiB9E,OAhBI,EAMD,EAAa,OAAO,EAAI,CAEhB,IAIX,EAAa,QAAQ,EAAI,CACzB,KAAK,gBAAgB,KAAK,eAAgB,EAAI,CAAC,IAAI,EAAa,CAEhE,KAAK,KAAK,MAAM,cAAe,EAAK,cAAe,EAAa,KAAM,CAC/D,KAdH,KAAK,KAAK,KAAK,2BAA2B,CACnC,IAsBf,UAAU,EAA8B,EACxC,CACI,IAAM,EAAe,OAAO,GAAW,SAAW,KAAK,SAAS,IAAI,EAAO,CAAG,EAC9E,GAAG,CAAC,EAGA,OADA,KAAK,KAAK,KAAK,8BAA8B,CACtC,GAGX,GAAG,CAAC,EAAa,WAAW,EAAI,CAE5B,MAAO,GAIX,IAAM,EAAS,KAAK,eAAe,IAAI,EAAI,CAW3C,OAVG,IAEC,EAAO,OAAO,EAAa,CACxB,EAAO,OAAS,GAEf,KAAK,eAAe,OAAO,EAAI,EAIvC,KAAK,KAAK,MAAM,gBAAiB,EAAK,gBAAiB,EAAa,KAAM,CACnE,GAWX,UAAU,EACV,CACI,OAAO,KAAK,gBAAgB,IAAI,EAAK,CAWzC,aAAa,EAA8B,EAC3C,CACI,IAAM,EAAe,OAAO,GAAW,SAAW,KAAK,SAAS,IAAI,EAAO,CAAG,EAyB9E,OAxBI,EAOD,KAAK,gBAAgB,IAAI,EAAK,EAE7B,KAAK,KAAK,KAAK,oDAAoD,CAC5D,KAIR,EAAa,MAEZ,KAAK,gBAAgB,OAAO,EAAa,KAAK,CAIlD,EAAa,cAAc,EAAM,KAAK,QAAQ,CAC9C,KAAK,gBAAgB,IAAI,EAAM,EAAa,CAE5C,KAAK,KAAK,MAAM,mBAAoB,EAAa,GAAI,YAAa,EAAK,KAAM,GAAG,CACzE,KAtBH,KAAK,KAAK,KAAK,iCAAiC,CACzC,IA8Bf,eAAe,EACf,CACI,IAAM,EAAe,OAAO,GAAW,SAAW,KAAK,SAAS,IAAI,EAAO,CAAG,EAC9E,GAAG,CAAC,EAGA,OADA,KAAK,KAAK,KAAK,mCAAmC,CAC3C,GAGX,GAAG,CAAC,EAAa,KAEb,MAAO,GAIX,KAAK,gBAAgB,OAAO,EAAa,KAAK,CAG9C,IAAM,EAAW,EAAa,KAAK,KAInC,OAHA,EAAa,iBAAiB,CAE9B,KAAK,KAAK,MAAM,mBAAoB,EAAa,GAAI,cAAe,EAAU,GAAG,CAC1E,GAYX,MAAM,QAAQ,EAAe,EAC7B,CACI,KAAK,KAAK,MAAM,wBAAyB,EAAM,SAAU,EAAO,WAAW,CAE3E,IAAI,IAAI,EAAI,EAAG,EAAI,EAAO,IAC1B,CAEI,IAAM,EAAS,MAAM,KAAK,aAAa,EAAK,CAE5C,MAAM,KAAK,cAAc,EAAO,GAAG,EAQ3C,MAAM,UAAU,EAChB,CACI,IAAM,EAAO,KAAK,OAAO,IAAI,EAAK,CAC9B,KAKJ,MAAK,KAAK,MAAM,sBAAuB,EAAM,KAAM,EAAK,OAAQ,YAAY,CAE5E,IAAI,IAAM,KAAU,EAGhB,MAAM,EAAO,UAAU,CAG3B,EAAK,OAAS,EACd,KAAK,OAAO,OAAO,EAAK,EAW5B,MAAM,WACN,CACI,KAAK,KAAK,KAAK,mCAAoC,KAAK,SAAS,KAAM,WAAW,CAGlF,IAAM,EAAY,CAAE,GAAG,KAAK,SAAS,MAAM,CAAE,CAG7C,IAAI,IAAM,KAAY,EAElB,GACA,CAEI,MAAM,KAAK,cAAc,EAAS,OAE/B,EACP,CACI,KAAK,KAAK,MAAM,2BAA4B,EAAU,IAAK,IAAO,CAK1E,IAAI,IAAM,IAAQ,CAAE,GAAG,KAAK,OAAO,MAAM,CAAE,CAEvC,GACA,CAEI,MAAM,KAAK,UAAU,EAAK,OAEvB,EACP,CACI,KAAK,KAAK,MAAM,4BAA6B,EAAM,KAAM,IAAO,CAKxE,KAAK,kBAAkB,OAAO,CAG9B,KAAK,gBAAgB,OAAO,CAC5B,KAAK,gBAAgB,OAAO,CAC5B,KAAK,eAAe,OAAO,CAC3B,KAAK,gBAAgB,OAAO,CAE5B,KAAK,KAAK,KAAK,uCAAuC,GC/pCxD,GAA6C,CAC/C,MAAO,EAAA,6BAA6B,qBACpC,SAAU,EAAA,6BAA6B,qBACvC,WAAY,EAAA,6BAA6B,qBACzC,aAAc,EAAA,6BAA6B,qBAC3C,KAAM,EAAA,6BAA6B,iBACtC,CAUD,SAAgB,GAA4B,EAAe,EAC3D,CAEI,GAAG,CAAC,EAAM,aAEN,OAID,EAAO,YAEN,QAAQ,KAAK,wFAAwF,CAIzG,IAAM,EAAW,IAAI,EAAA,yBAAyB,eAAgB,GAAM,EAAO,EAAM,QAAQ,CAyGzF,GAtGG,EAAO,QAEN,EAAS,aAAe,GAErB,EAAO,MAAM,SAAW,IAAA,KAEvB,EAAS,YAAc,EAAO,MAAM,QAErC,EAAO,MAAM,YAAc,IAAA,KAE1B,EAAS,eAAiB,EAAO,MAAM,WAExC,EAAO,MAAM,QAAU,IAAA,KAEtB,EAAS,WAAa,EAAO,MAAM,OAEpC,EAAO,MAAM,SAAW,IAAA,KAEvB,EAAS,YAAc,EAAO,MAAM,SAKzC,EAAO,UAEN,EAAS,uBAAyB,GAClC,EAAS,gBAAgB,mBAAqB,GAE3C,EAAO,QAAQ,WAEd,EAAS,gBAAgB,gBACnB,GAAkB,EAAO,QAAQ,WAAa,EAAA,6BAA6B,uBAKtF,EAAO,QAEN,EAAS,aAAe,GAErB,EAAO,MAAM,YAAc,IAAA,KAE1B,EAAS,MAAM,UAAY,EAAO,MAAM,WAEzC,EAAO,MAAM,WAAa,IAAA,KAEzB,EAAS,MAAM,SAAW,EAAO,MAAM,WAK5C,EAAO,WAEN,EAAS,uBAAyB,GAClC,EAAS,gBAAgB,gBAAkB,GAExC,EAAO,SAAS,SAAW,IAAA,KAE1B,EAAS,gBAAgB,eAAiB,EAAO,SAAS,QAE3D,EAAO,SAAS,UAAY,IAAA,KAE3B,EAAS,gBAAgB,gBAAkB,EAAO,SAAS,SAE5D,EAAO,SAAS,QAEf,EAAS,gBAAgB,cAAgB,IAAI,EAAA,OACzC,EAAO,SAAS,MAAM,EACtB,EAAO,SAAS,MAAM,EACtB,EAAO,SAAS,MAAM,EACtB,EACH,GAKN,EAAO,UAEN,EAAS,eAAiB,GAEvB,EAAO,QAAQ,OAAS,IAAA,KAEvB,EAAS,QAAQ,WAAa,EAAO,QAAQ,MAE9C,EAAO,QAAQ,QAAU,IAAA,KAExB,EAAS,QAAQ,YAAc,EAAO,QAAQ,QAKnD,EAAO,sBAEN,EAAS,2BAA6B,GAEnC,EAAO,oBAAoB,SAAW,IAAA,KAErC,EAAS,oBAAoB,iBAAmB,EAAO,oBAAoB,SAKhF,EAAO,KACV,CACI,IAAM,EAAO,IAAI,EAAA,uBAAuB,YAAa,EAAO,CAAE,UAAW,GAAK,UAAW,EAAK,CAAC,CAE5F,EAAO,KAAK,SAAW,IAAA,KAEtB,EAAK,OAAS,EAAO,KAAK,QAE3B,EAAO,KAAK,UAAY,IAAA,KAEvB,EAAK,QAAU,EAAO,KAAK,SAE5B,EAAO,KAAK,gBAAkB,IAAA,KAE7B,EAAK,cAAgB,EAAO,KAAK,iECjI7C,eAAsB,GAA8B,EAAe,EACnE,CACI,IAAM,EAAS,EAAM,aACrB,GAAG,CAAC,EAEA,OAGJ,IAAM,EAAS,EAAM,WAAW,CAC1B,EAAK,IAAI,EAAA,WAAW,EAAM,CAG1B,EAAe,EAAG,eAAe,0BAA0B,gBAAiB,CAC9E,KAAM,CAAE,MAAO,IAAK,OAAQ,IAAK,CACjC,iBAAkB,GAClB,QAAS,CAAE,QAAS,CAAE,EAAA,UAAU,mBAAoB,CAAE,QAAS,EAAG,CACrE,CAAC,CAGI,EAAY,IAAI,EAAA,2BAA2B,aAAc,EAAG,CAClE,EAAU,cAAgB,EAC1B,EAAG,QAAQ,EAAU,CAGrB,IAAM,EAAa,IAAI,EAAA,6BAA6B,cAAe,EAAI,EAAM,CAC7E,EAAW,cAAgB,EAAU,cACrC,EAAW,aAAe,EAAU,mBACpC,EAAW,OAAS,EACpB,EAAW,WAAa,CAAE,OAAQ,EAAM,OAAQ,gBAAiB,EAAM,gBAAiB,CACxF,EAAW,qBAAuB,GAClC,EAAG,QAAQ,EAAW,CAGtB,IAAI,EAA2C,EAAW,cAG1D,GAAG,EAAO,KACV,CACI,IAAM,EAAU,IAAI,EAAA,+BAA+B,gBAAiB,EAAI,EAAM,CAC9E,EAAG,QAAQ,EAAQ,CAEnB,IAAM,EAAW,IAAI,EAAA,qCAAqC,YAAa,EAAI,GAAK,EAAI,CACpF,EAAS,cAAgB,EACzB,EAAS,aAAe,EAAQ,yBAChC,EAAS,cAAgB,EAAQ,0BACjC,EAAS,OAAS,EAClB,EAAG,QAAQ,EAAS,CAEjB,EAAO,KAAK,SAAW,IAAA,KAAa,EAAS,KAAK,OAAS,EAAO,KAAK,QACvE,EAAO,KAAK,UAAY,IAAA,KAAa,EAAS,KAAK,QAAU,EAAO,KAAK,SACzE,EAAO,KAAK,gBAAkB,IAAA,KAAa,EAAS,KAAK,cAAgB,EAAO,KAAK,eAExF,EAAiB,EAAS,cAI9B,GAAG,EAAO,MACV,CACI,IAAM,EAAY,IAAI,EAAA,oBAClB,aACA,EACA,EAAO,MAAM,QAAU,GACvB,EAAO,MAAM,QAAU,IACvB,EAAO,MAAM,WAAa,GAC1B,GACA,EAAO,MAAM,OAAS,GACzB,CACD,EAAU,cAAgB,EAC1B,EAAG,QAAQ,EAAU,CAErB,EAAiB,EAAU,cAI/B,GAAG,EAAO,MACV,CACI,IAAM,EAAY,IAAI,EAAA,oBAAoB,aAAc,EAAG,CAC3D,EAAU,cAAgB,EAC1B,EAAG,QAAQ,EAAU,CAElB,EAAO,MAAM,YAAc,IAAA,KAAa,EAAU,YAAY,UAAY,EAAO,MAAM,WACvF,EAAO,MAAM,WAAa,IAAA,KAAa,EAAU,YAAY,SAAW,EAAO,MAAM,UAExF,EAAiB,EAAU,cAI/B,GAAG,EAAO,QACV,CACI,IAAM,EAAc,IAAI,EAAA,sBAAsB,eAAgB,EAAG,CACjE,EAAY,cAAgB,EAC5B,EAAG,QAAQ,EAAY,CAEpB,EAAO,QAAQ,OAAS,IAAA,KAAa,EAAY,YAAY,WAAa,EAAO,QAAQ,MACzF,EAAO,QAAQ,QAAU,IAAA,KAAa,EAAY,YAAY,YAAc,EAAO,QAAQ,OAE9F,EAAiB,EAAY,cAIjC,GAAG,EAAO,oBACV,CACI,IAAM,EAAY,IAAI,EAAA,kCAAkC,eAAgB,EAAG,CAC3E,EAAU,cAAgB,EAC1B,EAAG,QAAQ,EAAU,CAElB,EAAO,oBAAoB,SAAW,IAAA,KAErC,EAAU,YAAY,iBAAmB,EAAO,oBAAoB,QAGxE,EAAiB,EAAU,cAI/B,GAAG,EAAO,SAAW,EAAO,SAC5B,CACI,IAAM,EAAU,IAAI,EAAA,8BAA8B,eAAgB,EAAG,CAgBrE,GAfA,EAAQ,cAAgB,EACxB,EAAG,QAAQ,EAAQ,CAEhB,EAAO,UAEN,EAAQ,YAAY,mBAAqB,GAEtC,EAAO,QAAQ,WAEd,EAAQ,YAAY,gBACd,EAAkB,EAAO,QAAQ,WAChC,EAAA,6BAA6B,uBAIzC,EAAO,WAEN,EAAQ,YAAY,gBAAkB,GAEnC,EAAO,SAAS,SAAW,IAAA,KAAa,EAAQ,YAAY,eAAiB,EAAO,SAAS,QAC7F,EAAO,SAAS,UAAY,IAAA,KAE3B,EAAQ,YAAY,gBAAkB,EAAO,SAAS,SAEvD,EAAO,SAAS,OACnB,CACI,IAAM,EAAK,EAAO,SAAS,MAC3B,EAAQ,YAAY,cAAc,IAAI,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,EAAE,CAIlE,EAAiB,EAAQ,cAI1B,EAAO,YAEN,QAAQ,KACJ,6KAEH,CAIL,IAAM,EAAW,IAAI,EAAA,oCAAoC,cAAe,EAAG,CAC3E,EAAS,cAAgB,EACzB,EAAG,QAAQ,EAAS,CAGpB,EAAM,uBAAyB,EAC/B,EAAM,WAAa,EAGnB,EAAO,mBAAmB,IAAI,SAC9B,CACI,MAAM,EAAG,YAAY,EACvB,CAGF,MAAM,EAAG,YAAY,kBA3LnB,EAA6C,CAC/C,MAAO,EAAA,6BAA6B,qBACpC,SAAU,EAAA,6BAA6B,qBACvC,WAAY,EAAA,6BAA6B,qBACzC,aAAc,EAAA,6BAA6B,qBAC3C,KAAM,EAAA,6BAA6B,iBACtC,ICxBD,eAAsB,EAAoB,EAAe,EACzD,CACI,GAAG,EAAO,WAAa,aACvB,CACI,GAAM,CAAE,iCAAkC,MAAA,QAAA,SAAA,CAAA,UAAA,IAAA,CAAA,IAAA,CAC1C,MAAM,EAA8B,EAAO,EAAO,MAIlD,GAA4B,EAAO,EAAO,CCXlD,SAAgB,EAAa,EAC7B,CACI,MAAO,CAAE,EAAG,EAAO,EAAG,EAAG,EAAO,EAAG,EAAG,EAAO,EAAG,CAMpD,SAAgB,EAAU,EAC1B,CACI,OAAO,IAAI,EAAA,QAAQ,EAAI,EAAG,EAAI,EAAG,EAAI,EAAE,CAM3C,SAAgB,EAAa,EAC7B,CACI,MAAO,CAAE,EAAG,EAAK,EAAG,EAAG,EAAK,EAAG,EAAG,EAAK,EAAG,EAAG,EAAK,EAAG,CAMzD,SAAgB,GAAa,EAC7B,CACI,OAAO,IAAI,EAAA,WAAW,EAAK,EAAG,EAAK,EAAG,EAAK,EAAG,EAAK,EAAE,CAUzD,SAAS,EAAmB,EAC5B,CACI,OAAO,EAAA,OAAO,QACV,EAAK,QACL,EAAK,oBAAsB,EAAA,WAAW,gBAAgB,EAAK,SAAS,CACpE,EAAK,SACR,CAeL,SAAgB,EAAsB,EAKtC,CACI,EAAK,mBAAmB,GAAK,CAI7B,IAAI,EAAY,EAAmB,EAAK,CAEpC,EAAW,EAAK,OACpB,KAAM,GACN,CACI,EAAS,mBAAmB,GAAK,CACjC,IAAM,EAAgB,EAAmB,EAAS,CAE/C,EAAc,aAAa,EAAI,IAE9B,EAAY,EAAU,SAAS,EAAc,EAGjD,EAAW,EAAS,OAGxB,IAAM,EAAW,IAAI,EAAA,QACf,EAAW,IAAI,EAAA,WACf,EAAU,IAAI,EAAA,QAGpB,OAFA,EAAU,UAAU,EAAS,EAAU,EAAS,CAEzC,CAAE,WAAU,WAAU,UAAS,CC9D1C,IAAsB,EAAtB,KACA,CACI,KACA,KACA,SACA,OAAkC,KAClC,gBAA0D,KAC1D,SAA0C,KAY1C,YAAY,EAAsB,EAClC,CACI,KAAK,KAAO,EAAO,KACnB,KAAK,SAAW,EAChB,KAAK,KAAO,EAAQ,QAAQ,UAAU,SAAU,EAAO,OAAQ,EACxD,IAAI,EAAW,SAAU,EAAO,OAAS,OAAO,CAO3D,IAAI,OACJ,CACI,OAAO,KAAK,OAGhB,IAAI,UACJ,CACI,OAAO,KAAK,SAAW,KAG3B,IAAI,YACJ,CACI,OAAO,KAAK,SAAS,WAGzB,IAAI,kBACJ,CACI,OAAO,KAAK,SAAS,iBAazB,cAAwB,EAAmB,EAC3C,CACI,KAAK,WAAW,SAAS,QAAQ,CAC7B,KAAM,iBACN,QAAS,CACL,UAAW,KAAK,KAChB,WACA,UACH,CACJ,CAAC,CAWN,MAAM,MACN,CACI,GAAG,KAAK,OAGJ,OADA,KAAK,KAAK,KAAK,SAAU,KAAK,KAAM,oBAAoB,CACjD,KAAK,OAGhB,KAAK,KAAK,KAAK,kBAAmB,KAAK,OAAQ,CAC/C,KAAK,KAAK,KAAK,SAAU,KAAK,KAAM,OAAO,CAE3C,GACA,CAmBI,OAlBA,KAAK,cAAc,EAAG,yBAAyB,CAG/C,KAAK,OAAS,MAAM,KAAK,YAAY,CAErC,KAAK,cAAc,IAAK,4BAA4B,CACpD,KAAK,WAAW,SAAS,QAAQ,CAC7B,KAAM,iBACN,QAAS,CACL,UAAW,KAAK,KAChB,QAAS,4BACT,MAAO,KACV,CACJ,CAAC,CAEF,KAAK,KAAK,QAAQ,SAAU,KAAK,KAAM,OAAO,CAC9C,KAAK,KAAK,KAAK,SAAU,KAAK,KAAM,sBAAsB,CAEnD,KAAK,aAET,EACP,CAUI,MATA,KAAK,KAAK,MAAM,wBAAyB,KAAK,KAAM,GAAI,EAAM,CAC9D,KAAK,WAAW,SAAS,QAAQ,CAC7B,KAAM,cACN,QAAS,CACL,UAAW,KAAK,KAChB,QAAS,uBACT,QACH,CACJ,CAAC,CACI,GAmCd,MAAM,UACN,CACI,GAAG,KAAK,OACR,CACI,KAAK,KAAK,KAAK,oBAAqB,KAAK,OAAQ,CAIjD,IAAM,EAAgB,KAAK,SAAS,YAAY,UAAU,cAC1D,GAAG,GAAe,mBAEV,IAAM,KAAU,EAAc,gBAAgB,CAE3C,EAAO,MAAQ,EAAO,KAAK,UAAU,GAAK,KAAK,QAE9C,EAAc,eAAe,EAAO,CAKhD,AAGI,KAAK,YADL,KAAK,SAAS,SAAS,CACP,MAGpB,AAGI,KAAK,mBADL,KAAK,gBAAgB,SAAS,CACP,MAG3B,KAAK,OAAO,SAAS,CACrB,KAAK,OAAS,QClNb,EAAb,KACA,CACI,OACA,QAAkB,IAAI,IAEtB,YAAY,EAAe,EAC3B,CAGI,GAFA,KAAK,OAAS,EAEX,EAEC,IAAI,GAAM,CAAE,EAAM,KAAY,OAAO,QAAQ,EAAQ,CAEjD,KAAK,OAAO,EAAM,EAAO,CASrC,IAAI,EACJ,CACI,OAAO,KAAK,QAAQ,IAAI,EAAK,EAAE,MAGnC,OAAO,EAAe,EACtB,CACI,IAAM,EAAW,KAAK,QAAQ,IAAI,EAAK,CACvC,GAAG,EAEC,OAAO,EAAS,MAGpB,IAAM,EAAQ,IAAI,EAAA,sBAAsB,gBAAiB,IAAS,KAAK,OAAO,CAW9E,OATG,GAAQ,QAEP,EAAM,aAAe,IAAI,EAAA,OAAO,EAAO,MAAM,EAAG,EAAO,MAAM,EAAG,EAAO,MAAM,EAAE,EAGhF,GAAQ,YAAc,IAAA,KAAa,EAAM,iBAAmB,EAAO,WACnE,GAAQ,oBAAsB,IAAA,KAAa,EAAM,kBAAoB,EAAO,mBAE/E,KAAK,QAAQ,IAAI,EAAM,CAAE,QAAO,SAAU,IAAI,IAAO,CAAC,CAC/C,EAGX,gBAAgB,EAAoB,EACpC,CACI,IAAM,EAAQ,KAAK,QAAQ,IAAI,EAAU,CACrC,IAKJ,EAAM,SAAS,IAAI,EAAO,CAC1B,KAAK,kBAAkB,EAAM,EAGjC,kBAAkB,EAAoB,EACtC,CACI,IAAM,EAAQ,KAAK,QAAQ,IAAI,EAAU,CACrC,IAKJ,EAAM,SAAS,OAAO,EAAO,CAC7B,KAAK,kBAAkB,EAAM,EAGjC,qBAAqB,EACrB,CACI,IAAI,IAAM,KAAS,KAAK,QAAQ,QAAQ,CAEjC,EAAM,SAAS,OAAO,EAAO,EAE5B,KAAK,kBAAkB,EAAM,CAKzC,MAAM,EACN,CACI,IAAM,EAAQ,KAAK,QAAQ,IAAI,EAAU,CACrC,IAKJ,EAAM,SAAS,OAAO,CACtB,EAAM,MAAM,gBAAgB,EAGhC,SACA,CACI,IAAI,IAAM,KAAS,KAAK,QAAQ,QAAQ,CAEpC,EAAM,SAAS,OAAO,CACtB,EAAM,MAAM,SAAS,CAGzB,KAAK,QAAQ,OAAO,CAOxB,kBAA0B,EAC1B,CACI,EAAM,MAAM,gBAAgB,CAE5B,IAAI,IAAM,KAAU,EAAM,SAEtB,GAAG,EAAO,KACV,CACI,IAAM,EAAS,EAAO,KAAK,gBAAgB,CACxC,EAAO,OAAS,GAEf,EAAM,MAAM,aAAa,EAAO,IClEvC,EAAb,cAA+B,CAC/B,CAEI,QAGA,aAA4C,EAAE,CAG9C,aAA4C,EAAE,CAG9C,iBAA4C,EAAE,CAG9C,aAAyB,IAAI,IAG7B,+BAAyC,IAAI,IAY7C,YAAY,EAAsB,EAClC,CACI,MAAM,EAAQ,EAAQ,CACtB,KAAK,QAAU,EAOnB,IAAI,QACJ,CACI,OAAO,KAAK,QAGhB,IAAI,iBACJ,CACI,OAAO,KAAK,iBAUhB,MAAgB,YAChB,CACI,GAAM,CAAE,eAAgB,KAAK,WAAW,QAClC,CAAE,gBAAiB,KAAK,WAAW,SAGtC,KAAK,QAAQ,SAAW,KAAK,QAAQ,QAAQ,OAAS,IAErD,KAAK,cAAc,EAAG,uBAAuB,CAC7C,MAAM,EAAa,QAAQ,KAAK,QAAQ,QAAQ,EAIpD,IAAM,EAAQ,EAAY,aAAa,CAkCvC,GAjCA,KAAK,OAAS,EAGX,KAAK,QAAQ,UAEZ,KAAK,cAAc,EAAG,0BAA0B,CAChD,MAAM,KAAK,eAAe,EAAM,EAIjC,KAAK,QAAQ,QAEZ,KAAK,cAAc,GAAI,wBAAwB,CAC/C,MAAM,KAAK,eAAe,EAAM,EAIjC,KAAK,QAAQ,cAEZ,KAAK,cAAc,GAAI,4BAA4B,CACnD,KAAK,oBAAoB,EAAM,EAInC,KAAK,gBAAgB,EAAM,CAG3B,KAAK,eAAe,EAAM,CAG1B,KAAK,iBAAiB,EAAM,CAGzB,KAAK,QAAQ,eAChB,CACI,IAAM,EAAW,KAAK,QAAQ,eAAe,UAAY,WACzD,KAAK,KAAK,KAAK,iCAAkC,EAAU,WAAW,CACtE,MAAM,EAAoB,EAAO,KAAK,QAAQ,eAAe,CAyBjE,MArBA,MAAK,SAAW,IAAI,EAAe,EAAO,KAAK,QAAQ,SAAS,CAGhE,KAAK,cAAc,GAAI,iCAAiC,CACxD,MAAM,KAAK,uBAAuB,EAAM,CAGxC,KAAK,cAAc,GAAI,uBAAuB,CAC9C,MAAM,KAAK,qBAAqB,CAGhC,KAAK,cAAc,GAAI,0BAA0B,CACjD,MAAM,KAAK,qBAAqB,CAG7B,KAAK,QAAQ,SAEZ,KAAK,cAAc,GAAI,oBAAoB,CAC3C,MAAM,KAAK,qBAAqB,EAG7B,EAMX,MAAc,eAAe,EAC7B,CACI,IAAM,EAAY,KAAK,eAAe,CAClC,KAQJ,CAHA,KAAK,KAAK,MAAM,uBAAwB,IAAa,CAGlD,KAAK,gBAAgB,GAEpB,EAAM,qBAAuB,GAC7B,KAAK,KAAK,MAAM,8CAA8C,EAGlE,GACA,CACI,MAAM,KAAK,WAAW,QAAQ,YAAY,aAAa,EAAE,CAAE,EAAW,EAAM,CAC5E,KAAK,KAAK,MAAM,iCAAiC,OAE9C,EACP,CAEI,MADA,KAAK,KAAK,MAAM,8BAA+B,IAAc,EAAM,CAC7D,IAOd,eACA,CACQ,QAAK,QAAQ,MAKjB,OAAO,OAAO,KAAK,QAAQ,OAAU,SAC/B,KAAK,QAAQ,MACb,KAAK,QAAQ,MAAM,KAM7B,gBACA,CAMI,MALG,CAAC,KAAK,QAAQ,OAAS,OAAO,KAAK,QAAQ,OAAU,SAE7C,GAGJ,KAAK,QAAQ,MAAM,cAAgB,GAM9C,oBAA4B,EAC5B,CACI,IAAM,EAAM,KAAK,QAAQ,YACrB,KAUJ,IAPG,EAAI,MAEH,EAAM,mBAAqB,KAAK,0BAA0B,EAAI,IAAK,EAAI,eAAiB,IAAK,EAAM,CACnG,KAAK,KAAK,MAAM,iBAAkB,EAAI,MAAO,EAI9C,EAAI,OACP,CACI,IAAM,EAAM,KAAK,kBAAkB,EAAI,OAAO,CAE9C,GAAG,IAAQ,QAAU,IAAQ,OAC7B,CAEI,GAAG,CAAC,EAAI,IACR,CACI,IAAM,EAAM,EAAI,eAAiB,IACjC,EAAM,mBAAqB,KAAK,0BAA0B,EAAI,OAAQ,EAAK,EAAM,CAGlF,EAAM,oBAEL,EAAM,oBAAoB,EAAM,mBAAoB,GAAM,EAAI,YAAc,IAAM,EAAG,GAAM,KAInG,CAEI,IAAM,EAAO,EAAI,YAAc,IACzB,EAAS,EAAA,YAAY,aAAa,SAAU,CAAE,SAAU,EAAM,SAAU,GAAI,CAAE,EAAM,CAEpF,EAAM,IAAI,EAAA,iBAAiB,aAAc,EAAM,CACrD,EAAI,gBAAkB,GACtB,EAAI,gBAAkB,GACtB,EAAI,gBAAkB,IAAI,EAAA,QAAQ,EAAI,OAAQ,EAAM,CACpD,EAAO,SAAW,EAClB,EAAO,iBAAmB,GAG9B,KAAK,KAAK,MAAM,oBAAqB,EAAI,SAAU,CAIpD,EAAI,WAAa,IAAA,IAAa,EAAM,qBAElC,EAAM,mBAAsC,UAAY,EAAI,WAOrE,0BAAkC,EAAe,EAAqB,EACtE,CAQI,OAPY,KAAK,kBAAkB,EAAK,GAE7B,OAEA,EAAA,YAAY,0BAA0B,EAAM,EAAM,CAGtD,IAAI,EAAA,eAAe,EAAM,EAAO,EAAW,CAGtD,kBAA0B,EAC1B,CACI,IAAM,EAAW,EAAK,YAAY,IAAI,CACtC,OAAO,GAAY,EAAI,EAAK,MAAM,EAAS,CAAC,aAAa,CAAG,GAShE,gBAAwB,EACxB,CACI,IAAM,EAAgB,KAAK,QAAQ,QAC7B,EAAS,KAAK,WAAW,OAE/B,GAAG,CAAC,EACJ,CAEO,EAAM,QAAQ,OAAS,IAEtB,EAAM,aAAe,EAAM,QAAQ,GAEhC,GAEC,EAAM,aAAa,cAAc,EAAQ,GAAK,CAGlD,KAAK,KAAK,MAAM,+BAAgC,EAAM,aAAa,KAAM,GAAG,EAGhF,OAGJ,IAAI,EAA+B,KAEnC,IAAI,GAAM,CAAE,EAAM,KAAY,OAAO,QAAQ,EAAc,CAC3D,CACI,IAAM,EAAW,EAAM,gBAAgB,EAAK,CAE5C,GAAG,EAEC,KAAK,mBAAmB,EAAU,EAAO,CACzC,KAAK,KAAK,MAAM,iCAAkC,EAAM,GAAG,EAExD,CAAC,GAAgB,EAAO,UAEvB,EAAe,WAGf,EAAO,KACf,CACI,IAAM,EAAS,KAAK,cAAc,EAAM,EAAQ,EAAM,CACtD,KAAK,KAAK,MAAM,WAAY,EAAO,KAAM,YAAa,EAAM,GAAG,EAE5D,CAAC,GAAgB,EAAO,UAEvB,EAAe,QAKnB,KAAK,KAAK,KACN,WAAY,EAAM,uDACrB,CAIT,GAAG,EACH,CACI,EAAM,aAAe,EAErB,IAAM,EAAY,EAAc,EAAa,MAC1C,GAAU,GAAW,gBAAkB,IAEtC,EAAa,cAAc,EAAQ,GAAK,EAQpD,cAAsB,EAAe,EAA2B,EAChE,CACI,IAAM,EAAM,EAAO,SAAW,EAAU,EAAO,SAAS,CAAG,EAAA,QAAQ,MAAM,CAErE,EAEJ,OAAQ,EAAO,KAAf,CAEI,IAAK,YACL,CACI,IAAM,EAAS,EAAO,OAAS,EAAU,EAAO,OAAO,CAAG,EAAA,QAAQ,MAAM,CAExE,EAAS,IAAI,EAAA,gBACT,EACA,EAAO,OAAS,KAAK,GAAK,EAC1B,EAAO,MAAQ,KAAK,GAAK,EACzB,EAAO,QAAU,GACjB,EACA,EACH,CACD,MAGJ,IAAK,YAED,EAAS,IAAI,EAAA,gBAAgB,EAAM,EAAK,EAAM,CAC9C,MAGJ,IAAK,aACL,CACI,GAAG,CAAC,EAAO,aAGP,OADA,KAAK,KAAK,KAAK,sBAAuB,EAAM,qCAAqC,CAC1E,IAAI,EAAA,WAAW,EAAM,EAAA,QAAQ,MAAM,CAAE,EAAM,CAGtD,IAAM,EAAM,IAAI,EAAA,iBAAiB,EAAM,EAAO,CAC1C,aAAc,EAAO,aACxB,CAAC,CAEC,EAAO,SAAU,EAAI,OAAS,EAAU,EAAO,OAAO,EACtD,EAAO,MAAQ,IAAA,KAAa,EAAI,IAAM,EAAO,KAC7C,EAAO,QAAU,IAAA,KAAa,EAAI,MAAQ,EAAO,OACjD,EAAO,SAAW,IAAA,KAAa,EAAI,OAAS,EAAO,QACnD,EAAO,kBAAoB,IAAA,KAAa,EAAI,gBAAkB,EAAO,iBAErE,EAAO,YAAc,IAAA,KAAa,EAAI,OAAO,UAAY,EAAO,WAChE,EAAO,YAAc,IAAA,KAAa,EAAI,OAAO,UAAY,EAAO,WAChE,EAAO,WAAa,IAAA,KAAa,EAAI,OAAO,SAAW,EAAO,UAC9D,EAAO,WAAa,IAAA,KAAa,EAAI,OAAO,SAAW,EAAO,UAC9D,EAAO,SAAW,IAAA,KAAa,EAAI,OAAO,OAAS,EAAO,QAC1D,EAAO,SAAW,IAAA,KAAa,EAAI,OAAO,OAAS,EAAO,QAE7D,EAAS,EACT,MAIJ,QAEI,EAAS,IAAI,EAAA,WAAW,EAAM,EAAK,EAAM,CACzC,MAKR,OADA,KAAK,mBAAmB,EAAQ,EAAO,CAChC,EAMX,mBAA2B,EAAiB,EAC5C,CACO,EAAO,MAAQ,IAAA,KAAa,EAAO,IAAM,EAAO,KAChD,EAAO,WAAY,EAAO,SAAW,EAAU,EAAO,SAAS,EAC/D,EAAO,OAAS,IAAA,KAAa,EAAO,KAAO,EAAO,MAClD,EAAO,OAAS,IAAA,KAAa,EAAO,KAAO,EAAO,MAElD,aAAkB,EAAA,aAEd,EAAO,QAAU,IAAA,KAAa,EAAO,MAAQ,EAAO,OACpD,EAAO,WAAY,EAAO,SAAW,EAAU,EAAO,SAAS,GAGnE,aAAkB,EAAA,kBAEd,EAAO,QAAU,EAAO,UAAU,EAAU,EAAO,OAAO,CAAC,CAC3D,EAAO,mBAAqB,IAAA,KAAa,EAAO,iBAAmB,EAAO,kBAC1E,EAAO,mBAAqB,IAAA,KAAa,EAAO,iBAAmB,EAAO,kBAC1E,EAAO,iBAAmB,IAAA,KAAa,EAAO,eAAiB,EAAO,gBACtE,EAAO,iBAAmB,IAAA,KAAa,EAAO,eAAiB,EAAO,gBACtE,EAAO,iBAAmB,IAAA,KAAa,EAAO,eAAiB,EAAO,iBAG1E,aAAkB,EAAA,mBAEd,EAAO,MAAQ,IAAA,KAAa,EAAO,IAAM,EAAO,KAChD,EAAO,QAAU,IAAA,KAAa,EAAO,MAAQ,EAAO,OACpD,EAAO,SAAW,IAAA,KAAa,EAAO,OAAS,EAAO,QACtD,EAAO,SAAU,EAAO,OAAS,EAAU,EAAO,OAAO,EACzD,EAAO,kBAAoB,IAAA,KAAa,EAAO,gBAAkB,EAAO,iBACxE,EAAO,YAAc,IAAA,KAAa,EAAO,OAAO,UAAY,EAAO,WACnE,EAAO,YAAc,IAAA,KAAa,EAAO,OAAO,UAAY,EAAO,WACnE,EAAO,WAAa,IAAA,KAAa,EAAO,OAAO,SAAW,EAAO,UACjE,EAAO,WAAa,IAAA,KAAa,EAAO,OAAO,SAAW,EAAO,WAU5E,eAAuB,EACvB,CACI,IAAM,EAAe,KAAK,QAAQ,OAC9B,KAEJ,IAAI,GAAM,CAAE,EAAM,KAAY,OAAO,QAAQ,EAAa,CAC1D,CACI,IAAM,EAAW,EAAM,eAAe,EAAK,CAExC,GAEC,KAAK,kBAAkB,EAAU,EAAO,CACxC,KAAK,KAAK,MAAM,gCAAiC,EAAM,GAAG,EAEtD,EAAO,MAEX,KAAK,aAAa,EAAM,EAAQ,EAAM,CACtC,KAAK,KAAK,MAAM,WAAY,EAAO,KAAM,WAAY,EAAM,GAAG,EAI9D,KAAK,KAAK,KACN,UAAW,EAAM,uDACpB,EAQb,aAAqB,EAAe,EAA0B,EAC9D,CACI,IAAM,EAAM,EAAO,UAAY,EAAU,EAAO,UAAU,CAAG,IAAI,EAAA,QAAQ,EAAG,GAAI,EAAE,CAC5E,EAAM,EAAO,SAAW,EAAU,EAAO,SAAS,CAAG,EAAA,QAAQ,MAAM,CAEzE,OAAQ,EAAO,KAAf,CAEI,IAAK,cACL,CACI,IAAM,EAAQ,IAAI,EAAA,iBAAiB,EAAM,EAAK,EAAM,CAEjD,EAAO,cAEN,EAAM,YAAc,KAAK,UAAU,EAAO,YAAY,EAG1D,KAAK,kBAAkB,EAAO,EAAO,CACrC,MAGJ,IAAK,cACL,CACI,IAAM,EAAQ,IAAI,EAAA,iBAAiB,EAAM,EAAK,EAAM,CACpD,KAAK,kBAAkB,EAAO,EAAO,CACrC,MAGJ,IAAK,QACL,CACI,IAAM,EAAQ,IAAI,EAAA,WAAW,EAAM,EAAK,EAAM,CAC9C,KAAK,kBAAkB,EAAO,EAAO,CACrC,MAGJ,IAAK,OACL,CACI,IAAM,EAAQ,IAAI,EAAA,UACd,EACA,EACA,EACA,EAAO,OAAS,KAAK,GAAK,EAC1B,EAAO,UAAY,EACnB,EACH,CACD,KAAK,kBAAkB,EAAO,EAAO,CACrC,MAGJ,IAAK,WACL,CACI,IAAM,EAAQ,IAAI,EAAA,cACd,EACA,EACA,EAAO,OAAS,EAChB,EAAO,QAAU,EACjB,EACH,CAKD,GAJA,KAAK,kBAAkB,EAAO,EAAO,CAIlC,EAAO,UACV,CACI,IAAM,EAAQ,IAAI,EAAA,cAAc,GAAI,EAAM,QAAS,EAAM,CACzD,EAAM,SAAW,EAAM,SAAS,OAAO,CACvC,EAAM,SAAW,EAAA,QAAQ,MAAM,CAC/B,EAAM,OAAS,EAGf,EAAM,OAAO,EAAM,SAAS,SAAS,EAAI,CAAC,CAG9C,QAQZ,kBACI,EACA,EAEJ,CACO,EAAO,YAAc,IAAA,KAAa,EAAM,UAAY,EAAO,WAC3D,EAAO,UAAW,EAAM,QAAU,KAAK,UAAU,EAAO,QAAQ,EAChE,EAAO,WAAY,EAAM,SAAW,KAAK,UAAU,EAAO,SAAS,EAO1E,iBAAyB,EACzB,CACI,IAAM,EAAgB,KAAK,QAAQ,WACnC,GAAG,CAAC,GAAe,QAEf,OAIJ,IAAM,EAAiB,EAAM,OAAO,OAC/B,GAAU,EAAA,wBAAwB,iBAAiB,EAAM,CAC7D,CAED,GAAG,EAAe,SAAW,EAC7B,CACI,KAAK,KAAK,MAAM,kDAAkD,CAClE,OAIJ,IAAI,IAAM,KAAS,EAEf,EAAM,YAAY,EAAM,CAI5B,IAAM,EAAY,IAAI,EAAA,wBAAwB,iBAAkB,EAAgB,EAAM,CAGnF,EAAc,kBAAoB,IAAA,KAAa,EAAU,gBAAkB,EAAc,iBACzF,EAAc,gBAAkB,IAAA,KAAa,EAAU,cAAgB,EAAc,eACrF,EAAc,cAAgB,IAAA,KAAa,EAAU,YAAc,EAAc,aACjF,EAAc,WAAa,IAAA,KAAa,EAAU,SAAW,EAAc,UAE9E,KAAK,gBAAkB,EAEvB,KAAK,KAAK,MACN,+BAAgC,EAAe,OAAQ,WACjD,EAAU,gBAAiB,GAAI,EAAU,cAAe,UACxD,EAAU,YAAa,eAChC,CAGD,IAAI,IAAM,KAAY,EAAM,UAErB,aAAoB,EAAA,cAEnB,EAAS,oBAAsB,IAO3C,UAAkB,EAClB,CACI,OAAO,IAAI,EAAA,OAAO,EAAM,EAAG,EAAM,EAAG,EAAM,EAAE,CAMhD,MAAc,eAAe,EAC7B,CACI,IAAM,EAAgB,KAAK,QAAQ,QAG/B,EAAU,IAAI,EAAA,QAAQ,EAAG,MAAO,EAAE,CAEnC,OAAO,GAAkB,UAAY,EAAc,UAElD,EAAU,IAAI,EAAA,QACV,EAAc,QAAQ,EACtB,EAAc,QAAQ,EACtB,EAAc,QAAQ,EACzB,EAIL,IAAM,EAAuB,KAAK,WAAW,oBACvC,IACA,IAAA,GAEN,KAAK,KAAK,MAAM,kCAAmC,EAAQ,UAAU,GAAI,CACzE,MAAM,KAAK,WAAW,QAAQ,YAAY,cAAc,EAAO,EAAS,EAAqB,CAMjG,MAAc,uBAAuB,EACrC,CACI,IAAM,EAAQ,EAAM,eAAe,OAAO,EAAM,OAA0B,CAE1E,IAAI,IAAM,KAAQ,EAClB,CAEI,IAAM,EAAW,KAAK,uBAAuB,EAAK,CAE/C,GAAY,OAAO,KAAK,EAAS,CAAC,OAAS,IAG1C,EAAK,SAAW,CAAE,GAAG,EAAK,SAAU,GAAG,EAAU,CAGjD,MAAM,KAAK,qBAAqB,EAAK,EAI7C,KAAK,KAAK,MACN,aAAc,EAAM,OAAQ,gBAAiB,KAAK,aAAa,OAAQ,oBAC7D,KAAK,aAAa,OAAQ,eACvC,CASL,uBAA+B,EAC/B,CACI,GAAG,CAAC,EAAK,SAEL,OAAO,KAIX,IAAM,EAAa,EAAK,UAAU,MAAM,OASxC,OAPG,EAGQ,CAAE,GAAG,EAAY,CAIrB,EAAK,SAMhB,MAAc,qBAAqB,EACnC,CAMI,GAAG,UAAW,EAAK,SACnB,CACI,GAAM,CAAE,WAAU,WAAU,WAAY,EAAsB,EAAK,CAEnE,KAAK,aAAa,KAAK,CACnB,KAAM,EAAK,SAAS,MACpB,WACA,WACA,UACA,OACH,CAAC,CAIH,WAAY,EAAK,UAEhB,KAAK,aAAa,KAAK,CACnB,KAAM,EAAK,SAAS,OACpB,OACH,CAAC,CAIN,MAAM,KAAK,qBAAqB,EAAK,CAMzC,MAAc,qBAAqB,EACnC,CACI,IAAI,GAAM,CAAE,EAAU,KAAa,KAAK,iBAEpC,GAAG,KAAY,EAAK,SAEhB,GACA,CAEI,MAAM,EAAQ,EAAM,EAAK,SAAS,GAAW,KAAM,KAAK,WAAW,OAEhE,EACP,CACI,KAAK,KAAK,MACN,8BAA+B,EAAU,cAAe,EAAK,KAAM,IACnE,EACH,EASjB,MAAc,qBACd,CACI,IAAI,IAAM,KAAc,KAAK,aAC7B,CACI,IAAM,EAAW,KAAK,QAAQ,SAAS,EAAW,MAE9C,EAOA,MAAM,KAAK,mBAAmB,EAAY,EAAS,CALnD,KAAK,KAAK,KAAK,kCAAmC,EAAW,KAAM,aAAa,EAa5F,MAAc,mBAAmB,EAA6B,EAC9D,CACI,GACA,CACI,IAAM,EAAS,MAAM,KAAK,aAAa,EAAU,EAAW,CAC5D,KAAK,iBAAiB,KAAK,EAAO,CAGlC,EAAW,KAAK,SAAS,CAEzB,KAAK,KAAK,MAAM,mBAAoB,EAAS,OAAQ,oBAAqB,EAAW,KAAM,GAAG,OAE3F,EACP,CACI,KAAK,KAAK,MAAM,0CAA2C,EAAW,KAAM,IAAK,EAAM,EAO/F,MAAc,aAAa,EAA4B,EACvD,CACI,IAAM,EAAe,CACjB,GAAG,EAAS,OACZ,SAAU,EAAa,EAAW,SAAS,CAC3C,SAAU,EAAa,EAAW,SAAS,CAC3C,QAAS,EAAa,EAAW,QAAQ,CAC5C,CAEK,EAAS,MAAM,KAAK,WAAW,SAAS,cAAc,aAAa,EAAS,OAAQ,CACtF,KAAM,EAAS,KACf,KAAM,EAAS,KACf,eACH,CAAC,CAKF,OAFA,MAAM,KAAK,kBAAkB,EAAQ,EAAW,CAEzC,EAOX,MAAc,kBAAkB,EAAqB,EACrD,CACI,GAAG,CAAC,KAAK,OAEL,OAIJ,IAAM,EAAa,KAAK,WAAW,SAAS,cAAc,cAAc,EAAO,KAAK,CACpF,GAAG,CAAC,GAAY,KAEZ,OAGJ,IAAM,EAAa,EAAW,KACxB,EAAQ,KAAK,OAEb,EAAO,IAAI,EAAA,cAAc,UAAW,EAAO,KAAO,EAAM,CAG1D,EAEJ,OAAQ,EAAW,OAAnB,CAEI,IAAK,MACD,EAAS,CAAE,EAAA,YAAY,UAAU,GAAI,EAAO,KAAM,OAAQ,CACtD,KAAM,EAAW,QAAQ,MAAQ,EACjC,MAAO,EAAW,QAAQ,MAC1B,OAAQ,EAAW,QAAQ,OAC3B,MAAO,EAAW,QAAQ,MAC7B,CAAE,EAAM,CAAE,CACX,MAEJ,IAAK,SACD,EAAS,CAAE,EAAA,YAAY,aAAa,GAAI,EAAO,KAAM,OAAQ,CACzD,SAAU,EAAW,QAAQ,UAAY,EACzC,SAAU,EAAW,QAAQ,UAAY,GAC5C,CAAE,EAAM,CAAE,CACX,MAEJ,IAAK,UACD,EAAS,CAAE,EAAA,YAAY,cAAc,GAAI,EAAO,KAAM,OAAQ,CAC1D,OAAQ,EAAW,QAAQ,QAAU,IACrC,OAAQ,EAAW,QAAQ,QAAU,GACxC,CAAE,EAAM,CAAE,CACX,MAEJ,IAAK,WACD,EAAS,CAAE,EAAA,YAAY,eAAe,GAAI,EAAO,KAAM,OAAQ,CAC3D,OAAQ,EAAW,QAAQ,QAAU,EACrC,SAAU,EAAW,QAAQ,UAAY,EAC5C,CAAE,EAAM,CAAE,CACX,MAEJ,QACA,CAEI,IAAM,EAAS,MAAM,KAAK,WAAW,QAAQ,YAAY,aAAa,EAAE,CAAE,EAAW,OAAQ,EAAM,CAEnG,GAAG,EAAO,OAAO,SAAW,EAC5B,CACI,KAAK,KAAK,KAAK,0BAA2B,EAAW,SAAU,CAC/D,EAAK,SAAS,CACd,OAGJ,EAAS,EAAO,OAGhB,IAAI,IAAM,KAAM,EAAO,eAEnB,AAEI,EAAG,SAAS,EAIpB,OAMR,IAAI,IAAM,KAAQ,EAEd,AAEI,EAAK,SAAS,EAOtB,EAAK,SAAS,SAAS,EAAW,SAAS,CAC3C,EAAK,mBAAqB,EAAW,SAAS,OAAO,CACrD,EAAK,QAAQ,SAAS,EAAW,QAAQ,CAGtC,EAAW,QAEP,OAAO,EAAW,OAAU,SAE3B,EAAK,QAAQ,OAAO,EAAW,MAAM,CAIrC,EAAK,QAAQ,IAAI,EAAW,MAAM,EAAG,EAAW,MAAM,EAAG,EAAW,MAAM,EAAE,EAKpF,IAAM,EAAY,EAAW,SAC7B,GAAG,GAAW,MACd,CACI,IAAM,EAAQ,EAAU,MAExB,GAAG,EAAU,OAAS,MACtB,CACI,IAAM,EAAW,IAAI,EAAA,YAAY,GAAI,EAAO,KAAM,WAAY,EAAM,CACpE,EAAS,YAAc,KAAK,UAAU,EAAM,CAEzC,EAAU,WAET,EAAS,cAAgB,KAAK,UAAU,EAAU,SAAS,EAG/D,EAAS,SAAW,EAAU,UAAY,EAC1C,EAAS,UAAY,EAAU,WAAa,EAE5C,IAAI,IAAM,KAAQ,EAEd,EAAK,SAAW,MAIxB,CACI,IAAM,EAAW,IAAI,EAAA,iBAAiB,GAAI,EAAO,KAAM,WAAY,EAAM,CACzE,EAAS,aAAe,KAAK,UAAU,EAAM,CAE1C,EAAU,WAET,EAAS,cAAgB,KAAK,UAAU,EAAU,SAAS,EAG/D,IAAI,IAAM,KAAQ,EAEd,EAAK,SAAW,GAQ5B,IAAM,EAAc,EAAK,eAAe,GAAM,CACzC,OAAQ,GAAmC,aAAiB,EAAA,cAAc,CAE/E,IAAI,IAAM,KAAY,EACtB,CACI,IAAM,EAAW,KAAK,uBAAuB,EAAS,CAEnD,GAAY,OAAO,KAAK,EAAS,CAAC,OAAS,IAE1C,EAAS,SAAW,CAChB,GAAG,EAAS,SACZ,GAAG,EACN,CAGD,MAAM,KAAK,qBAAqB,EAAS,EAKjD,KAAK,WAAW,SAAS,cAAc,aAAa,EAAQ,EAAK,CAMrE,MAAc,qBACd,CACI,IAAI,IAAM,KAAc,KAAK,aAGzB,MAAM,KAAK,mBAAmB,EAAW,CAOjD,MAAc,mBAAmB,EACjC,CACI,IAAM,EAAY,KAAK,QAAQ,WAAW,EAAW,MAErD,GACA,CACI,IAAM,EAAe,CACjB,GAAG,GAAW,OACd,SAAU,EAAa,EAAW,KAAK,SAAS,CACnD,CAEK,EAAS,MAAM,KAAK,WAAW,SAAS,cAAc,aAAa,EAAW,KAAM,CACtF,KAAM,GAAW,MAAQ,EAAW,KAAK,KACzC,KAAM,GAAW,KACjB,eACA,KAAM,EAAW,KACpB,CAAC,CACF,KAAK,iBAAiB,KAAK,EAAO,CAElC,KAAK,KAAK,MAAM,mBAAoB,EAAW,KAAM,cAAe,EAAW,KAAK,KAAM,GAAG,OAE1F,EACP,CACI,KAAK,KAAK,MAAM,qCAAsC,EAAW,KAAK,KAAM,IAAK,EAAM,EAY/F,MAAc,qBACd,CACI,IAAM,EAAe,KAAK,QAAQ,OAClC,GAAG,CAAC,EAAgB,OAEpB,GAAM,CAAE,gBAAiB,KAAK,WAAW,SACzC,GAAG,CAAC,EACJ,CACI,KAAK,KAAK,KAAK,gEAAgE,CAC/E,OAGJ,IAAI,GAAM,CAAE,EAAM,KAAY,OAAO,QAAQ,EAAa,CAEtD,GACA,CAEI,IAAM,EAAQ,MAAM,EAAa,YAAY,EAAM,EAAO,IAAK,EAAO,QAAS,CAC3E,KAAM,EAAO,MAAQ,GACrB,OAAQ,EAAO,QAAU,EAC5B,CAAC,CAEF,KAAK,aAAa,IAAI,EAAM,EAAM,CAE/B,EAAO,UAEN,EAAM,MAAM,CAGhB,KAAK,KAAK,MAAM,yBAA0B,EAAM,GAAG,OAEhD,EACP,CACI,KAAK,KAAK,MAAM,iCAAkC,EAAM,IAAK,EAAM,EAa/E,MAAM,cACN,CACI,IAAI,GAAM,CAAE,EAAM,KAAW,KAAK,aAE3B,EAAM,QAAU,EAAA,WAAW,UAE1B,KAAK,+BAA+B,IAAI,EAAK,CAC7C,EAAM,OAAO,EASzB,MAAM,YACN,CACI,IAAI,IAAM,KAAQ,KAAK,+BACvB,CACI,IAAM,EAAQ,KAAK,aAAa,IAAI,EAAK,CACtC,GAEC,EAAM,MAAM,CAIpB,KAAK,+BAA+B,OAAO,CAU/C,MAAe,UACf,CAEI,GAAM,CAAE,iBAAkB,KAAK,WAAW,SACpC,EAAkB,KAAK,iBAAiB,IAAI,KAAO,IACzD,CACI,GACA,CACI,MAAM,EAAc,cAAc,EAAO,GAAG,OAEzC,EACP,CACI,KAAK,KAAK,MAAM,4BAA6B,EAAO,GAAI,GAAI,EAAM,GAExE,CAEF,MAAM,QAAQ,IAAI,EAAgB,CAElC,KAAK,iBAAmB,EAAE,CAC1B,KAAK,aAAe,EAAE,CACtB,KAAK,aAAe,EAAE,CAGtB,IAAI,IAAM,KAAS,KAAK,aAAa,QAAQ,CAEzC,EAAM,SAAS,CAEnB,KAAK,aAAa,OAAO,CACzB,KAAK,+BAA+B,OAAO,CAE3C,MAAM,MAAM,UAAU,GCtsCjB,GAAb,KACA,CACI,UACA,YAA0C,KAC1C,KACA,QAGA,cAAwB,IAAI,IAG5B,cAAwB,IAAI,IAG5B,cAAwB,IAAI,IAG5B,kBAA4B,IAAI,IAGhC,cAAuC,KAGvC,oBAA+C,EAAE,CAOjD,IAAW,cACX,CACI,OAAO,KAAK,cAMhB,IAAW,kBACX,CACI,OAAO,KAAK,kBAKhB,YACI,EACA,EAEJ,CACI,KAAK,UAAY,EACjB,KAAK,QAAU,EACf,KAAK,KAAO,GAAQ,UAAU,eAAe,EAAI,IAAI,EAAW,eAAe,CAG/E,KAAK,oBAAoB,KACrB,KAAK,UAAU,UAAU,iBAAmB,GAC5C,CACI,KAAK,gBAAgB,EAAM,QAAQ,EACrC,CACL,CAED,KAAK,oBAAoB,KACrB,KAAK,UAAU,UAAU,iBAAmB,GAC5C,CACI,KAAK,gBAAgB,EAAM,QAAQ,EACrC,CACL,CAED,KAAK,oBAAoB,KACrB,KAAK,UAAU,UAAU,cAAgB,GACzC,CACI,KAAK,aAAa,EAAM,QAAQ,EAClC,CACL,CAED,KAAK,KAAK,KAAK,2BAA2B,CAW9C,eAAe,EACf,CACI,KAAK,YAAc,EAOvB,gBAAwB,EACxB,CACI,KAAK,KAAK,MAAM,SAAU,EAAQ,UAAW,aAAc,EAAQ,SAAU,GAAG,CAGpF,gBAAwB,EACxB,CACI,KAAK,KAAK,KAAK,SAAU,EAAQ,UAAW,sBAAsB,CAGtE,aAAqB,EACrB,CACI,KAAK,KAAK,MAAM,SAAU,EAAQ,UAAW,UAAW,EAAQ,UAAW,CAM/E,gBACA,CACI,GAAG,CAAC,KAAK,YAEL,MAAU,MAAM,iEAAiE,CAGrF,MAAO,CACH,WAAY,KAAK,YACjB,iBAAkB,KAAK,kBACvB,OAAQ,KAAK,QAChB,CAML,qBAA6B,EAC7B,CACI,IAAM,EAAU,KAAK,gBAAgB,CAErC,GAAG,EAAO,MACV,CAEI,IAAM,EAAa,KAAK,cAAc,IAAI,EAAO,MAAM,CACvD,GAAG,CAAC,EAEA,MAAU,MAAM,gBAAiB,EAAO,MAAO,sBAAsB,CAIzE,OADA,KAAK,KAAK,MAAM,mBAAoB,EAAO,KAAM,iBAAkB,EAAO,MAAO,GAAG,CAC7E,IAAI,EAAW,EAAQ,EAAQ,CAK1C,OADA,KAAK,KAAK,MAAM,mBAAoB,EAAO,KAAM,mBAAmB,CAC7D,IAAI,EAAU,EAAQ,EAAQ,CAazC,oBAA2B,EAC3B,CACO,KAAK,cAAc,IAAI,EAAO,KAAK,EAElC,KAAK,KAAK,KAAK,iBAAkB,EAAO,KAAM,uCAAuC,CAGzF,KAAK,cAAc,IAAI,EAAO,KAAM,EAAO,CAC3C,KAAK,KAAK,KAAK,4BAA6B,EAAO,OAAQ,CAU/D,mBAA0B,EAAe,EACzC,CACO,KAAK,cAAc,IAAI,EAAK,EAE3B,KAAK,KAAK,KAAK,gBAAiB,EAAM,uCAAuC,CAGjF,KAAK,cAAc,IAAI,EAAM,EAAW,CACxC,KAAK,KAAK,KAAK,2BAA4B,IAAQ,CAcvD,wBAA+B,EAAmB,EAClD,CACO,KAAK,kBAAkB,IAAI,EAAS,EAEnC,KAAK,KAAK,KAAK,qBAAsB,EAAU,uCAAuC,CAG1F,KAAK,kBAAkB,IAAI,EAAU,EAAQ,CAC7C,KAAK,KAAK,MAAM,gCAAiC,IAAY,CAQjE,0BAAiC,EACjC,CACI,KAAK,kBAAkB,OAAO,EAAS,CAO3C,0BACA,CACI,KAAK,kBAAkB,OAAO,CAalC,SAAgB,EAChB,CACI,OAAO,KAAK,cAAc,IAAI,EAAK,EAAI,KAQ3C,eAAsB,EACtB,CACI,OAAO,KAAK,cAAc,IAAI,EAAK,EAAI,KAwB3C,MAAa,UAAU,EACvB,CAEI,IAAI,EACJ,GAAG,OAAO,GAAiB,SAC3B,CACI,IAAM,EAAa,KAAK,cAAc,IAAI,EAAa,CACvD,GAAG,CAAC,EAEA,MAAU,MAAM,iBAAkB,EAAc,sBAAsB,CAE1E,EAAS,OAIT,EAAS,EAET,KAAK,oBAAoB,EAAO,CAIpC,IAAM,EAAW,KAAK,cAAc,IAAI,EAAO,KAAK,CACpD,GAAG,EAGC,OADA,KAAK,KAAK,KAAK,UAAW,EAAO,KAAM,qBAAqB,CACrD,EAIX,KAAK,KAAK,MAAM,kBAAmB,EAAO,OAAQ,CAClD,IAAM,EAAQ,KAAK,qBAAqB,EAAO,CAM/C,OALA,MAAM,EAAM,MAAM,CAGlB,KAAK,cAAc,IAAI,EAAO,KAAM,EAAM,CAEnC,EAuBX,MAAa,cAAc,EAC3B,CACI,IAAM,EACA,MAAM,KAAK,UAAU,EAAa,CAIxC,MAFA,MAAK,cAAgB,EACrB,KAAK,KAAK,KAAK,oBAAqB,EAAM,OAAQ,CAC3C,EASX,MAAa,YAAY,EACzB,CACI,IAAM,EAAQ,KAAK,cAAc,IAAI,EAAK,CACvC,IAGI,KAAK,gBAAkB,IAEtB,KAAK,cAAgB,MAIzB,MAAM,EAAM,UAAU,CAGtB,KAAK,cAAc,OAAO,EAAK,CAE/B,KAAK,KAAK,KAAK,mBAAoB,IAAQ,EAOnD,MAAa,oBACb,CACO,KAAK,cAEJ,MAAM,KAAK,YAAY,KAAK,cAAc,KAAK,CAI/C,KAAK,KAAK,KAAK,6BAA6B,CAiBpD,MAAa,WAAW,EAAoB,EAC5C,CACI,IAAM,EAAW,KAAK,cAEtB,GACA,CAEI,KAAK,UAAU,QAAQ,CACnB,KAAM,yBACN,QAAS,CACL,KAAM,GAAU,KAChB,GAAI,EACP,CACJ,CAAC,CAGC,GAAU,cAET,MAAM,EAAS,cAAc,CAIjC,KAAK,UAAU,QAAQ,CACnB,KAAM,4BACN,QAAS,CACL,MAAO,UACP,YACH,CACJ,CAAC,CAGF,IAAM,EAAW,MAAM,KAAK,UAAU,EAAU,CAGhD,GAAG,GAAS,YAER,OAID,GAAY,CAAC,GAAS,WAErB,MAAM,KAAK,YAAY,EAAS,KAAK,CAIzC,KAAK,cAAgB,EAGlB,EAAS,YAER,MAAM,EAAS,YAAY,CAI/B,KAAK,UAAU,QAAQ,CACnB,KAAM,4BACN,QAAS,CAAE,YAAW,CACzB,CAAC,OAEC,EACP,CAWI,MATA,KAAK,UAAU,QAAQ,CACnB,KAAM,yBACN,QAAS,CACL,KAAM,GAAU,KAChB,GAAI,EACJ,QACH,CACJ,CAAC,CAEI,GAWd,MAAM,WACN,CAEI,IAAI,IAAM,KAAe,KAAK,oBAE1B,GAAa,CAEjB,KAAK,oBAAsB,EAAE,CAG7B,IAAI,IAAM,KAAS,KAAK,cAAc,QAAQ,CAG1C,MAAM,EAAM,UAAU,CAG1B,KAAK,cAAc,OAAO,CAC1B,KAAK,cAAc,OAAO,CAC1B,KAAK,cAAgB,OClhBhB,EAAb,KACA,CACI,aACA,UAAoB,IAAI,IACxB,aAAuB,GACvB,wBAAkC,EAClC,KAEA,YAAY,EAA2B,EACvC,CACI,KAAK,aAAe,EACpB,KAAK,KAAO,EAAO,UAAU,eAAe,CAOhD,MAAM,WAAW,EACjB,CACI,KAAK,KAAK,KAAK,gCAAiC,EAAS,KAAK,KAAK,GAAI,CAEvE,IAAI,IAAM,KAAQ,EAClB,CAEI,IAAM,EAAM,MAAM,KAAK,aAAa,UAAU,EAAK,CACnD,KAAK,UAAU,IAAI,EAAM,CAAE,MAAK,OAAQ,EAAG,MAAO,GAAO,CAAC,CAG9D,KAAK,KAAK,KAAK,8BAA8B,CAGjD,MAAM,WACN,CACI,KAAK,KAAK,KAAK,gCAAgC,CAE/C,IAAI,IAAM,KAAS,KAAK,UAAU,QAAQ,CAEtC,EAAM,IAAI,SAAS,CAGvB,KAAK,UAAU,OAAO,CACtB,KAAK,KAAK,KAAK,2BAA2B,CAO9C,MAAM,YACF,EACA,EACA,EACA,EAEJ,CACI,IAAI,EAEJ,GAAG,EACH,CACI,IAAM,EAAQ,KAAK,UAAU,IAAI,EAAQ,CACrC,EAMA,EAAM,EAAM,IAJZ,KAAK,KAAK,KAAK,0BAA2B,EAAS,YAAa,EAAM,yBAAyB,CAQvG,OAAO,KAAK,aAAa,YAAY,EAAM,EAAK,EAAK,EAAQ,CAOjE,gBAAgB,EAChB,CACQ,KAAK,aAOL,KAAK,wBAA0B,EAL/B,KAAK,aAAa,gBAAgB,EAAO,CASjD,iBACA,CAMI,OALG,KAAK,aAEG,KAAK,wBAGT,KAAK,aAAa,iBAAiB,CAG9C,eAAe,EACf,CACO,IAAU,KAAK,eAElB,KAAK,aAAe,EAEjB,GAEC,KAAK,wBAA0B,KAAK,aAAa,iBAAiB,CAClE,KAAK,aAAa,gBAAgB,EAAE,EAIpC,KAAK,aAAa,gBAAgB,KAAK,wBAAwB,EAIvE,eACA,CACI,OAAO,KAAK,aAOhB,iBAAiB,EAAkB,EACnC,CACI,IAAM,EAAQ,KAAK,UAAU,IAAI,EAAQ,CACzC,GAAG,CAAC,EACJ,CACI,KAAK,KAAK,KAAK,0BAA2B,IAAW,CACrD,OAGJ,EAAM,OAAS,EAEX,EAAM,OAEN,KAAK,aAAa,aAAa,EAAM,IAAK,EAAO,CAIzD,iBAAiB,EACjB,CAEI,OADc,KAAK,UAAU,IAAI,EAAQ,EAC3B,QAAU,EAG5B,gBAAgB,EAAkB,EAClC,CACI,IAAM,EAAQ,KAAK,UAAU,IAAI,EAAQ,CACzC,GAAG,CAAC,EACJ,CACI,KAAK,KAAK,KAAK,0BAA2B,IAAW,CACrD,OAGD,IAAU,EAAM,QAEnB,EAAM,MAAQ,EACd,KAAK,aAAa,aAAa,EAAM,IAAK,EAAQ,EAAI,EAAM,OAAO,EAGvE,eAAe,EACf,CAEI,OADc,KAAK,UAAU,IAAI,EAAQ,EAC3B,OAAS,GAG3B,aACA,CACI,OAAO,MAAM,KAAK,KAAK,UAAU,MAAM,CAAC,GCpKnC,EAAb,KACA,CACI,eACA,cACA,KAEA,sBAAwD,EAAE,CAC1D,uBAA0D,EAAE,CAE5D,YACI,EACA,EACA,EAEJ,CACI,KAAK,eAAiB,EACtB,KAAK,cAAgB,EACrB,KAAK,KAAO,GAAQ,UAAU,cAAc,EAAI,IAAI,EAAW,cAAc,CAC7E,KAAK,KAAK,KAAK,0BAA0B,CAW7C,kBAAkB,EAClB,CACI,KAAK,sBAAsB,KAAK,EAAK,CAOzC,mBAAmB,EACnB,CACI,KAAK,uBAAuB,KAAK,EAAK,CAY1C,WACA,CACI,KAAK,KAAK,MAAM,4BAA4B,CAE5C,IAAM,EAAY,KAAK,cAAc,cAAc,MAAQ,GACrD,EAAgC,EAAE,CAExC,IAAI,IAAM,KAAU,KAAK,eAAe,gBAAgB,CACxD,CACI,IAAM,EAAgC,CAClC,GAAI,EAAO,GACX,KAAM,EAAO,KACb,KAAM,MAAM,KAAK,EAAO,KAAK,CAC7B,MAAO,gBAAgB,EAAO,MAAM,CACvC,CAYD,GAVG,EAAO,OAEN,EAAW,KAAO,EAAO,MAG1B,EAAO,SAEN,EAAW,SAAW,EAAO,OAAO,IAGrC,EAAO,KACV,CACI,GAAM,CAAE,WAAU,WAAU,WAAY,EAAO,KAC/C,EAAW,UAAY,CACnB,SAAU,CAAE,EAAG,EAAS,EAAG,EAAG,EAAS,EAAG,EAAG,EAAS,EAAG,CACzD,SAAU,CAAE,EAAG,EAAS,EAAG,EAAG,EAAS,EAAG,EAAG,EAAS,EAAG,CACzD,QAAS,CAAE,EAAG,EAAQ,EAAG,EAAG,EAAQ,EAAG,EAAG,EAAQ,EAAG,CACxD,CAGL,EAAS,KAAK,EAAW,CAI7B,IAAI,EAAmC,EAAE,CACzC,IAAI,IAAM,KAAQ,KAAK,sBACvB,CACI,IAAM,EAAW,GAAM,CACvB,EAAS,CAAE,GAAG,EAAQ,GAAG,EAAU,CAGvC,IAAM,EAAsB,CACxB,QAAS,EACT,YACA,WACA,SACH,CAGD,OADA,KAAK,KAAK,KAAK,cAAe,EAAS,OAAQ,wBAAyB,EAAW,GAAG,CAC/E,EAkBX,MAAM,YAAY,EAClB,CACI,KAAK,KAAK,MAAM,oCAAqC,EAAK,QAAS,WAAY,EAAK,UAAW,OAAO,CAGtG,MAAM,KAAK,cAAc,WAAW,EAAK,UAAU,CAGnD,IAAM,EAAyB,EAAE,CACjC,IAAI,IAAM,KAAU,KAAK,eAAe,gBAAgB,CAEpD,EAAY,KAAK,EAAO,GAAG,CAG/B,IAAI,IAAM,KAAM,EAGZ,MAAM,KAAK,eAAe,cAAc,EAAG,CAI/C,IAAM,EAAQ,IAAI,IAElB,IAAI,IAAM,KAAc,EAAK,SAC7B,CAEI,IAAM,EAAS,MAAM,KAAK,eAAe,aAAa,EAAW,KAAM,CACnE,KAAM,EAAW,KACjB,aAAc,EAAW,MACzB,KAAM,EAAW,KACpB,CAAC,CAKF,GAHA,EAAM,IAAI,EAAW,GAAI,EAAO,GAAG,CAGhC,EAAW,WAAa,EAAO,KAClC,CACI,GAAM,CAAE,WAAU,WAAU,WAAY,EAAW,UACnD,EAAO,KAAK,SAAS,EAAI,EAAS,EAClC,EAAO,KAAK,SAAS,EAAI,EAAS,EAClC,EAAO,KAAK,SAAS,EAAI,EAAS,EAClC,EAAO,KAAK,SAAS,EAAI,EAAS,EAClC,EAAO,KAAK,SAAS,EAAI,EAAS,EAClC,EAAO,KAAK,SAAS,EAAI,EAAS,EAClC,EAAO,KAAK,QAAQ,EAAI,EAAQ,EAChC,EAAO,KAAK,QAAQ,EAAI,EAAQ,EAChC,EAAO,KAAK,QAAQ,EAAI,EAAQ,GAKxC,IAAI,IAAM,KAAc,EAAK,SAEzB,GAAG,EAAW,SACd,CACI,IAAM,EAAc,EAAM,IAAI,EAAW,SAAS,CAC5C,EAAa,EAAM,IAAI,EAAW,GAAG,CAExC,GAAe,EAEd,KAAK,eAAe,SAAS,EAAa,EAAW,CAIrD,KAAK,KAAK,KACN,mCAAoC,EAAW,GAAI,MAAO,EAAW,SAAU,YAChE,EAAY,MAAO,EAAa,GAClD,CAMb,IAAI,IAAM,KAAQ,KAAK,uBAEnB,EAAK,EAAK,OAAO,CAGrB,KAAK,KAAK,KAAK,gBAAiB,EAAK,SAAS,OAAQ,uBAAwB,EAAK,UAAW,GAAG,CAOrG,MAAM,WACN,CACI,KAAK,sBAAwB,EAAE,CAC/B,KAAK,uBAAyB,EAAE,CAChC,KAAK,KAAK,KAAK,wBAAwB,GC9OlC,GAAb,KACA,CACI,gBACA,WAA+C,EAAE,CAGjD,mBACA,gBAOA,aACA,CAEI,KAAK,gBAAkB,CACnB,GAAI,aACJ,KAAM,WACN,KAAM,WACN,UAAW,GACd,CAGD,KAAK,sBAAsB,CAG3B,eAAiB,KAAK,wBAAwB,CAAE,EAAE,CAYtD,kBAAyB,EACzB,CACI,KAAK,mBAAqB,EAQ9B,eAAsB,EACtB,CACI,KAAK,gBAAkB,EAM3B,UACA,CACI,MAAO,CACH,KAAM,WACN,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,MAAO,EAAE,CACZ,CAML,WACA,CACI,MAAO,CAAE,GAAG,KAAK,gBAAiB,CAMtC,WACA,CAKI,OAHA,OAAO,oBAAoB,UAAW,KAAK,eAAe,CAC1D,OAAO,oBAAoB,QAAS,KAAK,aAAa,CAE/C,QAAQ,SAAS,CAU5B,sBACA,CAEI,KAAK,eAAiB,KAAK,eAAe,KAAK,KAAK,CACpD,KAAK,aAAe,KAAK,aAAa,KAAK,KAAK,CAGhD,OAAO,iBAAiB,UAAW,KAAK,eAAe,CACvD,OAAO,iBAAiB,QAAS,KAAK,aAAa,CAMvD,eAAuB,EACvB,CACI,KAAK,WAAW,EAAM,MAAQ,GAE9B,IAAM,EAAkC,EAAE,CAC1C,EAAM,EAAM,MAAQ,GAEpB,IAAM,EAA6B,CAC/B,KAAM,WACN,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,QACA,QACH,CAED,KAAK,oBAAoB,EAAM,CAMnC,aAAqB,EACrB,CACI,KAAK,WAAW,EAAM,MAAQ,GAE9B,IAAM,EAAkC,EAAE,CAC1C,EAAM,EAAM,MAAQ,GAEpB,IAAM,EAA6B,CAC/B,KAAM,WACN,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,QACA,QACH,CAED,KAAK,oBAAoB,EAAM,CAMnC,wBACA,CACO,KAAK,oBAEJ,KAAK,mBAAmB,KAAK,gBAAgB,CAOrD,oBAA4B,EAC5B,CACO,KAAK,iBAEJ,KAAK,gBAAgB,KAAK,gBAAiB,EAAM,GCnKhD,GAAb,KACA,CACI,eACA,aACA,aAAqD,EAAE,CACvD,WAA8C,EAAE,CAChD,UAA+B,CAC3B,SAAU,CAAE,EAAG,EAAG,EAAG,EAAG,CACxB,SAAU,CAAE,EAAG,EAAG,EAAG,EAAG,CAC3B,CAED,YAAsB,CAClB,OAAQ,EACR,OAAQ,EACR,OAAQ,EACR,UAAW,EACd,CAGD,mBACA,gBASA,YAAY,EAA8B,SAAS,KACnD,CACI,KAAK,eAAiB,EAGtB,KAAK,aAAe,CAChB,GAAI,UACJ,KAAM,QACN,KAAM,QACN,UAAW,GACd,CAGD,KAAK,mBAAmB,CAGxB,KAAK,WAAW,UAAY,EAC5B,KAAK,WAAW,UAAY,EAG5B,eAAiB,KAAK,wBAAwB,CAAE,EAAE,CAYtD,kBAAyB,EACzB,CACI,KAAK,mBAAqB,EAQ9B,eAAsB,EACtB,CACI,KAAK,gBAAkB,EAM3B,UACA,CACI,MAAO,CACH,KAAM,QACN,QAAS,CAAE,GAAG,KAAK,aAAc,CACjC,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,SAAU,CACN,SAAU,CAAE,GAAG,KAAK,UAAU,SAAU,CACxC,SAAU,CAAE,GAAG,KAAK,UAAU,SAAU,CAC3C,CACD,MAAO,CAAE,GAAG,KAAK,YAAa,CACjC,CAML,WACA,CACI,MAAO,CAAE,GAAG,KAAK,aAAc,CAMnC,WACA,CAMI,OALA,KAAK,eAAe,oBAAoB,cAAe,KAAK,mBAAmB,CAC/E,KAAK,eAAe,oBAAoB,YAAa,KAAK,iBAAiB,CAC3E,KAAK,eAAe,oBAAoB,cAAe,KAAK,mBAAmB,CAC/E,KAAK,eAAe,oBAAoB,QAAS,KAAK,kBAAkB,CAEjE,QAAQ,SAAS,CAY5B,mBACA,CAEI,KAAK,mBAAqB,KAAK,mBAAmB,KAAK,KAAK,CAC5D,KAAK,iBAAmB,KAAK,iBAAiB,KAAK,KAAK,CACxD,KAAK,mBAAqB,KAAK,mBAAmB,KAAK,KAAK,CAC5D,KAAK,kBAAoB,KAAK,kBAAkB,KAAK,KAAK,CAE1D,KAAK,eAAe,iBAAiB,cAAe,KAAK,mBAAmB,CAC5E,KAAK,eAAe,iBAAiB,YAAa,KAAK,iBAAiB,CACxE,KAAK,eAAe,iBAAiB,cAAe,KAAK,mBAAmB,CAC5E,KAAK,eAAe,iBAAiB,QAAS,KAAK,kBAAkB,CAMzE,mBAA2B,EAC3B,CACI,GAAG,EAAM,cAAgB,QAAW,OAEpC,IAAM,EAAY,UAAW,EAAM,SACnC,KAAK,aAAa,GAAa,CAAE,QAAS,GAAM,CAEhD,KAAK,oBAAoB,CACrB,KAAM,QACN,QACA,QAAS,CAAE,GAAG,KAAK,aAAc,CACjC,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,SAAU,CAAE,GAAG,KAAK,UAAW,CAClC,CAAC,CAMN,iBAAyB,EACzB,CACI,GAAG,EAAM,cAAgB,QAAW,OAEpC,IAAM,EAAY,UAAW,EAAM,SACnC,KAAK,aAAa,GAAa,CAAE,QAAS,GAAO,CAEjD,KAAK,oBAAoB,CACrB,KAAM,QACN,QACA,QAAS,CAAE,GAAG,KAAK,aAAc,CACjC,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,SAAU,CAAE,GAAG,KAAK,UAAW,CAClC,CAAC,CAMN,mBAA2B,EAC3B,CACO,EAAM,cAAgB,UAEzB,KAAK,UAAY,CACb,SAAU,CACN,EAAG,EAAM,QACT,EAAG,EAAM,QACZ,CACD,SAAU,CACN,EAAG,EAAM,UACT,EAAG,EAAM,UACZ,CACJ,CAED,KAAK,WAAW,UAAY,EAAM,QAClC,KAAK,WAAW,UAAY,EAAM,QAElC,KAAK,oBAAoB,CACrB,KAAM,QACN,QACA,QAAS,CAAE,GAAG,KAAK,aAAc,CACjC,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,SAAU,CAAE,GAAG,KAAK,UAAW,CAClC,CAAC,EAMN,kBAA0B,EAC1B,CACI,KAAK,YAAc,CACf,OAAQ,EAAM,OACd,OAAQ,EAAM,OACd,OAAQ,EAAM,OACd,UAAW,EAAM,UACpB,CAED,KAAK,oBAAoB,CACrB,KAAM,QACN,QACA,QAAS,CAAE,GAAG,KAAK,aAAc,CACjC,KAAM,CAAE,GAAG,KAAK,WAAY,CAC5B,SAAU,CAAE,GAAG,KAAK,UAAW,CAC/B,MAAO,CAAE,GAAG,KAAK,YAAa,CACjC,CAAC,CAMN,wBACA,CACO,KAAK,oBAEJ,KAAK,mBAAmB,KAAK,aAAa,CAOlD,oBAA4B,EAC5B,CACO,KAAK,iBAEJ,KAAK,gBAAgB,KAAK,aAAc,EAAM,GCrP7C,GAAb,KACA,CACI,gBAAsE,EAAE,CACxE,cAAkF,EAAE,CACpF,YAA2E,EAAE,CAG7E,mBACA,sBACA,gBAOA,aACA,CAEI,KAAK,qBAAqB,CAY9B,kBAAyB,EACzB,CACI,KAAK,mBAAqB,EAQ9B,qBAA4B,EAC5B,CACI,KAAK,sBAAwB,EAQjC,eAAsB,EACtB,CACI,KAAK,gBAAkB,EAM3B,YACA,CACI,OAAO,OAAO,OAAO,KAAK,gBAAgB,CAAC,IAAK,IAAY,CAAE,GAAG,EAAQ,EAAmB,CAMhG,WACA,CACI,IAAM,EAA6C,EAAE,CAerD,OAbA,OAAO,KAAK,KAAK,cAAc,CAAC,QAAS,GACzC,CACI,IAAM,EAAQ,OAAO,EAAS,CACxB,EAAU,KAAK,cAAc,IAAU,EAAE,CACzC,EAAO,KAAK,YAAY,IAAU,EAAE,CAE1C,EAAO,GAAS,CACZ,KAAM,UACN,QAAS,CAAE,GAAG,EAAS,CACvB,KAAM,CAAE,GAAG,EAAM,CACpB,EACH,CAEK,EAQX,UAAiB,EACjB,CACI,IAAM,EAAS,KAAK,gBAAgB,GACpC,OAAO,EAAS,CAAE,GAAG,EAAQ,CAAG,KAQpC,SAAgB,EAChB,CACI,IAAM,EAAU,KAAK,cAAc,GAC7B,EAAO,KAAK,YAAY,GAI9B,MAFG,CAAC,GAAW,CAAC,EAAe,KAExB,CACH,KAAM,UACN,QAAS,EAAU,CAAE,GAAG,EAAS,CAAG,EAAE,CACtC,KAAM,EAAO,CAAE,GAAG,EAAM,CAAG,EAAE,CAChC,CAML,cACA,CAGI,GAAG,CAAC,UAAU,YAAe,OAE7B,IAAM,EAAW,UAAU,aAAa,CACxC,IAAI,IAAM,KAAW,EACrB,CACI,GAAG,CAAC,EAAW,SAEf,IAAM,EAAQ,EAAQ,MAEtB,GAAG,CAAC,KAAK,gBAAgB,GACzB,CACI,KAAK,wBAAwB,EAAQ,CACrC,SAIJ,IAAM,EAAS,KAAK,gBAAgB,GACpC,GAAG,CAAC,EAAU,SAGd,IAAM,EAAiB,KAAK,cAAc,IAAU,EAAE,CAChD,EAAc,KAAK,YAAY,IAAU,EAAE,CAG3C,EAA2C,EAAE,CAC/C,EAAiB,GAErB,EAAQ,QAAQ,SAAS,EAAK,IAC9B,CACI,IAAM,EAAY,UAAW,IACvB,EAA4B,CAC9B,QAAS,EAAI,QACb,QAAS,EAAI,QACb,MAAO,EAAI,MACd,CAED,EAAW,GAAa,EAExB,IAAM,EAAa,EAAe,IAC/B,CAAC,GACG,EAAW,UAAY,EAAY,SACnC,EAAW,UAAY,EAAY,SACnC,EAAW,QAAU,EAAY,SAEpC,EAAiB,KAEvB,CAGF,IAAM,EAAmC,EAAE,CACvC,EAAc,GAmBlB,GAjBA,EAAQ,KAAK,SAAS,EAAW,IACjC,CACI,IAAM,EAAU,QAAS,IACzB,EAAQ,GAAW,EAEF,EAAY,KACb,IAEZ,EAAc,KAEpB,CAGF,KAAK,cAAc,GAAS,EAC5B,KAAK,YAAY,GAAS,EAGvB,GAAkB,EACrB,CACI,IAAM,EAA4B,CAC9B,KAAM,UACN,QAAS,CAAE,GAAG,EAAY,CAC1B,KAAM,CAAE,GAAG,EAAS,CACvB,CAED,KAAK,oBAAoB,EAAQ,EAAM,GAUnD,WACA,CAKI,OAHA,OAAO,oBAAoB,mBAAoB,KAAK,wBAAwB,CAC5E,OAAO,oBAAoB,sBAAuB,KAAK,2BAA2B,CAE3E,QAAQ,SAAS,CAU5B,qBACA,CAUI,GARA,KAAK,wBAA0B,KAAK,wBAAwB,KAAK,KAAK,CACtE,KAAK,2BAA6B,KAAK,2BAA2B,KAAK,KAAK,CAG5E,OAAO,iBAAiB,mBAAoB,KAAK,wBAAwB,CACzE,OAAO,iBAAiB,sBAAuB,KAAK,2BAA2B,CAG5E,UAAU,YACb,CACI,IAAM,EAAW,UAAU,aAAa,CACxC,IAAI,IAAM,KAAW,EAEd,GAEC,KAAK,wBAAwB,EAAQ,EASrD,wBAAgC,EAChC,CACI,IAAM,EAAU,aAAiB,aAAe,EAAM,QAAU,EAC1D,EAAQ,EAAQ,MAGhB,EAAgC,CAClC,GAAI,WAAY,IAChB,KAAM,EAAQ,GACd,KAAM,UACN,UAAW,GACX,QACA,QAAS,EAAQ,QACjB,KAAM,MAAM,KAAK,EAAQ,KAAK,CAC9B,QAAS,MAAM,KAAK,EAAQ,QAAQ,CACvC,CAGD,KAAK,gBAAgB,GAAS,EAG9B,IAAM,EAA+C,EAAE,CACvD,MAAM,KAAK,EAAQ,QAAQ,CAAC,SAAS,EAAK,IAC1C,CACI,EAAe,UAAW,KAAQ,CAC9B,QAAS,EAAI,QACb,QAAS,EAAI,QACb,MAAO,EAAI,MACd,EACH,CACF,KAAK,cAAc,GAAS,EAG5B,IAAM,EAAwC,EAAE,CAChD,MAAM,KAAK,EAAQ,KAAK,CAAC,SAAS,EAAW,IAC7C,CACI,EAAa,QAAS,KAAQ,GAChC,CACF,KAAK,YAAY,GAAS,EAG1B,KAAK,uBAAuB,EAAc,CAG1C,KAAK,oBAAoB,EAAe,CACpC,KAAM,UACN,QAAS,CAAE,GAAG,EAAgB,CAC9B,KAAM,CAAE,GAAG,EAAc,CACzB,MAAO,aAAiB,aAAe,EAAQ,IAAA,GAClD,CAAC,CAMN,2BAAmC,EACnC,CAEI,IAAM,EADU,EAAM,QACA,MAGhB,EAAgB,KAAK,gBAAgB,GAExC,IAGC,EAAc,UAAY,GAG1B,KAAK,0BAA0B,EAAc,CAG7C,KAAK,gBAAgB,GAAS,IAAA,GAC9B,KAAK,cAAc,GAAS,IAAA,GAC5B,KAAK,YAAY,GAAS,IAAA,IAOlC,uBAA+B,EAC/B,CACO,KAAK,oBAEJ,KAAK,mBAAmB,EAAO,CAOvC,0BAAkC,EAClC,CACO,KAAK,uBAEJ,KAAK,sBAAsB,EAAO,CAO1C,oBAA4B,EAAwB,EACpD,CACO,KAAK,iBAEJ,KAAK,gBAAgB,EAAQ,EAAM,GCtWlC,GAAb,KACA,CACI,UAEA,YACA,SACA,WAGA,KAWA,YACI,EACA,EACA,EAEJ,CACI,KAAK,UAAY,EAGjB,KAAK,KAAO,GAAQ,UAAU,mBAAmB,EAAI,IAAI,EAAW,mBAAmB,CAEvF,KAAK,KAAK,KAAK,gCAAgC,CAG/C,KAAK,KAAK,MAAM,6CAA6C,CAC7D,KAAK,YAAc,IAAI,GACvB,KAAK,SAAW,IAAI,GAAiB,EAAO,CAC5C,KAAK,WAAa,IAAI,GAGtB,KAAK,KAAK,MAAM,oCAAoC,CACpD,KAAK,YAAY,kBAAkB,KAAK,wBAAwB,KAAK,KAAK,CAAC,CAC3E,KAAK,YAAY,eAAe,KAAK,qBAAqB,KAAK,KAAK,CAAC,CACrE,KAAK,SAAS,kBAAkB,KAAK,wBAAwB,KAAK,KAAK,CAAC,CACxE,KAAK,SAAS,eAAe,KAAK,qBAAqB,KAAK,KAAK,CAAC,CAClE,KAAK,WAAW,kBAAkB,KAAK,wBAAwB,KAAK,KAAK,CAAC,CAC1E,KAAK,WAAW,qBAAqB,KAAK,2BAA2B,KAAK,KAAK,CAAC,CAChF,KAAK,WAAW,eAAe,KAAK,qBAAqB,KAAK,KAAK,CAAC,CAEpE,KAAK,KAAK,KAAK,4CAA4C,CAU/D,wBAAgC,EAChC,CACI,KAAK,KAAK,MAAM,qBAAsB,EAAO,GAAI,IAAK,EAAO,KAAM,GAAG,CACtE,KAAK,UAAU,QAAQ,CAAE,KAAM,yBAA0B,QAAS,CAAE,SAAQ,CAAE,CAAC,CAMnF,2BAAmC,EACnC,CACI,KAAK,KAAK,MAAM,wBAAyB,EAAO,GAAI,IAAK,EAAO,KAAM,GAAG,CACzE,KAAK,UAAU,QAAQ,CAAE,KAAM,4BAA6B,QAAS,CAAE,SAAQ,CAAE,CAAC,CAMtF,qBACI,EACA,EAEJ,CACI,KAAK,KAAK,MAAM,kBAAmB,EAAO,KAAO,EAAM,CACvD,KAAK,UAAU,QAAQ,CACnB,KAAM,gBACN,QAAS,CACL,SAAU,EAAO,GACjB,SACA,QACH,CACJ,CAAC,CAUN,WACA,CAOI,OANA,KAAK,KAAK,KAAK,gCAAgC,CAG/C,KAAK,KAAK,MAAM,8CAA8C,CAGvD,QAAQ,IAAI,CACf,KAAK,YAAY,WAAW,CAC5B,KAAK,SAAS,WAAW,CACzB,KAAK,WAAW,WAAW,CAC9B,CAAC,CAAC,SACH,CACI,KAAK,KAAK,KAAK,0CAA0C,EAC3D,CAUN,aACA,CACI,KAAK,KAAK,MAAM,sCAAsC,CAEtD,IAAM,EAA0B,EAAE,CAUlC,OAPA,EAAQ,KAAK,KAAK,YAAY,WAAW,CAAC,CAC1C,EAAQ,KAAK,KAAK,SAAS,WAAW,CAAC,CAGvC,EAAQ,KAAK,GAAG,KAAK,WAAW,YAAY,CAAC,CAE7C,KAAK,KAAK,MAAM,SAAU,EAAQ,OAAQ,oBAAoB,CACvD,EASX,UAAiB,EACjB,CAII,GAHA,KAAK,KAAK,MAAM,mBAAoB,IAAY,CAG7C,EAAS,WAAW,YAAY,CAE/B,OAAO,KAAK,YAAY,WAAW,CAGvC,GAAG,EAAS,WAAW,SAAS,CAE5B,OAAO,KAAK,SAAS,WAAW,CAGpC,GAAG,EAAS,WAAW,WAAW,CAClC,CACI,IAAM,EAAQ,SAAS,EAAS,MAAM,IAAI,CAAC,GAAI,GAAG,CAClD,OAAO,KAAK,WAAW,UAAU,EAAM,CAW3C,OAPe,KAAK,aAAa,CAAC,KAAM,GAAQ,EAAI,KAAO,EAAS,GAMpE,KAAK,KAAK,KAAK,qBAAsB,IAAY,CAC1C,MAMX,cACA,CACI,KAAK,WAAW,cAAc,GCtLtC,eAAe,GACX,EACA,EAEJ,CACI,IAAM,EAAS,IAAI,EAAA,aAAa,EAAQ,EAAQ,CAEhD,OADA,MAAM,EAAO,WAAW,CACjB,EASX,SAAS,EAAkB,EAAqB,EAChD,CACI,OAAO,IAAI,EAAA,OAAO,EAAQ,EAAQ,UAAW,EAAQ,QAAS,EAAQ,mBAAmB,CAQ7F,SAAS,GAAiB,EAC1B,CACI,OAAO,IAAI,EAAA,WAAW,EAA6B,CAavD,eAAsB,GAClB,EACA,EACA,EAAsB,GAE1B,CAEI,GAAG,IAAW,KAGV,OADA,QAAQ,MAAM,oBAAoB,CAC3B,GAAiB,EAA6B,CAKzD,GAAG,EAEC,GAAG,YAAa,EAChB,CACI,IAAM,EAAc,EACpB,EAAU,CACN,GAAG,EACH,QAAS,CAAE,GAAG,EAAY,QAAS,uBAAwB,GAAM,CACpE,MAKD,EAAU,CAAE,GAAG,EAAS,uBAAwB,GAAM,CAK9D,IAAM,EAAc,EAAQ,QAAU,OAGtC,GAAG,IAAgB,SAEf,GAAG,GAAW,CAEV,GACA,CAEI,OADA,QAAQ,MAAM,6BAA6B,CACpC,MAAM,GAAmB,EAAQ,EAAQ,OAE7C,EACP,CAEI,MADA,QAAQ,MAAM,uCAAwC,EAAM,CAClD,MACN,0FACA,CAAE,MAAO,EAAO,CACnB,MAKL,MAAU,MAAM,0DAA0D,CAKlF,GAAG,IAAgB,QAGf,OADA,QAAQ,MAAM,4BAA4B,CACnC,EAAkB,EAAQ,EAAgC,CAIrE,GAAG,GAAW,CAEV,GACA,CAGI,OAFA,QAAQ,MAAM,eAAe,CAEtB,GAAmB,EAAQ,EAAQ,CACrC,MAAO,IAEJ,QAAQ,KAAK,uDAAwD,EAAM,CACpE,EAAkB,EAAQ,EAAyB,EAC5D,OAEH,EACP,CACI,QAAQ,KAAK,uDAAwD,EAAM,MAK/E,QAAQ,KAAK,+CAA+C,CAKhE,OAFA,QAAQ,MAAM,cAAc,CAErB,EAAkB,EAAQ,EAAyB,CCnJ9D,eAAsB,IACtB,CACI,OAAO,MAAA,EAAA,EAAA,UAAoB,CAM/B,SAAgB,GAAkB,EAClC,CACI,OAAO,IAAI,EAAA,YAAY,GAAM,EAAM,CCKvC,IAAa,GAAb,KACA,CACI,eAEA,YAAY,EACZ,CACI,KAAK,eAAiB,EAO1B,WAAW,EAAe,EAAY,EAAY,EAClD,CACI,IAAM,EAAO,EAAM,KAAK,EAAG,EAAE,CAC7B,OAAO,KAAK,gBAAgB,EAAM,EAAO,CAG7C,aAAa,EAAe,EAAY,EAAY,EACpD,CACI,IAAM,EAAQ,EAAM,UAAU,EAAG,EAAE,CACnC,GAAG,CAAC,EAAS,MAAO,EAAE,CAEtB,IAAM,EAA+B,EAAE,CACvC,IAAI,IAAM,KAAQ,EAClB,CACI,IAAM,EAAS,KAAK,gBAAgB,EAAM,EAAO,CAC9C,GAEC,EAAQ,KAAK,EAAO,CAG5B,OAAO,EAGX,kBAAkB,EAAe,EAAW,EAC5C,CACI,IAAM,EAAO,EAAM,YAAY,EAAI,CAEnC,OADI,EACG,KAAK,gBAAgB,EAAM,EAAO,CADtB,KAIvB,oBAAoB,EAAe,EAAW,EAC9C,CACI,IAAM,EAAQ,EAAM,iBAAiB,EAAI,CACzC,GAAG,CAAC,EAAS,MAAO,EAAE,CAEtB,IAAM,EAA+B,EAAE,CACvC,IAAI,IAAM,KAAQ,EAClB,CACI,IAAM,EAAS,KAAK,gBAAgB,EAAM,EAAO,CAC9C,GAEC,EAAQ,KAAK,EAAO,CAG5B,OAAO,EAGX,kBACI,EACA,EACA,EAAc,IACd,EAEJ,CACI,IAAM,EAAM,EAAO,cAAc,EAAY,CAC7C,OAAO,KAAK,kBAAkB,EAAO,EAAK,EAAO,CAOrD,gBAAwB,EAAoB,EAC5C,CACI,GAAG,CAAC,EAAK,KAAO,CAAC,EAAK,YAAc,CAAC,EAAK,YAEtC,OAAO,KAGX,IAAM,EAAO,EAAK,WAGd,EAAS,KAAK,eAAe,UAAU,EAAiC,CAC5E,GAAG,CAAC,EACJ,CACI,IAAI,EAAkC,EAAkC,OACxE,KAAM,IAEF,EAAS,KAAK,eAAe,UAAU,EAAQ,CAC5C,KACH,EAAU,EAAQ,OAI1B,GAAG,CAAC,EAEA,OAAO,KAIX,GAAG,EACH,CACI,GAAG,EAAO,MAAQ,EAAO,OAAS,EAAO,KAErC,OAAO,KAGX,GAAG,EAAO,UAEF,IAAM,KAAO,EAAO,KAEpB,GAAG,CAAC,EAAO,OAAO,EAAI,CAElB,OAAO,KAKnB,GAAG,EAAO,WAAa,CAAC,EAAO,UAAU,EAAO,CAE5C,OAAO,KAIf,IAAM,EAAS,EAAK,UAAU,GAAM,GAAK,CAEzC,MAAO,CACH,SACA,MAAO,EAAK,YACZ,SAAU,EAAK,SACf,OAAQ,GAAU,CAAE,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,CACtC,OACA,YAAa,EAChB,GCzHI,GAAb,KACA,CACI,QAAkB,EAClB,QAAiC,EAAE,CACnC,WAAuC,EAAE,CACzC,WAAuC,EAAE,CAWzC,MAAM,EAAa,EACnB,CACI,GAAG,EAAK,EAEJ,MAAU,WAAW,iCAAkC,IAAM,CAGjE,IAAM,EAAK,KAAK,UAGhB,OAFA,KAAK,QAAQ,KAAK,CAAE,KAAI,UAAW,EAAI,WAAU,CAAC,KAGlD,CACI,KAAK,QAAU,KAAK,QAAQ,OAAQ,GAAU,EAAM,KAAO,EAAG,EAStE,SAAS,EAAa,EACtB,CACI,GAAG,GAAM,EAEL,MAAU,WAAW,mCAAoC,IAAM,CAGnE,IAAM,EAAK,KAAK,UAGhB,OAFA,KAAK,WAAW,KAAK,CAAE,KAAI,SAAU,EAAI,QAAS,EAAG,WAAU,CAAC,KAGhE,CACI,KAAK,WAAa,KAAK,WAAW,OAAQ,GAAU,EAAM,KAAO,EAAG,EAO5E,SAAS,EACT,CAEI,IAAM,EAAwB,CAAE,GADrB,KAAK,UACoB,SAAU,EAAI,UAAW,EAAG,CAGhE,OAFA,KAAK,WAAW,KAAK,EAAM,CAEpB,CACH,IAAI,OACJ,CACI,OAAO,EAAM,WAAa,GAE9B,OACA,CACI,EAAM,UAAY,EAAM,UAE/B,CAML,WACA,CACI,KAAK,QAAU,EAAE,CACjB,KAAK,WAAa,EAAE,CAGpB,IAAI,IAAM,KAAS,KAAK,WAEpB,EAAM,UAAY,EAGtB,KAAK,WAAa,EAAE,CAMxB,KAAK,EACL,CAEI,IAAM,EAAyB,EAAE,CACjC,IAAI,IAAM,KAAS,KAAK,QAEpB,EAAM,WAAa,EAChB,EAAM,WAAa,IAElB,EAAM,UAAU,CAChB,EAAY,KAAK,EAAM,GAAG,EAI/B,EAAY,OAAS,IAEpB,KAAK,QAAU,KAAK,QAAQ,OAAQ,GAAU,CAAC,EAAY,SAAS,EAAM,GAAG,CAAC,EAIlF,IAAI,IAAM,KAAS,KAAK,WAGpB,IADA,EAAM,SAAW,EACX,EAAM,SAAW,EAAM,UAEzB,EAAM,UAAU,CAChB,EAAM,SAAW,EAAM,SAK/B,IAAI,IAAM,KAAS,KAAK,WAEjB,EAAM,UAAY,IAEjB,EAAM,UAAY,KAAK,IAAI,EAAG,EAAM,UAAY,EAAK,ICvKxD,GAAY,CACrB,QACA,QACA,OACA,OACA,QACA,OACH,CCoEY,GAAb,cAAmC,CACnC,CACI,KAAO,QACP,mBAAgC,EAAE,CAGlC,QAAkB,IAAI,IAGtB,aAAuB,GAGvB,cAA8C,KAG9C,MAAuC,KAEvC,KAAiC,IAAI,EAAW,gBAAgB,CAMhE,eAAe,EAAsB,EACrC,CACI,GAAG,KAAK,aAEJ,OAQJ,GALA,KAAK,MAAQ,EACb,KAAK,aAAe,GAEpB,KAAK,KAAO,EAAW,OAAO,UAAU,gBAAgB,CAErD,EAAW,SAAS,aAEnB,KAAK,cAAgB,EAAW,SAAS,iBAG7C,CACI,KAAK,KAAK,KAAK,0DAA0D,CACzE,OAGJ,GAAG,CAAC,KAAK,OAEL,OAIJ,IAAM,EADQ,KAAK,OAAO,MACC,OACvB,KAKJ,IAAI,GAAM,CAAE,EAAM,KAAY,OAAO,QAAQ,EAAa,CAEtD,KAAK,aAAa,EAAM,EAAsB,CAItD,aAAa,EAAoB,EACjC,CACI,MAAO,GAGX,QAAQ,EACR,CACI,KAAK,gBAAgB,CAGzB,MAAM,SACN,CACI,KAAK,gBAAgB,CAOzB,aAAqB,EAAe,EACpC,CACI,GAAG,CAAC,KAAK,eAAiB,CAAC,KAAK,OAE5B,OAGJ,IAAM,EAAW,KAAK,OAAO,GACvB,EAAO,KAAK,MAElB,KAAK,cAAc,YACf,GAAI,EAAU,GAAI,IAClB,EAAO,IACP,EAAO,QACP,CACI,KAAM,EAAO,MAAQ,GACrB,SAAU,EAAO,UAAY,GAC7B,OAAQ,EAAO,QAAU,EACzB,eAAgB,EAAO,SAAW,GAClC,mBAAoB,EAAO,aAAe,IAC1C,qBAAsB,SACzB,CACJ,CACI,KAAM,GACP,CACI,KAAK,QAAQ,IAAI,EAAM,EAAM,EAEzB,EAAO,SAAW,KAAU,GAE5B,EAAM,QAAQ,OAAO,EAAK,EAEhC,CACD,MAAO,GACR,CACI,KAAK,KAAK,MAAM,2BAA4B,EAAM,KAAM,IAAO,EACjE,CAGV,gBACA,CACI,IAAI,IAAM,KAAS,KAAK,QAAQ,QAAQ,CAEpC,EAAM,SAAS,CAEnB,KAAK,QAAQ,OAAO,CACpB,KAAK,aAAe,GACpB,KAAK,MAAQ,KACb,KAAK,cAAgB,KAazB,KAAK,EAAgB,EACrB,CACI,IAAM,EAAQ,KAAK,UAAU,EAAK,CAC/B,IAEI,IAAS,IAAA,GAMR,EAAM,MAAM,CAJZ,EAAM,KAAK,CAAE,YAAa,EAAM,CAAC,EAc7C,KAAK,EACL,CACI,GAAG,EACH,CACI,IAAM,EAAQ,KAAK,QAAQ,IAAI,EAAK,CACjC,GAEC,EAAM,MAAM,MAKhB,IAAI,IAAM,KAAS,KAAK,QAAQ,QAAQ,CAEpC,EAAM,MAAM,CAUxB,MAAM,EACN,CACI,IAAM,EAAQ,KAAK,UAAU,EAAK,CAC/B,GAEC,EAAM,OAAO,CAUrB,UAAU,EAAiB,EAC3B,CACI,GAAG,EACH,CACI,IAAM,EAAQ,KAAK,QAAQ,IAAI,EAAK,CACjC,IAEC,EAAM,OAAS,QAKnB,IAAI,IAAM,KAAS,KAAK,QAAQ,QAAQ,CAEpC,EAAM,OAAS,EAU3B,UAAU,EACV,CAEI,OADc,KAAK,UAAU,EAAK,EACpB,QAAU,EAAA,WAAW,QAMvC,eACA,CACI,OAAO,MAAM,KAAK,KAAK,QAAQ,MAAM,CAAC,CAQ1C,SAAS,EACT,CACI,OAAO,KAAK,QAAQ,IAAI,EAAK,CASjC,cAAc,EAAe,EAC7B,CACI,GAAG,CAAC,KAAK,cAAgB,CAAC,KAAK,cAE3B,MAAU,MAAM,8DAA8D,CAGlF,IAAM,EAAW,KAAK,QAAQ,IAAI,EAAK,CACpC,GAEC,EAAS,SAAS,CAGtB,KAAK,aAAa,EAAM,EAAO,CAQnC,gBAAgB,EAChB,CACI,IAAM,EAAQ,KAAK,QAAQ,IAAI,EAAK,CACjC,IAEC,EAAM,SAAS,CACf,KAAK,QAAQ,OAAO,EAAK,EAWjC,UAAkB,EAClB,CACI,GAAG,EAEC,OAAO,KAAK,QAAQ,IAAI,EAAK,CAGjC,GAAG,KAAK,QAAQ,OAAS,EAErB,OAAO,KAAK,QAAQ,QAAQ,CAAC,MAAM,CAAC,QC7WnC,EAAb,KACA,CACI,cACA,YAA+B,IAAI,IACnC,oBAAuC,IAAI,IAC3C,eAAkC,IAAI,IACtC,cAAiC,IAAI,IACrC,SAEA,YAAY,EAAuB,EACnC,CACI,KAAK,cAAgB,EACrB,KAAK,SAAW,EAOpB,IAAI,cACJ,CACI,OAAO,KAAK,cAUhB,cAAc,EAAe,EAAa,EAC1C,CACI,IAAM,EAAM,GAAI,EAAM,IAAK,IAC3B,KAAK,YAAY,IAAI,EAAK,CAAE,QAAO,CAAC,CAMxC,qBAAqB,EAAa,EAClC,CACI,KAAK,oBAAoB,IAAI,EAAI,CAAE,QAAO,CAAC,CAM/C,QAAQ,EAAgB,EACxB,CACI,IAAM,EAAY,KAAK,eAAe,IAAI,EAAM,EAAI,EAAE,CACtD,EAAU,KAAK,EAAS,CACxB,KAAK,eAAe,IAAI,EAAO,EAAU,CAM7C,OAAO,EAAgB,EACvB,CACI,IAAM,EAAY,KAAK,cAAc,IAAI,EAAM,EAAI,EAAE,CACrD,EAAU,KAAK,EAAS,CACxB,KAAK,cAAc,IAAI,EAAO,EAAU,CAU5C,cAAc,EACd,CACI,IAAM,EAAM,GAAI,KAAK,cAAe,IAAK,IACzC,OAAO,KAAK,YAAY,IAAI,EAAI,EAAI,KAAK,oBAAoB,IAAI,EAAG,CAaxE,WAAW,EACX,CACI,IAAM,EAAM,GAAI,KAAK,cAAe,IAAK,IACrC,EAAgB,KAAK,YAAY,IAAI,EAAI,CAQ7C,GALA,AAEI,IAAgB,KAAK,oBAAoB,IAAI,EAAG,CAGjD,CAAC,EAEA,MAAU,MACN,+BAAgC,KAAK,cAAe,QAAS,EAAI,IACpE,CAIL,GAAG,EAAc,OAAS,CAAC,EAAc,OAAO,CAE5C,MAAU,MACN,mCAAoC,KAAK,cAAe,QAAS,EAAI,IACxE,CAGL,IAAM,EAAgB,KAAK,cAGrB,EAAU,KAAK,cAAc,IAAI,EAAc,CACrD,GAAG,EAEC,IAAI,IAAM,KAAM,EAEZ,GAAI,CAKT,KAAK,UAEJ,KAAK,SAAS,QAAQ,CAClB,KAAM,cAAe,IACrB,QAAS,CAAE,KAAM,EAAe,KAAI,CACvC,CAAyC,CAI9C,KAAK,cAAgB,EAGrB,IAAM,EAAW,KAAK,eAAe,IAAI,EAAG,CAC5C,GAAG,EAEC,IAAI,IAAM,KAAM,EAEZ,GAAI,CAKT,KAAK,UAEJ,KAAK,SAAS,QAAQ,CAClB,KAAM,eAAgB,IACtB,QAAS,CAAE,KAAM,EAAe,KAAI,CACvC,CAAyC,GCvGzC,GAAb,cAGU,CACV,CACI,KAAO,eACP,mBAAgC,EAAE,CAElC,aACA,SAEA,aACA,CACI,OAAO,CAIP,KAAK,aAAe,IAAI,EAAqB,GAAa,CAC1D,KAAK,SAAW,GAUpB,IAAI,cACJ,CACI,OAAO,KAAK,aAAa,aAQ7B,WAAW,EACX,CACI,KAAK,aAAa,WAAW,EAAG,CAG7B,KAAK,SAEH,KAAK,OAAO,MAAkC,KAAK,UAAY,EAChE,KAAK,mBAAmB,EAQhC,aAAa,EAAoB,EACjC,CACI,MAAO,GAWX,OAAO,OACH,EAEJ,CAEI,GAAM,CAAE,eAAc,WAAU,cAAa,uBAAwB,EAErE,MAAM,UAAuC,EAC7C,CACI,aACA,CACI,OAAO,CAEP,KAAK,SAAW,EAChB,KAAK,aAAe,IAAI,EAAqB,EAAa,CAE1D,IAAI,IAAM,KAAM,EAEZ,KAAK,aAAa,cAAc,EAAG,KAAM,EAAG,GAAI,EAAG,MAAM,CAG7D,GAAG,EAEC,IAAI,IAAM,KAAM,EAEZ,KAAK,aAAa,qBAAqB,EAAG,GAAI,EAAG,MAAM,EAMvE,OAAO,IC9JT,EAAS,IAAI,EAAW,kBAAkB,CAMhD,SAAS,EAAuB,EAAsB,EAA8B,EACpF,CACI,IAAM,EAAQ,EAAK,UAAU,CAC7B,IAAI,EAAA,iBAAiB,EAAM,EAAW,CAAE,OAAM,CAAE,EAAM,CAM1D,SAAS,GAAmB,EAAa,EACzC,CACI,IAAM,EAAgB,EAAK,gBAAgB,CAAC,KACvC,GAAU,EAAM,UAAU,gBAAkB,GAChD,CAEE,aAAyB,EAAA,MAExB,EAAc,UAAY,GAC1B,EAAuB,EAAe,EAAA,iBAAiB,KAAM,EAAK,EAIlE,EAAuB,EAAM,EAAA,iBAAiB,KAAM,EAAK,CAmBjE,SAAgB,GAAwB,EACxC,CACI,EAAa,wBAAwB,YAAa,EAAM,EAAO,EAAO,IACtE,CACI,IAAM,EAAe,EAGrB,GAFA,EAAO,MAAM,wBAAyB,EAAK,KAAM,MAAO,EAAc,YAAa,aAAgB,EAAA,KAAM,GAAG,CAEzG,CAAC,EAAM,MAEN,MAAU,MAAM,2CAA2C,CAG/D,GAAG,EAAE,aAAgB,EAAA,OAAS,IAAiB,OAC/C,CACI,EAAO,KAAK,8BAA+B,EAAK,KAAM,uBAAuB,CAC7E,OAGJ,IAAM,EAAQ,EAAK,UAAU,eAAwC,EAErE,OAAQ,EAAR,CAEI,IAAK,MACD,EAAuB,EAAM,EAAA,iBAAiB,IAAK,EAAK,CACxD,MAEJ,IAAK,SACD,EAAuB,EAAM,EAAA,iBAAiB,OAAQ,EAAK,CAC3D,MAEJ,IAAK,OACD,GAAmB,EAAc,EAAK,CACtC,MAEJ,IAAK,OACE,aAAgB,EAAA,eAEf,EAAK,gBAAkB,IAE3B,MAEJ,QACI,EAAO,KAAK,0BAA2B,IAAgB,CAI5D,EAAK,UAAU,oBAAsB,aAAgB,EAAA,cAEjD,EAAK,cAEJ,EAAK,YAAY,eAAiB,KAG5C,CCpGN,IAAM,GAAS,IAAI,EAAW,aAAa,CAM3C,SAAS,GAAe,EACxB,CACI,OAAO,EACF,MAAM,IAAI,CACV,IAAK,GAAQ,WAAW,EAAI,MAAM,CAAC,CAAC,CACpC,OAAQ,GAAQ,CAAC,MAAM,EAAI,CAAC,CAkBrC,SAAgB,GAAmB,EACnC,CACI,EAAa,wBAAwB,iBAAkB,EAAM,EAAO,EAAQ,IAC5E,CACI,IAAM,EAAe,EAErB,GAAG,EAAE,aAAgB,EAAA,MAEjB,OAGJ,IAAM,EAAY,GAAe,EAAa,CAE9C,GAAG,EAAU,SAAW,EACxB,CACI,GAAO,KAAK,gCAAiC,IAAgB,CAC7D,OAGJ,IAAM,EAAW,EAAK,eAAe,GAAK,CACpC,EAAa,KAAK,IAAI,EAAU,OAAQ,EAAS,OAAO,CAG9D,IAAI,IAAI,EAAI,EAAG,EAAI,EAAY,IAE3B,EAAK,YAAY,EAAU,GAAI,EAAS,GAAG,CAI/C,IAAM,EAAe,EAAU,EAAU,OAAS,GAClD,EAAK,YAAY,EAAc,KAAK,EACtC,CClDN,SAAgB,EAAwB,EACxC,CACI,EAAa,wBAAwB,YAAa,EAAM,EAAO,EAAQ,IACvE,CACQ,GAMC,aAAgB,EAAA,OAOpB,EAAyC,WAAa,GACvD,EAAK,UAAY,KACnB,CCpBN,SAAgB,GACZ,EACA,EAEJ,CACI,IAAM,EAAmC,EAAE,CACrC,EAAuB,GAAI,EAAQ,GAEzC,IAAI,GAAM,CAAE,EAAK,KAAW,OAAO,QAAQ,EAAS,CAEhD,GAAG,EAAI,WAAW,EAAqB,CACvC,CACI,IAAM,EAAS,EAAI,MAAM,EAAqB,OAAO,CACrD,EAAO,GAAU,EAIzB,OAAO,EC3BX,IAAM,GAAS,IAAI,EAAW,eAAe,CAkB7C,SAAgB,GAAqB,EACrC,CACI,EAAa,wBAAwB,SAAU,EAAM,EAAO,EAAO,IACnE,CACI,IAAM,EAAY,EAGlB,GAAG,CAFW,EAAM,MAIhB,MAAU,MAAM,wCAAwC,CAI5D,IAAM,EAAS,GACX,EAAK,SACL,QACH,CAEK,EAAU,EAAO,QAAqB,EACtC,EAAQ,EAAO,MAAoB,GACnC,EAAW,EAAO,SAAuB,GACzC,EAAe,EAAO,UAAuB,IAC7C,EAAY,EAAO,UAAwB,GAC3C,EAAW,EAAO,SAAsB,UAExC,EAAe,EAAW,SAAS,aAEzC,GAAG,CAAC,EACJ,CACI,GAAO,KAAK,yCAA0C,EAAK,KAAM,YAAY,CAC7E,OAGJ,EAAa,YACT,GAAI,EAAK,KAAM,QACf,EACA,EACA,CACI,OACA,WACA,SACA,eAAgB,EAChB,mBAAoB,EACpB,qBAAsB,SACzB,CACJ,CACI,KAAM,GACP,CACO,GAEC,EAAM,QAAQ,OAAO,EAAK,EAEhC,CACD,MAAO,GACR,CACI,GAAO,MAAM,oCAAqC,EAAK,KAAM,KAAM,IAAO,EAC5E,EACR,CCxEN,SAAS,GACL,EACA,EACA,EACA,EACA,EAEJ,CACI,IAAM,EAAc,IAAe,QAC7B,EAAA,cAAc,2BACd,EAAA,cAAc,0BAEd,EAAY,WAAY,IAE9B,EAAc,eACV,IAAI,EAAA,kBACA,CACI,QAAS,EACT,UAAW,CAAE,KAAM,EAAY,CAClC,CACA,GACD,CACI,EAAS,QAAQ,CACb,KAAM,EACN,QAAS,CACL,QAAS,EACT,MAAO,EAAI,OACd,CACJ,CAAC,EAET,CACJ,CAML,SAAS,GACL,EACA,EACA,EACA,EAEJ,CACI,GAA2B,EAAe,EAAa,EAAU,EAAY,QAAQ,CACrF,GAA2B,EAAe,EAAa,EAAU,EAAY,OAAO,CAmBxF,SAAgB,GAAuB,EACvC,CACI,EAAa,wBAAwB,WAAY,EAAM,EAAO,EAAO,IACrE,CACI,IAAM,EAAc,EACd,EAAQ,EAAM,MAEpB,GAAG,CAAC,EAEA,MAAU,MAAM,0CAA0C,CAG9D,GAAG,EAAE,aAAgB,EAAA,cAEjB,OAGJ,IAAM,EAAW,EAAW,SAG5B,EAAK,UAAY,GACjB,EAAK,gBAAkB,GAGvB,IAAM,EAAiB,EAAK,eAAmC,IAAI,EAAA,cAAc,EAAM,CACvF,EAAK,cAAgB,EAGrB,IAAI,IAAM,KAAQ,EAAM,OAEjB,IAAS,GAAQ,CAAC,EAAK,UAAU,SAEhC,GAAwB,EAAe,EAAa,EAAU,EAAK,CAK3E,IAAM,EAAW,EAAM,yBAAyB,IAAK,GACrD,CACO,IAAS,GAAQ,CAAC,EAAK,UAAU,SAEhC,GAAwB,EAAe,EAAa,EAAU,EAAK,EAEzE,CAGF,EAAK,oBAAoB,QACzB,CACI,EAAM,yBAAyB,OAAO,EAAS,EACjD,EACJ,CC7GN,SAAgB,EAAuB,EACvC,CACI,EAAa,wBAAwB,WAAY,EAAsB,EAAiB,EAAQ,IAChG,CAEI,GAAG,EAAE,aAAgB,EAAA,cAEjB,OAKJ,IAAI,EAAU,GAEX,OAAO,GAAU,UAEhB,EAAU,EAEN,OAAO,GAAU,SAErB,EAAU,EAAM,aAAa,GAAK,QAAU,IAAU,IAElD,OAAO,GAAU,WAErB,EAAU,IAAU,GAGxB,EAAK,UAAY,GACnB,CCZN,SAAgB,GAA4B,EAC5C,CACI,GAAwB,EAAa,CACrC,GAAmB,EAAa,CAChC,EAAwB,EAAa,CACrC,GAAqB,EAAa,CAClC,GAAuB,EAAa,CACpC,EAAuB,EAAa,CCExC,eAAsB,GAClB,EACA,EACA,EAAwB,EAAE,CAE9B,CAEI,IAAM,EAAS,IAAI,EAAe,EAAQ,UAAY,QAAQ,CACxD,EAAe,EAAO,UAAU,OAAO,CAE7C,EAAa,KAAK,kCAAmC,EAAS,KAAK,CAGnE,EAAa,MAAM,+BAA+B,CAQlD,IAAM,EAAS,MAAM,GAAa,EANU,CACxC,UAAW,GACX,mBAAoB,GACpB,GAAG,EAAQ,cACd,CAEwD,EAAQ,qBAAuB,GAAM,CAE9F,EAAa,MAAM,6BAA6B,CAChD,IAAM,EAAQ,MAAM,IAAW,CACzB,EAAU,GAAkB,EAAM,CAGxC,EAAa,MAAM,wBAAwB,CAC3C,IAAM,EAAW,IAAI,EAAa,EAAO,CAGzC,EAAa,MAAM,2BAA2B,CAC9C,IAAM,EAAc,IAAI,EAAY,EAAQ,EAAQ,EAAO,EAAO,CAGlE,EAAa,MAAM,uBAAuB,CAC1C,IAAM,EAAe,IAAI,GAAa,EAAU,EAAa,EAAO,CAC9D,EAAe,IAAI,GAAiB,EAAU,EAAuB,EAAO,CAC5E,EAAiB,IAAI,GAAe,EAAU,EAAO,CACrD,EAAgB,IAAI,GAAkB,EAAU,EAAQ,EAAe,CACvE,EAAe,IAAI,GAAa,EAAU,EAAO,CACjD,EAAc,IAAI,EAAY,EAAe,EAAc,EAAO,CAClE,EAAc,IAAI,GAAY,EAAQ,EAAU,EAAe,EAAc,EAAc,EAAO,CAGxG,EAAa,MAAM,6BAA6B,CAChD,IAAM,EAAU,IAAI,GAAc,EAAc,CAGhD,EAAa,MAAM,yBAAyB,CAC5C,IAAM,EAAQ,IAAI,GAClB,EAAY,sBAAuB,GAAO,EAAM,KAAK,EAAK,IAAK,CAAC,CAGhE,IAAI,EACA,EAED,EAAQ,eAAiB,EAAQ,cAAc,OAAS,IAEvD,EAAa,MAAM,2BAA2B,CAC9C,EAAc,IAAI,EAAY,EAAO,CACrC,MAAM,EAAY,YAAY,CAE9B,EAAa,MAAM,4BAA4B,CAC/C,EAAe,IAAI,EAAa,EAAa,EAAO,CACpD,MAAM,EAAa,WAAW,EAAQ,cAAc,EAGxD,EAAa,KAAK,qBAAsB,EAAS,4BAA4B,CAG7E,EAAa,MAAM,sBAAsB,CACzC,IAAI,IAAM,KAAU,EAEhB,EAAc,yBAAyB,EAAO,CAKlD,GADA,EAAa,MAAM,wCAAwC,CACxD,EAAQ,SAEP,IAAI,IAAM,KAAW,EAAQ,SAEzB,EAAe,gBAAgB,EAAQ,CAI/C,IAAM,EAAK,IAAI,EACX,EACA,EACA,EACA,EACA,EACA,EACA,EAGA,CACI,cACA,cACH,CAGD,CACI,eACA,iBACA,gBACA,cACA,eACA,eACA,cACA,eACH,CAED,EAAQ,qBAAuB,GAClC,CAQD,OAHA,EAAc,eAAe,EAAG,CAChC,EAAa,eAAe,EAAG,CAExB"}
|