ecspresso 0.12.4 → 0.12.6
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/ecspresso-builder.d.ts +1 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +6 -6
- package/dist/plugins/audio.js +2 -2
- package/dist/plugins/audio.js.map +2 -2
- package/dist/plugins/bounds.js +2 -2
- package/dist/plugins/bounds.js.map +2 -2
- package/dist/plugins/camera.js +2 -2
- package/dist/plugins/camera.js.map +2 -2
- package/dist/plugins/collision.js +2 -2
- package/dist/plugins/collision.js.map +2 -2
- package/dist/plugins/coroutine.js +2 -2
- package/dist/plugins/coroutine.js.map +2 -2
- package/dist/plugins/diagnostics.js +3 -3
- package/dist/plugins/diagnostics.js.map +2 -2
- package/dist/plugins/input.js +2 -2
- package/dist/plugins/input.js.map +2 -2
- package/dist/plugins/particles.js +2 -2
- package/dist/plugins/particles.js.map +2 -2
- package/dist/plugins/physics2D.js +2 -2
- package/dist/plugins/physics2D.js.map +2 -2
- package/dist/plugins/renderers/renderer2D.js +2 -2
- package/dist/plugins/renderers/renderer2D.js.map +2 -2
- package/dist/plugins/spatial-index.js +2 -2
- package/dist/plugins/spatial-index.js.map +2 -2
- package/dist/plugins/sprite-animation.js +2 -2
- package/dist/plugins/sprite-animation.js.map +2 -2
- package/dist/plugins/state-machine.js +2 -2
- package/dist/plugins/state-machine.js.map +2 -2
- package/dist/plugins/timers.js +2 -2
- package/dist/plugins/timers.js.map +2 -2
- package/dist/plugins/transform.js +2 -2
- package/dist/plugins/transform.js.map +2 -2
- package/dist/plugins/tween.js +2 -2
- package/dist/plugins/tween.js.map +2 -2
- package/package.json +4 -4
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * 2D Renderer Plugin for ECSpresso\n *\n * An opt-in PixiJS-based 2D rendering plugin that automates scene graph wiring.\n * Import from 'ecspresso/plugins/renderers/renderer2D'\n *\n * This plugin includes transform propagation automatically.\n */\n\nimport type { Application, ApplicationOptions, Container, Sprite, Graphics } from 'pixi.js';\nimport { definePlugin, type Plugin } from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../../type-utils';\nimport type ECSpresso from 'ecspresso';\nimport {\n\tcreateTransformPlugin,\n\ttype LocalTransform,\n\ttype WorldTransform,\n\ttype TransformComponentTypes,\n\ttype TransformPluginOptions,\n} from 'ecspresso/plugins/transform';\nimport { createBounds, type BoundsRect } from 'ecspresso/plugins/bounds';\nimport type { CameraResourceTypes, CameraState } from 'ecspresso/plugins/camera';\n\n// Re-export transform and bounds types for convenience\nexport type { LocalTransform, WorldTransform, TransformComponentTypes };\nexport type { BoundsRect };\nexport { createTransform, createLocalTransform, createWorldTransform, DEFAULT_LOCAL_TRANSFORM, DEFAULT_WORLD_TRANSFORM } from 'ecspresso/plugins/transform';\n\n// Dynamic import for Application to avoid requiring pixi.js at plugin creation time\n// when using managed mode (init options instead of pre-initialized app)\nasync function createPixiApplication(options: Partial<ApplicationOptions>): Promise<Application> {\n\tconst { Application } = await import('pixi.js');\n\tconst app = new Application();\n\tawait app.init(options);\n\treturn app;\n}\n\n// ==================== Component Types ====================\n\n/**\n * Visibility and alpha component\n */\nexport interface Visible {\n\tvisible: boolean;\n\talpha?: number;\n}\n\n/**\n * Aggregate component types for the 2D renderer plugin.\n * Included automatically via `.withPlugin(createRenderer2DPlugin({ ... }))`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ ... }))\n * .withComponentTypes<{ velocity: { x: number; y: number }; player: true }>()\n * .build();\n * ```\n */\nexport interface Renderer2DComponentTypes extends TransformComponentTypes {\n\tsprite: Sprite;\n\tgraphics: Graphics;\n\tcontainer: Container;\n\tvisible: Visible;\n\t/** Assigns the entity to a named render layer for z-ordering */\n\trenderLayer: string;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Events emitted by the 2D renderer plugin\n */\nexport interface Renderer2DEventTypes {\n\thierarchyChanged: {\n\t\tentityId: number;\n\t\toldParent: number | null;\n\t\tnewParent: number | null;\n\t};\n}\n\n// ==================== Resource Types ====================\n\n/**\n * Resources provided by the 2D renderer plugin\n */\nexport interface Renderer2DResourceTypes {\n\tpixiApp: Application;\n\trootContainer: Container;\n\t/** Screen bounds derived from PixiJS screen dimensions, updated on resize */\n\tbounds: BoundsRect;\n}\n\n// ==================== Scale Mode Types ====================\n\nexport type ScaleMode = 'fit' | 'cover' | 'stretch';\n\nexport interface ScreenScaleOptions {\n\treadonly width: number;\n\treadonly height: number;\n\treadonly mode?: ScaleMode;\n}\n\nexport interface ViewportScale {\n\tscaleX: number;\n\tscaleY: number;\n\toffsetX: number;\n\toffsetY: number;\n\tphysicalWidth: number;\n\tphysicalHeight: number;\n\treadonly designWidth: number;\n\treadonly designHeight: number;\n}\n\nexport interface ViewportScaleResourceTypes {\n\tviewportScale: ViewportScale;\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Common options shared between both initialization modes\n */\ninterface Renderer2DPluginCommonOptions<G extends string = 'renderer2d'> {\n\t/** Optional custom root container (defaults to app.stage) */\n\trootContainer?: Container;\n\t/** System group name (default: 'renderer2d') */\n\tsystemGroup?: G;\n\t/** Priority for render sync system (default: 500) */\n\trenderSyncPriority?: number;\n\t/** Options for the included transform plugin */\n\ttransform?: TransformPluginOptions;\n\t/** When true, wires up pixiApp.ticker to drive ecs.update() automatically (default: true) */\n\tstartLoop?: boolean;\n\t/** Ordered render layer names (back-to-front). Entities with a renderLayer component are placed in the corresponding container. */\n\trenderLayers?: string[];\n\t/** Automatically apply cameraState resource to rootContainer each frame.\n\t * Requires the camera plugin to be installed. (default: false) */\n\tcamera?: boolean;\n\t/** Enforce a logical design resolution with automatic aspect-ratio-aware scaling.\n\t * When set, systems work in design-resolution coordinate space. */\n\tscreenScale?: ScreenScaleOptions;\n}\n\n/**\n * Options when providing a pre-initialized PixiJS Application\n */\nexport interface Renderer2DPluginAppOptions<G extends string = 'renderer2d'> extends Renderer2DPluginCommonOptions<G> {\n\t/** The PixiJS Application instance (already initialized) */\n\tapp: Application;\n\tinit?: never;\n\tcontainer?: never;\n}\n\n/**\n * Options when letting the plugin create and manage the PixiJS Application\n */\nexport interface Renderer2DPluginManagedOptions<G extends string = 'renderer2d'> extends Renderer2DPluginCommonOptions<G> {\n\tapp?: never;\n\t/** PixiJS ApplicationOptions - plugin will create and initialize the Application */\n\tinit: Partial<ApplicationOptions>;\n\t/** Container element to append the canvas to, or CSS selector string */\n\tcontainer?: HTMLElement | string;\n}\n\n/**\n * Configuration options for the 2D renderer plugin.\n *\n * Supports two modes:\n * 1. **Pre-initialized**: Pass an already-initialized Application via `app`\n * 2. **Managed**: Pass `init` options and the plugin creates the Application during `ecs.initialize()`\n *\n * This plugin includes transform propagation automatically - no need to add createTransformPlugin() separately.\n *\n * @example Pre-initialized mode (full control)\n * ```typescript\n * const app = new Application();\n * await app.init({ resizeTo: window });\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ app }))\n * .withComponentTypes<{ player: true }>()\n * .build();\n * ```\n *\n * @example Managed mode (convenience)\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({\n * init: { background: '#1099bb', resizeTo: window },\n * container: document.body,\n * }))\n * .withComponentTypes<{ player: true }>()\n * .build();\n * await ecs.initialize(); // Application created here\n * ```\n */\nexport type Renderer2DPluginOptions<G extends string = 'renderer2d'> = Renderer2DPluginAppOptions<G> | Renderer2DPluginManagedOptions<G>;\n\n// ==================== Helper Utilities ====================\n\ninterface PositionOption {\n\tx?: number;\n\ty?: number;\n}\n\ninterface TransformOptions {\n\trotation?: number;\n\tscale?: number | { x: number; y: number };\n\tvisible?: boolean;\n\talpha?: number;\n}\n\nfunction createLocalTransformInternal(\n\tposition?: PositionOption,\n\toptions?: TransformOptions\n): LocalTransform {\n\tconst scaleValue = options?.scale;\n\tconst scaleX = typeof scaleValue === 'number'\n\t\t? scaleValue\n\t\t: scaleValue?.x ?? 1;\n\tconst scaleY = typeof scaleValue === 'number'\n\t\t? scaleValue\n\t\t: scaleValue?.y ?? 1;\n\n\treturn {\n\t\tx: position?.x ?? 0,\n\t\ty: position?.y ?? 0,\n\t\trotation: options?.rotation ?? 0,\n\t\tscaleX,\n\t\tscaleY,\n\t};\n}\n\nfunction createWorldTransformInternal(\n\tposition?: PositionOption,\n\toptions?: TransformOptions\n): WorldTransform {\n\tconst scaleValue = options?.scale;\n\tconst scaleX = typeof scaleValue === 'number'\n\t\t? scaleValue\n\t\t: scaleValue?.x ?? 1;\n\tconst scaleY = typeof scaleValue === 'number'\n\t\t? scaleValue\n\t\t: scaleValue?.y ?? 1;\n\n\treturn {\n\t\tx: position?.x ?? 0,\n\t\ty: position?.y ?? 0,\n\t\trotation: options?.rotation ?? 0,\n\t\tscaleX,\n\t\tscaleY,\n\t};\n}\n\nfunction createVisibleComponent(options?: TransformOptions): Visible {\n\treturn {\n\t\tvisible: options?.visible ?? true,\n\t\talpha: options?.alpha,\n\t};\n}\n\n/**\n * Create components for a sprite entity.\n * Returns an object suitable for spreading into spawn().\n *\n * @example\n * ```typescript\n * const player = ecs.spawn({\n * ...createSpriteComponents(new Sprite(texture), { x: 100, y: 100 }),\n * velocity: { x: 0, y: 0 },\n * });\n * ```\n */\nexport function createSpriteComponents(\n\tsprite: Sprite,\n\tposition?: PositionOption,\n\toptions?: TransformOptions & { anchor?: { x: number; y: number } }\n): Pick<Renderer2DComponentTypes, 'sprite' | 'localTransform' | 'worldTransform' | 'visible'> {\n\tif (options?.anchor) {\n\t\tsprite.anchor.set(options.anchor.x, options.anchor.y);\n\t}\n\treturn {\n\t\tsprite,\n\t\tlocalTransform: createLocalTransformInternal(position, options),\n\t\tworldTransform: createWorldTransformInternal(position, options),\n\t\tvisible: createVisibleComponent(options),\n\t};\n}\n\n/**\n * Create components for a graphics entity.\n * Returns an object suitable for spreading into spawn().\n *\n * @example\n * ```typescript\n * const rect = ecs.spawn({\n * ...createGraphicsComponents(graphics, { x: 50, y: 50 }),\n * });\n * ```\n */\nexport function createGraphicsComponents(\n\tgraphics: Graphics,\n\tposition?: PositionOption,\n\toptions?: TransformOptions\n): Pick<Renderer2DComponentTypes, 'graphics' | 'localTransform' | 'worldTransform' | 'visible'> {\n\treturn {\n\t\tgraphics,\n\t\tlocalTransform: createLocalTransformInternal(position, options),\n\t\tworldTransform: createWorldTransformInternal(position, options),\n\t\tvisible: createVisibleComponent(options),\n\t};\n}\n\n/**\n * Create components for a container entity.\n * Returns an object suitable for spreading into spawn().\n *\n * @example\n * ```typescript\n * const group = ecs.spawn({\n * ...createContainerComponents(new Container(), { x: 0, y: 0 }),\n * });\n * ```\n */\nexport function createContainerComponents(\n\tcontainer: Container,\n\tposition?: PositionOption,\n\toptions?: TransformOptions\n): Pick<Renderer2DComponentTypes, 'container' | 'localTransform' | 'worldTransform' | 'visible'> {\n\treturn {\n\t\tcontainer,\n\t\tlocalTransform: createLocalTransformInternal(position, options),\n\t\tworldTransform: createWorldTransformInternal(position, options),\n\t\tvisible: createVisibleComponent(options),\n\t};\n}\n\n// ==================== Viewport Scale Utilities ====================\n\nconst scaleModeStrategy: Record<ScaleMode, (ratioX: number, ratioY: number) => { scaleX: number; scaleY: number }> = {\n\tfit: (ratioX, ratioY) => {\n\t\tconst s = Math.min(ratioX, ratioY);\n\t\treturn { scaleX: s, scaleY: s };\n\t},\n\tcover: (ratioX, ratioY) => {\n\t\tconst s = Math.max(ratioX, ratioY);\n\t\treturn { scaleX: s, scaleY: s };\n\t},\n\tstretch: (ratioX, ratioY) => ({ scaleX: ratioX, scaleY: ratioY }),\n};\n\nexport function computeViewportScale(\n\tphysicalW: number,\n\tphysicalH: number,\n\tdesignW: number,\n\tdesignH: number,\n\tmode: ScaleMode,\n): ViewportScale {\n\tconst ratioX = physicalW / designW;\n\tconst ratioY = physicalH / designH;\n\tconst { scaleX, scaleY } = scaleModeStrategy[mode](ratioX, ratioY);\n\n\treturn {\n\t\tscaleX,\n\t\tscaleY,\n\t\toffsetX: (physicalW - designW * scaleX) / 2,\n\t\toffsetY: (physicalH - designH * scaleY) / 2,\n\t\tphysicalWidth: physicalW,\n\t\tphysicalHeight: physicalH,\n\t\tdesignWidth: designW,\n\t\tdesignHeight: designH,\n\t};\n}\n\n/**\n * Convert physical canvas pixel coordinates to design-resolution (logical) coordinates.\n * Compose with camera `screenToWorld()` for full physical→world conversion.\n */\nexport function physicalToLogical(\n\tphysicalX: number,\n\tphysicalY: number,\n\tviewport: ViewportScale,\n): { x: number; y: number } {\n\treturn {\n\t\tx: (physicalX - viewport.offsetX) / viewport.scaleX,\n\t\ty: (physicalY - viewport.offsetY) / viewport.scaleY,\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a 2D rendering plugin for ECSpresso.\n *\n * This plugin provides:\n * - Transform propagation (localTransform -> worldTransform)\n * - Render sync system (updates PixiJS objects from ECS components)\n * - Scene graph management (mirrors ECS hierarchy in PixiJS scene graph)\n *\n * @example Pre-initialized mode\n * ```typescript\n * const app = new Application();\n * await app.init({ resizeTo: window });\n *\n * const ecs = ECSpresso.create<GameComponents, {}, {}>()\n * .withPlugin(createRenderer2DPlugin({ app }))\n * .build();\n * ```\n *\n * @example Managed mode\n * ```typescript\n * const ecs = ECSpresso.create<GameComponents, {}, {}>()\n * .withPlugin(createRenderer2DPlugin({\n * init: { background: '#1099bb', resizeTo: window },\n * container: document.body,\n * }))\n * .build();\n * await ecs.initialize();\n * ```\n */\ntype Renderer2DLabels = 'renderer2d-sync' | 'renderer2d-scene-graph' | 'renderer2d-camera-sync' | 'transform-propagation';\ntype Renderer2DReactiveQueryNames = 'renderer2d-sprites' | 'renderer2d-graphics' | 'renderer2d-containers';\n\nexport function createRenderer2DPlugin<G extends string = 'renderer2d'>(\n\toptions: Renderer2DPluginOptions<G> & { screenScale: ScreenScaleOptions; camera: true }\n): Plugin<WorldConfigFrom<Renderer2DComponentTypes, Renderer2DEventTypes, Renderer2DResourceTypes & ViewportScaleResourceTypes & CameraResourceTypes>, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames>;\nexport function createRenderer2DPlugin<G extends string = 'renderer2d'>(\n\toptions: Renderer2DPluginOptions<G> & { screenScale: ScreenScaleOptions }\n): Plugin<WorldConfigFrom<Renderer2DComponentTypes, Renderer2DEventTypes, Renderer2DResourceTypes & ViewportScaleResourceTypes>, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames>;\nexport function createRenderer2DPlugin<G extends string = 'renderer2d'>(\n\toptions: Renderer2DPluginOptions<G> & { camera: true }\n): Plugin<WorldConfigFrom<Renderer2DComponentTypes, Renderer2DEventTypes, Renderer2DResourceTypes & CameraResourceTypes>, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames>;\nexport function createRenderer2DPlugin<G extends string = 'renderer2d'>(\n\toptions: Renderer2DPluginOptions<G>\n): Plugin<WorldConfigFrom<Renderer2DComponentTypes, Renderer2DEventTypes, Renderer2DResourceTypes>, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames>;\nexport function createRenderer2DPlugin<G extends string = 'renderer2d'>(\n\toptions: Renderer2DPluginOptions<G> & { camera?: boolean; screenScale?: ScreenScaleOptions }\n): Plugin<WorldConfigFrom<Renderer2DComponentTypes, Renderer2DEventTypes, Renderer2DResourceTypes>, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames>\n| Plugin<WorldConfigFrom<Renderer2DComponentTypes, Renderer2DEventTypes, Renderer2DResourceTypes & CameraResourceTypes>, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames>\n| Plugin<WorldConfigFrom<Renderer2DComponentTypes, Renderer2DEventTypes, Renderer2DResourceTypes & ViewportScaleResourceTypes>, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames>\n| Plugin<WorldConfigFrom<Renderer2DComponentTypes, Renderer2DEventTypes, Renderer2DResourceTypes & ViewportScaleResourceTypes & CameraResourceTypes>, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames> {\n\tconst {\n\t\trootContainer: customRootContainer,\n\t\tsystemGroup = 'renderer2d',\n\t\trenderSyncPriority = 500,\n\t\ttransform: transformOptions,\n\t\tstartLoop = true,\n\t\trenderLayers = [],\n\t\tcamera = false,\n\t\tscreenScale,\n\t} = options;\n\n\tconst hasScreenScale = screenScale !== undefined;\n\tconst designWidth = screenScale?.width ?? 0;\n\tconst designHeight = screenScale?.height ?? 0;\n\tconst screenScaleMode: ScaleMode = screenScale?.mode ?? 'fit';\n\n\t// Entity ID -> PixiJS Container mapping for scene graph management\n\tconst entityToPixiObject = new Map<number, Container>();\n\n\t// Render layer name -> PixiJS Container mapping\n\tconst layerContainers = new Map<string, Container>();\n\n\t// Container constructor captured during initialization via dynamic import\n\t// Used by getOrCreateLayerContainer for lazy layer creation\n\tlet createLayerContainer: (label: string) => Container = () => {\n\t\tthrow new Error('renderer2D: createLayerContainer called before initialization');\n\t};\n\n\t// Helper to get or create a render layer container\n\tfunction getOrCreateLayerContainer(\n\t\tlayerName: string,\n\t\trootCont: Container\n\t): Container {\n\t\tconst existing = layerContainers.get(layerName);\n\t\tif (existing) return existing;\n\n\t\t// Lazy-create for undeclared layers, appended to end\n\t\tconst cont = createLayerContainer(`layer:${layerName}`);\n\t\tlayerContainers.set(layerName, cont);\n\t\trootCont.addChild(cont);\n\t\treturn cont;\n\t}\n\n\t// Helper to resolve the target container for an entity.\n\t// Scene graph stays flat (rootContainer or render layer) because the render\n\t// sync positions objects using absolute worldTransform. Nesting under a\n\t// parent's display object would double-apply the parent's transform.\n\ttype PluginResourceTypes = Renderer2DResourceTypes & ViewportScaleResourceTypes;\n\ttype PluginECS = ECSpresso<WorldConfigFrom<Renderer2DComponentTypes, Renderer2DEventTypes, PluginResourceTypes>>;\n\n\tfunction resolveTargetContainer(\n\t\tentityId: number,\n\t\tecs: PluginECS\n\t): Container {\n\t\tconst rootCont = ecs.getResource('rootContainer');\n\n\t\t// 1. Check render layer component\n\t\tconst layerName = ecs.getComponent(entityId, 'renderLayer');\n\t\tif (layerName) return getOrCreateLayerContainer(layerName, rootCont);\n\n\t\t// 2. Fall back to root container\n\t\treturn rootCont;\n\t}\n\n\t// Helper to add a PixiJS object to the scene graph\n\tfunction addToSceneGraph(\n\t\tentityId: number,\n\t\tpixiObject: Container,\n\t\tecs: PluginECS\n\t): void {\n\t\tconst targetContainer = resolveTargetContainer(entityId, ecs);\n\n\t\t// Only add if not already a child\n\t\tif (pixiObject.parent !== targetContainer) {\n\t\t\ttargetContainer.addChild(pixiObject);\n\t\t}\n\t}\n\n\t// Helper to update parent in scene graph\n\tfunction updateSceneGraphParent(\n\t\tentityId: number,\n\t\tecs: PluginECS\n\t): void {\n\t\tconst pixiObject = entityToPixiObject.get(entityId);\n\t\tif (!pixiObject) return;\n\n\t\tconst targetContainer = resolveTargetContainer(entityId, ecs);\n\n\t\tif (pixiObject.parent !== targetContainer) {\n\t\t\tpixiObject.removeFromParent();\n\t\t\ttargetContainer.addChild(pixiObject);\n\t\t}\n\t}\n\n\t// Determine mode and set up resource registration closures\n\tconst isManaged = 'init' in options && options.init !== undefined;\n\n\treturn definePlugin<WorldConfigFrom<Renderer2DComponentTypes, Renderer2DEventTypes, PluginResourceTypes>, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames>({\n\t\tid: 'renderer2d',\n\t\tinstall(world) {\n\t\t\t// Install transform plugin (deduplicates if already installed)\n\t\t\tworld.installPlugin(createTransformPlugin(transformOptions));\n\n\t\t\t// Register resources based on mode\n\t\t\tif (isManaged) {\n\t\t\t\tconst initOptions = (options as Renderer2DPluginManagedOptions<G>).init;\n\t\t\t\tconst containerOption = (options as Renderer2DPluginManagedOptions<G>).container;\n\n\t\t\t\tworld.addResource('pixiApp', async () => {\n\t\t\t\t\tconst app = await createPixiApplication(initOptions);\n\n\t\t\t\t\tif (containerOption) {\n\t\t\t\t\t\tconst containerEl = typeof containerOption === 'string'\n\t\t\t\t\t\t\t? document.querySelector(containerOption)\n\t\t\t\t\t\t\t: containerOption;\n\n\t\t\t\t\t\tif (containerEl) {\n\t\t\t\t\t\t\tcontainerEl.appendChild(app.canvas);\n\t\t\t\t\t\t} else if (typeof containerOption === 'string') {\n\t\t\t\t\t\t\tconsole.warn(`Renderer2D plugin: container selector \"${containerOption}\" not found`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn app;\n\t\t\t\t});\n\n\t\t\t\tworld.addResource('rootContainer', {\n\t\t\t\t\tdependsOn: ['pixiApp'],\n\t\t\t\t\tfactory: (ecs) => customRootContainer ?? ecs.getResource('pixiApp').stage,\n\t\t\t\t});\n\n\t\t\t\tworld.addResource('bounds', {\n\t\t\t\t\tdependsOn: ['pixiApp'],\n\t\t\t\t\tfactory: (ecs) => {\n\t\t\t\t\t\tif (hasScreenScale) return createBounds(designWidth, designHeight);\n\t\t\t\t\t\tconst pixiApp = ecs.getResource('pixiApp');\n\t\t\t\t\t\treturn createBounds(pixiApp.screen.width, pixiApp.screen.height);\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tif (hasScreenScale) {\n\t\t\t\t\tworld.addResource('viewportScale', {\n\t\t\t\t\t\tdependsOn: ['pixiApp'],\n\t\t\t\t\t\tfactory: (ecs) => {\n\t\t\t\t\t\t\tconst pixiApp = ecs.getResource('pixiApp');\n\t\t\t\t\t\t\treturn computeViewportScale(pixiApp.screen.width, pixiApp.screen.height, designWidth, designHeight, screenScaleMode);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst app = (options as Renderer2DPluginAppOptions<G>).app;\n\t\t\t\tworld.addResource('pixiApp', app);\n\t\t\t\tworld.addResource('rootContainer', customRootContainer ?? app.stage);\n\t\t\t\tworld.addResource('bounds', hasScreenScale\n\t\t\t\t\t? createBounds(designWidth, designHeight)\n\t\t\t\t\t: createBounds(app.screen.width, app.screen.height));\n\n\t\t\t\tif (hasScreenScale) {\n\t\t\t\t\tworld.addResource('viewportScale',\n\t\t\t\t\t\tcomputeViewportScale(app.screen.width, app.screen.height, designWidth, designHeight, screenScaleMode));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Register dispose callbacks for display object components\n\t\t\tworld.registerDispose('sprite', ({ value: sprite }) => {\n\t\t\t\tsprite.removeFromParent();\n\t\t\t});\n\t\t\tworld.registerDispose('graphics', ({ value: graphics }) => {\n\t\t\t\tgraphics.removeFromParent();\n\t\t\t});\n\t\t\tworld.registerDispose('container', ({ value: container }) => {\n\t\t\t\tcontainer.removeFromParent();\n\t\t\t});\n\n\t\t\t// Display objects require localTransform and visible\n\t\t\tworld.registerRequired('sprite', 'localTransform', () => createLocalTransformInternal());\n\t\t\tworld.registerRequired('sprite', 'visible', () => createVisibleComponent());\n\t\t\tworld.registerRequired('graphics', 'localTransform', () => createLocalTransformInternal());\n\t\t\tworld.registerRequired('graphics', 'visible', () => createVisibleComponent());\n\t\t\tworld.registerRequired('container', 'localTransform', () => createLocalTransformInternal());\n\t\t\tworld.registerRequired('container', 'visible', () => createVisibleComponent());\n\n\t\t\t// ==================== Render Sync System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('renderer2d-sync')\n\t\t\t\t.setPriority(renderSyncPriority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('sprites', {\n\t\t\t\t\twith: ['sprite', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t})\n\t\t\t\t.addQuery('graphics', {\n\t\t\t\t\twith: ['graphics', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t})\n\t\t\t\t.addQuery('containers', {\n\t\t\t\t\twith: ['container', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.sprites) {\n\t\t\t\t\t\tconst { sprite, worldTransform } = entity.components;\n\n\t\t\t\t\t\tsprite.position.set(worldTransform.x, worldTransform.y);\n\t\t\t\t\t\tsprite.rotation = worldTransform.rotation;\n\t\t\t\t\t\tsprite.scale.set(worldTransform.scaleX, worldTransform.scaleY);\n\n\t\t\t\t\t\tconst visibleComp = ecs.getComponent(entity.id, 'visible');\n\t\t\t\t\t\tif (visibleComp) {\n\t\t\t\t\t\t\tsprite.visible = visibleComp.visible;\n\t\t\t\t\t\t\tif (visibleComp.alpha !== undefined) {\n\t\t\t\t\t\t\t\tsprite.alpha = visibleComp.alpha;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.graphics) {\n\t\t\t\t\t\tconst { graphics, worldTransform } = entity.components;\n\n\t\t\t\t\t\tgraphics.position.set(worldTransform.x, worldTransform.y);\n\t\t\t\t\t\tgraphics.rotation = worldTransform.rotation;\n\t\t\t\t\t\tgraphics.scale.set(worldTransform.scaleX, worldTransform.scaleY);\n\n\t\t\t\t\t\tconst visibleComp = ecs.getComponent(entity.id, 'visible');\n\t\t\t\t\t\tif (visibleComp) {\n\t\t\t\t\t\t\tgraphics.visible = visibleComp.visible;\n\t\t\t\t\t\t\tif (visibleComp.alpha !== undefined) {\n\t\t\t\t\t\t\t\tgraphics.alpha = visibleComp.alpha;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.containers) {\n\t\t\t\t\t\tconst { container, worldTransform } = entity.components;\n\n\t\t\t\t\t\tcontainer.position.set(worldTransform.x, worldTransform.y);\n\t\t\t\t\t\tcontainer.rotation = worldTransform.rotation;\n\t\t\t\t\t\tcontainer.scale.set(worldTransform.scaleX, worldTransform.scaleY);\n\n\t\t\t\t\t\tconst visibleComp = ecs.getComponent(entity.id, 'visible');\n\t\t\t\t\t\tif (visibleComp) {\n\t\t\t\t\t\t\tcontainer.visible = visibleComp.visible;\n\t\t\t\t\t\t\tif (visibleComp.alpha !== undefined) {\n\t\t\t\t\t\t\t\tcontainer.alpha = visibleComp.alpha;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Scene Graph Manager System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('renderer2d-scene-graph')\n\t\t\t\t.setPriority(9999)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs) => {\n\t\t\t\t\tconst pixiApp = ecs.getResource('pixiApp');\n\t\t\t\t\tlet rootCont = ecs.getResource('rootContainer');\n\n\t\t\t\t\tconst { Container: ContainerClass } = await import('pixi.js');\n\t\t\t\t\tcreateLayerContainer = (label: string) => {\n\t\t\t\t\t\tconst cont = new ContainerClass();\n\t\t\t\t\t\tcont.label = label;\n\t\t\t\t\t\treturn cont;\n\t\t\t\t\t};\n\n\t\t\t\t\tlet viewportContainer: Container | undefined;\n\t\t\t\t\tif (hasScreenScale) {\n\t\t\t\t\t\tviewportContainer = new ContainerClass();\n\t\t\t\t\t\tviewportContainer.label = 'viewportContainer';\n\n\t\t\t\t\t\tconst vs = ecs.tryGetResource('viewportScale');\n\t\t\t\t\t\tif (!vs) throw new Error('renderer2D: viewportScale resource not found');\n\t\t\t\t\t\tviewportContainer.position.set(vs.offsetX, vs.offsetY);\n\t\t\t\t\t\tviewportContainer.scale.set(vs.scaleX, vs.scaleY);\n\n\t\t\t\t\t\tconst newRoot = new ContainerClass();\n\t\t\t\t\t\tnewRoot.label = 'rootContainer';\n\n\t\t\t\t\t\tpixiApp.stage.addChild(viewportContainer);\n\t\t\t\t\t\tviewportContainer.addChild(newRoot);\n\n\t\t\t\t\t\tecs.updateResource('rootContainer', () => newRoot);\n\t\t\t\t\t\trootCont = newRoot;\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const layerName of renderLayers) {\n\t\t\t\t\t\tconst cont = createLayerContainer(`layer:${layerName}`);\n\t\t\t\t\t\tlayerContainers.set(layerName, cont);\n\t\t\t\t\t\trootCont.addChild(cont);\n\t\t\t\t\t}\n\n\t\t\t\t\tecs.addReactiveQuery('renderer2d-sprites', {\n\t\t\t\t\t\twith: ['sprite'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\tconst pixiObject = entity.components.sprite;\n\t\t\t\t\t\t\tentityToPixiObject.set(entity.id, pixiObject);\n\t\t\t\t\t\t\taddToSceneGraph(entity.id, pixiObject, ecs);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tentityToPixiObject.delete(entityId);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\tecs.addReactiveQuery('renderer2d-graphics', {\n\t\t\t\t\t\twith: ['graphics'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\tconst pixiObject = entity.components.graphics;\n\t\t\t\t\t\t\tentityToPixiObject.set(entity.id, pixiObject);\n\t\t\t\t\t\t\taddToSceneGraph(entity.id, pixiObject, ecs);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tentityToPixiObject.delete(entityId);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\tecs.addReactiveQuery('renderer2d-containers', {\n\t\t\t\t\t\twith: ['container'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\tconst pixiObject = entity.components.container;\n\t\t\t\t\t\t\tentityToPixiObject.set(entity.id, pixiObject);\n\t\t\t\t\t\t\taddToSceneGraph(entity.id, pixiObject, ecs);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tentityToPixiObject.delete(entityId);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\tecs.on('hierarchyChanged', ({ entityId }) => {\n\t\t\t\t\t\tupdateSceneGraphParent(entityId, ecs);\n\t\t\t\t\t});\n\n\t\t\t\t\tecs.onComponentAdded('renderLayer', ({ entity }) => {\n\t\t\t\t\t\tupdateSceneGraphParent(entity.id, ecs);\n\t\t\t\t\t});\n\n\t\t\t\t\tecs.onComponentRemoved('renderLayer', ({ entity }) => {\n\t\t\t\t\t\tupdateSceneGraphParent(entity.id, ecs);\n\t\t\t\t\t});\n\n\t\t\t\t\tif (camera) {\n\t\t\t\t\t\tconst cameraState = ecs.tryGetResource<CameraState>('cameraState');\n\t\t\t\t\t\tif (!cameraState) throw new Error('renderer2D: cameraState resource not found');\n\t\t\t\t\t\tcameraState.viewportWidth = hasScreenScale ? designWidth : pixiApp.screen.width;\n\t\t\t\t\t\tcameraState.viewportHeight = hasScreenScale ? designHeight : pixiApp.screen.height;\n\t\t\t\t\t}\n\n\t\t\t\t\tpixiApp.renderer.on('resize', (width: number, height: number) => {\n\t\t\t\t\t\tif (hasScreenScale) {\n\t\t\t\t\t\t\tconst vs = computeViewportScale(width, height, designWidth, designHeight, screenScaleMode);\n\t\t\t\t\t\t\tconst vpResource = ecs.tryGetResource('viewportScale');\n\t\t\t\t\t\t\tif (!vpResource) throw new Error('renderer2D: viewportScale resource not found');\n\t\t\t\t\t\t\tvpResource.scaleX = vs.scaleX;\n\t\t\t\t\t\t\tvpResource.scaleY = vs.scaleY;\n\t\t\t\t\t\t\tvpResource.offsetX = vs.offsetX;\n\t\t\t\t\t\t\tvpResource.offsetY = vs.offsetY;\n\t\t\t\t\t\t\tvpResource.physicalWidth = width;\n\t\t\t\t\t\t\tvpResource.physicalHeight = height;\n\n\t\t\t\t\t\t\tif (viewportContainer) {\n\t\t\t\t\t\t\t\tviewportContainer.position.set(vs.offsetX, vs.offsetY);\n\t\t\t\t\t\t\t\tviewportContainer.scale.set(vs.scaleX, vs.scaleY);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tconst bounds = ecs.getResource('bounds');\n\t\t\t\t\t\t\tbounds.width = width;\n\t\t\t\t\t\t\tbounds.height = height;\n\n\t\t\t\t\t\t\tif (camera) {\n\t\t\t\t\t\t\t\tconst cameraState = ecs.tryGetResource<CameraState>('cameraState');\n\t\t\t\t\t\t\t\tif (!cameraState) throw new Error('renderer2D: cameraState resource not found');\n\t\t\t\t\t\t\t\tcameraState.viewportWidth = width;\n\t\t\t\t\t\t\t\tcameraState.viewportHeight = height;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\tif (startLoop) {\n\t\t\t\t\t\tpixiApp.ticker.add((ticker) => {\n\t\t\t\t\t\t\tecs.update(ticker.deltaMS / 1_000);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Camera Sync System (opt-in) ====================\n\t\t\tif (camera) {\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('renderer2d-camera-sync')\n\t\t\t\t\t.setPriority(900)\n\t\t\t\t\t.inPhase('render')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\t\tconst state = ecs.tryGetResource<CameraState>('cameraState');\n\t\t\t\t\t\tif (!state) throw new Error('renderer2D: cameraState resource not found');\n\t\t\t\t\t\tconst root = ecs.getResource('rootContainer');\n\t\t\t\t\t\tconst [centerW, centerH] = hasScreenScale\n\t\t\t\t\t\t\t? [designWidth, designHeight]\n\t\t\t\t\t\t\t: [ecs.getResource('pixiApp').screen.width, ecs.getResource('pixiApp').screen.height];\n\n\t\t\t\t\t\troot.position.set(\n\t\t\t\t\t\t\tcenterW / 2 - (state.x + state.shakeOffsetX) * state.zoom,\n\t\t\t\t\t\t\tcenterH / 2 - (state.y + state.shakeOffsetY) * state.zoom,\n\t\t\t\t\t\t);\n\t\t\t\t\t\troot.scale.set(state.zoom);\n\t\t\t\t\t\troot.rotation = -(state.rotation + state.shakeRotation);\n\t\t\t\t\t});\n\t\t\t}\n\t\t},\n\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "BFE63C0BDE8F323C64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var S=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(B,D)=>(typeof require<"u"?require:B)[D]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as Q}from"ecspresso";function R(j,B){return j*73856093^B*19349663}function _(j){return{cellSize:j,invCellSize:1/j,cells:new Map,entries:new Map}}function v(j){j.cells.clear(),j.entries.clear()}function H(j,B,D,F,K,L){j.entries.set(B,{entityId:B,x:D,y:F,halfW:K,halfH:L});let N=j.invCellSize,U=Math.floor((D-K)*N),J=Math.floor((D+K)*N),$=Math.floor((F-L)*N),A=Math.floor((F+L)*N);for(let O=U;O<=J;O++)for(let M=$;M<=A;M++){let V=R(O,M),T=j.cells.get(V);if(T)T.push(B);else j.cells.set(V,[B])}}function G(j,B,D,F,K,L){let N=j.invCellSize,U=Math.floor(B*N),J=Math.floor(F*N),$=Math.floor(D*N),A=Math.floor(K*N);for(let O=U;O<=J;O++)for(let M=$;M<=A;M++){let V=j.cells.get(R(O,M));if(!V)continue;for(let T=0;T<V.length;T++){let Z=V[T];if(Z!==void 0)L.add(Z)}}}var W=new Set;function k(j,B,D,F,K){let L=W;L.clear(),G(j,B-F,D-F,B+F,D+F,L);let N=F*F;for(let U of L){let J=j.entries.get(U);if(!J)continue;let $=Math.max(J.x-J.halfW,Math.min(B,J.x+J.halfW)),A=Math.max(J.y-J.halfH,Math.min(D,J.y+J.halfH)),O=B-$,M=D-A;if(O*O+M*M<=N)K.add(U)}}var E=new Set;function p(j){return{grid:j,queryRect(B,D,F,K){return E.clear(),G(j,B,D,F,K,E),Array.from(E)},queryRectInto(B,D,F,K,L){G(j,B,D,F,K,L)},queryRadius(B,D,F){return E.clear(),k(j,B,D,F,E),Array.from(E)},queryRadiusInto(B,D,F,K){k(j,B,D,F,K)},getEntry(B){return j.entries.get(B)}}}function b(j){let{cellSize:B=64,systemGroup:D="spatialIndex",priority:F=2000,phases:K=["fixedUpdate","postUpdate"]}=j??{},L=_(B),N=p(L);return Q({id:"spatialIndex",install(U){U.addResource("spatialIndex",N);for(let J of K){let $=J==="fixedUpdate"?"localTransform":"worldTransform";U.addSystem(`spatial-index-rebuild-${J}`).setPriority(F).inPhase(J).inGroup(D).addQuery("transforms",{with:[$]}).setProcess(({queries:A,ecs:O})=>{v(L);for(let M of A.transforms){let V=M.components[$],T=O.getComponent(M.id,"aabbCollider"),Z=O.getComponent(M.id,"circleCollider");if(!T&&!Z)continue;let{x:w,y:z}=V,P=0,q=0;if(T)w+=T.offsetX??0,z+=T.offsetY??0,P=T.width/2,q=T.height/2;if(Z)w+=Z.offsetX??0,z+=Z.offsetY??0,P=Math.max(P,Z.radius),q=Math.max(q,Z.radius);H(L,M.id,w,z,P,q)}})}}})}export{b as createSpatialIndexPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=926438F008C04F1E64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=spatial-index.js.map
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"/**\n * Spatial Index Plugin for ECSpresso\n *\n * Provides a uniform-grid spatial hash for broadphase collision detection\n * and proximity queries. Replaces O(n²) brute-force with O(n·d) where\n * d = local density.\n *\n * Standalone usage: queryRect / queryRadius for proximity queries.\n * Automatic acceleration: collision and physics2D plugins detect the\n * spatialIndex resource at runtime and use it for broadphase when present.\n */\n\nimport { definePlugin, type Plugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../type-utils';\nimport type { TransformComponentTypes } from './transform';\nimport type { CollisionComponentTypes } from './collision';\nimport {\n\ttype SpatialEntry,\n\ttype SpatialHashGrid,\n\ttype SpatialIndex,\n\tcreateGrid,\n\tclearGrid,\n\tinsertEntity,\n\tgridQueryRect,\n\tgridQueryRadius,\n} from '../utils/spatial-hash';\n\n// Module-scoped reusable set to reduce GC pressure\nconst _reusableQuerySet = new Set<number>();\n\n// ==================== Resource API ====================\n\nexport interface SpatialIndexResourceTypes {\n\tspatialIndex: SpatialIndex;\n}\n\nfunction createSpatialIndexResource(grid: SpatialHashGrid): SpatialIndex {\n\treturn {\n\t\tgrid,\n\t\tqueryRect(minX: number, minY: number, maxX: number, maxY: number): number[] {\n\t\t\t_reusableQuerySet.clear();\n\t\t\tgridQueryRect(grid, minX, minY, maxX, maxY, _reusableQuerySet);\n\t\t\treturn Array.from(_reusableQuerySet);\n\t\t},\n\t\tqueryRectInto(minX: number, minY: number, maxX: number, maxY: number, result: Set<number>): void {\n\t\t\tgridQueryRect(grid, minX, minY, maxX, maxY, result);\n\t\t},\n\t\tqueryRadius(cx: number, cy: number, radius: number): number[] {\n\t\t\t_reusableQuerySet.clear();\n\t\t\tgridQueryRadius(grid, cx, cy, radius, _reusableQuerySet);\n\t\t\treturn Array.from(_reusableQuerySet);\n\t\t},\n\t\tqueryRadiusInto(cx: number, cy: number, radius: number, result: Set<number>): void {\n\t\t\tgridQueryRadius(grid, cx, cy, radius, result);\n\t\t},\n\t\tgetEntry(entityId: number): SpatialEntry | undefined {\n\t\t\treturn grid.entries.get(entityId);\n\t\t},\n\t};\n}\n\n// ==================== Component Types ====================\n\ntype SpatialIndexComponentTypes =\n\tTransformComponentTypes & Pick<CollisionComponentTypes<string>, 'aabbCollider' | 'circleCollider'>;\n\n// ==================== Plugin Options ====================\n\nexport type SpatialIndexPhase = 'fixedUpdate' | 'postUpdate';\ntype SpatialIndexLabel = `spatial-index-rebuild-${SpatialIndexPhase}`;\n\nexport interface SpatialIndexPluginOptions<G extends string = 'spatialIndex'> {\n\t/** Cell size for the spatial hash grid (default: 64) */\n\tcellSize?: number;\n\t/** System group name (default: 'spatialIndex') */\n\tsystemGroup?: G;\n\t/** Priority for rebuild systems (default: 2000, before collision) */\n\tpriority?: number;\n\t/** Phases to register rebuild systems in (default: ['fixedUpdate', 'postUpdate']) */\n\tphases?: ReadonlyArray<SpatialIndexPhase>;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a spatial index plugin for ECSpresso.\n *\n * Provides a uniform-grid spatial hash that accelerates collision detection.\n * When installed alongside the collision or physics2D plugins, they\n * automatically use the spatial index for broadphase instead of O(n²)\n * brute-force.\n *\n * Also provides proximity query methods for game logic (e.g. \"find all\n * enemies within 200 units\").\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createCollisionPlugin({ layers }))\n * .withPlugin(createSpatialIndexPlugin({ cellSize: 128 }))\n * .build();\n *\n * // Proximity query in a system:\n * const si = ecs.getResource('spatialIndex');\n * const nearby = si.queryRadius(playerX, playerY, 200);\n * ```\n */\nexport function createSpatialIndexPlugin<G extends string = 'spatialIndex'>(\n\toptions?: SpatialIndexPluginOptions<G>,\n): Plugin<WorldConfigFrom<SpatialIndexComponentTypes, {}, SpatialIndexResourceTypes>, EmptyConfig, SpatialIndexLabel, G> {\n\tconst {\n\t\tcellSize = 64,\n\t\tsystemGroup = 'spatialIndex',\n\t\tpriority = 2000,\n\t\tphases = ['fixedUpdate', 'postUpdate'] as const,\n\t} = options ?? {};\n\n\tconst grid = createGrid(cellSize);\n\tconst resource = createSpatialIndexResource(grid);\n\n\treturn definePlugin<WorldConfigFrom<SpatialIndexComponentTypes, {}, SpatialIndexResourceTypes>, EmptyConfig, SpatialIndexLabel, G>({\n\t\tid: 'spatialIndex',\n\t\tinstall(world) {\n\t\t\tworld.addResource('spatialIndex', resource);\n\n\t\t\t// Register a rebuild system for each requested phase\n\t\t\tfor (const phase of phases) {\n\t\t\t\tconst transformComponent = phase === 'fixedUpdate' ? 'localTransform' : 'worldTransform';\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem(`spatial-index-rebuild-${phase}`)\n\t\t\t\t\t.setPriority(priority)\n\t\t\t\t\t.inPhase(phase as SystemPhase)\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.addQuery('transforms', {\n\t\t\t\t\t\twith: [transformComponent],\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tclearGrid(grid);\n\n\t\t\t\t\t\tfor (const entity of queries.transforms) {\n\t\t\t\t\t\t\tconst transform = entity.components[transformComponent];\n\t\t\t\t\t\t\tconst aabb = ecs.getComponent(entity.id, 'aabbCollider');\n\t\t\t\t\t\t\tconst circle = ecs.getComponent(entity.id, 'circleCollider');\n\n\t\t\t\t\t\t\t// Only insert entities that have a collider\n\t\t\t\t\t\t\tif (!aabb && !circle) continue;\n\n\t\t\t\t\t\t\tlet x = transform.x;\n\t\t\t\t\t\t\tlet y = transform.y;\n\t\t\t\t\t\t\tlet halfW = 0;\n\t\t\t\t\t\t\tlet halfH = 0;\n\n\t\t\t\t\t\t\tif (aabb) {\n\t\t\t\t\t\t\t\tx += aabb.offsetX ?? 0;\n\t\t\t\t\t\t\t\ty += aabb.offsetY ?? 0;\n\t\t\t\t\t\t\t\thalfW = aabb.width / 2;\n\t\t\t\t\t\t\t\thalfH = aabb.height / 2;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (circle) {\n\t\t\t\t\t\t\t\tx += circle.offsetX ?? 0;\n\t\t\t\t\t\t\t\ty += circle.offsetY ?? 0;\n\t\t\t\t\t\t\t\t// Circle: use radius as half-extent in both dimensions\n\t\t\t\t\t\t\t\thalfW = Math.max(halfW, circle.radius);\n\t\t\t\t\t\t\t\thalfH = Math.max(halfH, circle.radius);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tinsertEntity(grid, entity.id, x, y, halfW, halfH);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\t\t},\n\t});\n}\n",
|
|
6
6
|
"/**\n * Spatial Hash Grid\n *\n * Uniform-grid spatial hash for broadphase collision detection and\n * proximity queries. Pure data structure, no ECS dependencies.\n */\n\n// ==================== Data Structure ====================\n\nexport interface SpatialEntry {\n\tentityId: number;\n\tx: number;\n\ty: number;\n\thalfW: number;\n\thalfH: number;\n}\n\nexport interface SpatialHashGrid {\n\tcellSize: number;\n\tinvCellSize: number;\n\tcells: Map<number, number[]>;\n\tentries: Map<number, SpatialEntry>;\n}\n\n// ==================== Pure Functions ====================\n\n/**\n * Hash a cell coordinate pair to a single integer key.\n * Uses large-prime XOR to distribute values.\n */\nexport function hashCell(cx: number, cy: number): number {\n\t// Large primes for spatial hashing distribution\n\treturn (cx * 73856093) ^ (cy * 19349663);\n}\n\n/**\n * Create a new empty spatial hash grid.\n */\nexport function createGrid(cellSize: number): SpatialHashGrid {\n\treturn {\n\t\tcellSize,\n\t\tinvCellSize: 1 / cellSize,\n\t\tcells: new Map(),\n\t\tentries: new Map(),\n\t};\n}\n\n/**\n * Clear all data from the grid without reallocating the Maps.\n */\nexport function clearGrid(grid: SpatialHashGrid): void {\n\tgrid.cells.clear();\n\tgrid.entries.clear();\n}\n\n/**\n * Insert an entity into all overlapping cells of the grid.\n */\nexport function insertEntity(\n\tgrid: SpatialHashGrid,\n\tentityId: number,\n\tx: number,\n\ty: number,\n\thalfW: number,\n\thalfH: number,\n): void {\n\tgrid.entries.set(entityId, { entityId, x, y, halfW, halfH });\n\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor((x - halfW) * inv);\n\tconst maxCX = Math.floor((x + halfW) * inv);\n\tconst minCY = Math.floor((y - halfH) * inv);\n\tconst maxCY = Math.floor((y + halfH) * inv);\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tconst key = hashCell(cx, cy);\n\t\t\tconst bucket = grid.cells.get(key);\n\t\t\tif (bucket) {\n\t\t\t\tbucket.push(entityId);\n\t\t\t} else {\n\t\t\t\tgrid.cells.set(key, [entityId]);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Collect entity IDs from all cells overlapping the given rectangle.\n */\nexport function gridQueryRect(\n\tgrid: SpatialHashGrid,\n\tminX: number,\n\tminY: number,\n\tmaxX: number,\n\tmaxY: number,\n\tresult: Set<number>,\n): void {\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor(minX * inv);\n\tconst maxCX = Math.floor(maxX * inv);\n\tconst minCY = Math.floor(minY * inv);\n\tconst maxCY = Math.floor(maxY * inv);\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tconst bucket = grid.cells.get(hashCell(cx, cy));\n\t\t\tif (!bucket) continue;\n\t\t\tfor (let i = 0; i < bucket.length; i++) {\n\t\t\t\tconst entry = bucket[i];\n\t\t\t\tif (entry !== undefined) result.add(entry);\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Module-scoped reusable set to reduce GC pressure\nconst _radiusCandidates = new Set<number>();\n\n/**\n * Collect entity IDs within a circle. Uses rect broadphase then\n * AABB-to-point distance filter.\n */\nexport function gridQueryRadius(\n\tgrid: SpatialHashGrid,\n\tcx: number,\n\tcy: number,\n\tradius: number,\n\tresult: Set<number>,\n): void {\n\t// Broadphase: rect query for bounding box of circle\n\tconst candidates = _radiusCandidates;\n\tcandidates.clear();\n\tgridQueryRect(grid, cx - radius, cy - radius, cx + radius, cy + radius, candidates);\n\n\tconst rSq = radius * radius;\n\n\tfor (const entityId of candidates) {\n\t\tconst entry = grid.entries.get(entityId);\n\t\tif (!entry) continue;\n\n\t\t// Closest point on entity AABB to query center\n\t\tconst closestX = Math.max(entry.x - entry.halfW, Math.min(cx, entry.x + entry.halfW));\n\t\tconst closestY = Math.max(entry.y - entry.halfH, Math.min(cy, entry.y + entry.halfH));\n\t\tconst dx = cx - closestX;\n\t\tconst dy = cy - closestY;\n\n\t\tif (dx * dx + dy * dy <= rSq) {\n\t\t\tresult.add(entityId);\n\t\t}\n\t}\n}\n\n// ==================== Resource API ====================\n\nexport interface SpatialIndex {\n\treadonly grid: SpatialHashGrid;\n\tqueryRect(minX: number, minY: number, maxX: number, maxY: number): number[];\n\tqueryRectInto(minX: number, minY: number, maxX: number, maxY: number, result: Set<number>): void;\n\tqueryRadius(cx: number, cy: number, radius: number): number[];\n\tqueryRadiusInto(cx: number, cy: number, radius: number, result: Set<number>): void;\n\tgetEntry(entityId: number): SpatialEntry | undefined;\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "926438F008C04F1E64756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var G=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(z,E)=>(typeof require<"u"?require:z)[E]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as Z}from"ecspresso";function Y(j){return Object.freeze({frames:Object.freeze([...j.frames]),frameDuration:j.frameDuration??0.1,frameDurations:j.frameDurations?Object.freeze([...j.frameDurations]):null,loop:j.loop??"loop"})}function U(j,z){return Object.freeze({id:j,clips:Object.freeze({default:Y(z)}),defaultClip:"default"})}function q(j,z,E){let J={},H=Object.keys(z);for(let N of H)J[N]=Y(z[N]);let M=H[0];if(!M)throw Error("defineSpriteAnimations: clips object must have at least one key");return Object.freeze({id:j,clips:Object.freeze(J),defaultClip:E?.defaultClip??M})}function D(j,z){let E=z?.initial??j.defaultClip;return{spriteAnimation:{set:j,current:E,currentFrame:0,elapsed:0,playing:!0,speed:z?.speed??1,direction:1,totalLoops:z?.totalLoops??-1,completedLoops:0,justFinished:!1,onComplete:z?.onComplete}}}function K(j,z,E,J){let H=j.getComponent(z,"spriteAnimation");if(!H)return!1;if(!(E in H.set.clips))return!1;if(E!==H.current||J?.restart===!0)H.current=E,H.currentFrame=0,H.elapsed=0,H.direction=1,H.completedLoops=0,H.justFinished=!1;if(H.playing=!0,J?.speed!==void 0)H.speed=J.speed;return j.markChanged(z,"spriteAnimation"),!0}function S(j,z){let E=j.getComponent(z,"spriteAnimation");if(!E)return!1;return E.playing=!1,!0}function A(j,z){let E=j.getComponent(z,"spriteAnimation");if(!E)return!1;return E.playing=!0,!0}function W(j,z,E){j.playing=!1,j.justFinished=!0,j.onComplete?.({entityId:z,animation:j.current}),E.commands.removeComponent(z,"spriteAnimation")}function _(j,z,E,J){if(j.completedLoops++,z.loop==="once")return W(j,E,J),!1;if(j.totalLoops>0&&j.completedLoops>=j.totalLoops)return W(j,E,J),!1;if(z.loop==="pingPong")return j.direction=j.direction===1?-1:1,j.currentFrame+=j.direction,j.elapsed>0;return j.currentFrame=0,j.elapsed>0}function X(j,z,E,J){let H=j.currentFrame+j.direction;if(H>=z.frames.length||H<0)return _(j,z,E,J);return j.currentFrame=H,!0}function $(j,z,E,J){while(!0){let H=z.frameDurations!==null?z.frameDurations[j.currentFrame]??z.frameDuration:z.frameDuration;if(H<=0){if(!X(j,z,E,J))return;continue}let M=H-j.elapsed;if(M>0.000001)return;if(j.elapsed=M<0?-M:0,!X(j,z,E,J))return}}function b(j){let{systemGroup:z="spriteAnimation",priority:E=0,phase:J="update"}=j??{};return Z({id:"spriteAnimation",install(H){H.addSystem("sprite-animation-update").setPriority(E).inPhase(J).inGroup(z).addQuery("animations",{with:["spriteAnimation"]}).setProcess(({queries:M,dt:N,ecs:V})=>{for(let O of M.animations){let L=O.components.spriteAnimation,Q=L.set.clips[L.current];if(!Q)continue;if(L.justFinished){L.justFinished=!1;continue}if(!L.playing)continue;if(Q.frames.length<=1)continue;let R=L.currentFrame;if(L.elapsed+=N*L.speed,$(L,Q,O.id,V),L.currentFrame!==R||R===0)B(O.components,L,Q);if(L.currentFrame!==R)V.markChanged(O.id,"spriteAnimation")}})}})}function B(j,z,E){let J=j.sprite;if(J&&typeof J==="object"&&"texture"in J)J.texture=E.frames[z.currentFrame]}export{S as stopAnimation,A as resumeAnimation,K as playAnimation,q as defineSpriteAnimations,U as defineSpriteAnimation,b as createSpriteAnimationPlugin,D as createSpriteAnimation};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=C75605DB29C1814064756E2164756E21
|
|
4
4
|
//# sourceMappingURL=sprite-animation.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * Sprite Animation Plugin for ECSpresso\n *\n * ECS-native frame-based sprite animation. Advances through spritesheet frames,\n * handles loop modes (once, loop, pingPong), publishes completion events, and\n * syncs the current frame's texture to the PixiJS Sprite via structural access.\n *\n * Renderer2D is a required dependency — the `sprite` component comes from that plugin.\n * This plugin declares only `spriteAnimation` as its component type.\n */\n\nimport { definePlugin, type Plugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../type-utils';\n\n/** BaseWorld narrowed to sprite-animation components for typed access in helpers. */\ntype SpriteAnimationWorld = BaseWorld<SpriteAnimationComponentTypes>;\n\n// ==================== Loop Mode ====================\n\nexport type AnimationLoopMode = 'once' | 'loop' | 'pingPong';\n\n// ==================== Clip Types ====================\n\n/**\n * A single animation clip: an ordered sequence of texture frames with timing.\n * Immutable and shared across entities.\n */\nexport interface SpriteAnimationClip {\n\treadonly frames: readonly unknown[];\n\treadonly frameDuration: number;\n\treadonly frameDurations: readonly number[] | null;\n\treadonly loop: AnimationLoopMode;\n}\n\n/**\n * Input format for defining a clip. Accepts either uniform or per-frame timing.\n */\nexport interface SpriteAnimationClipInput {\n\t/** Array of PixiJS Texture objects */\n\tframes: readonly unknown[];\n\t/** Uniform seconds-per-frame (used when frameDurations is not provided) */\n\tframeDuration?: number;\n\t/** Per-frame durations in seconds (overrides frameDuration) */\n\tframeDurations?: readonly number[];\n\t/** Loop mode (default: 'loop') */\n\tloop?: AnimationLoopMode;\n}\n\n// ==================== Animation Set ====================\n\n/**\n * A named collection of animation clips. Immutable and shared across entities.\n * Parameterized by A (animation name union) for compile-time validation.\n */\nexport interface SpriteAnimationSet<A extends string = string> {\n\treadonly id: string;\n\treadonly clips: { readonly [K in A]: SpriteAnimationClip };\n\treadonly defaultClip: A;\n}\n\n// ==================== Component ====================\n\n/**\n * Per-entity runtime animation state.\n */\nexport interface SpriteAnimation<A extends string = string> {\n\treadonly set: SpriteAnimationSet<A>;\n\tcurrent: A;\n\tcurrentFrame: number;\n\telapsed: number;\n\tplaying: boolean;\n\tspeed: number;\n\tdirection: 1 | -1;\n\ttotalLoops: number;\n\tcompletedLoops: number;\n\tjustFinished: boolean;\n\tonComplete?: (data: SpriteAnimationEventData) => void;\n}\n\n/**\n * Component types provided by the sprite animation plugin.\n */\nexport interface SpriteAnimationComponentTypes<A extends string = string> {\n\tspriteAnimation: SpriteAnimation<A>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Data published when an animation completes.\n */\nexport interface SpriteAnimationEventData {\n\tentityId: number;\n\tanimation: string;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface SpriteAnimationPluginOptions<G extends string = 'spriteAnimation'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\nfunction buildClip(input: SpriteAnimationClipInput): SpriteAnimationClip {\n\treturn Object.freeze({\n\t\tframes: Object.freeze([...input.frames]),\n\t\tframeDuration: input.frameDuration ?? (1 / 10),\n\t\tframeDurations: input.frameDurations\n\t\t\t? Object.freeze([...input.frameDurations])\n\t\t\t: null,\n\t\tloop: input.loop ?? 'loop',\n\t});\n}\n\n/**\n * Define a single-clip animation set named 'default'.\n * For simple use cases like spinning coins, pulsing effects, etc.\n *\n * @param id Unique identifier for this animation set\n * @param clip Clip definition\n * @returns A frozen SpriteAnimationSet with one clip named 'default'\n */\nexport function defineSpriteAnimation(\n\tid: string,\n\tclip: SpriteAnimationClipInput,\n): SpriteAnimationSet<'default'> {\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze({ default: buildClip(clip) }),\n\t\tdefaultClip: 'default' as const,\n\t});\n}\n\n/**\n * Define a multi-clip animation set with named animations.\n * Animation names are inferred from the keys of the clips object.\n *\n * @param id Unique identifier for this animation set\n * @param clips Object mapping animation names to clip definitions\n * @param options Optional configuration (defaultClip)\n * @returns A frozen SpriteAnimationSet with inferred animation name union\n */\nexport function defineSpriteAnimations<A extends string>(\n\tid: string,\n\tclips: Record<A, SpriteAnimationClipInput>,\n\toptions?: { defaultClip?: NoInfer<A> },\n): SpriteAnimationSet<A> {\n\tconst builtClips = {} as Record<A, SpriteAnimationClip>;\n\tconst keys = Object.keys(clips) as A[];\n\n\tfor (const key of keys) {\n\t\tbuiltClips[key] = buildClip(clips[key]);\n\t}\n\n\tconst firstKey = keys[0];\n\tif (!firstKey) {\n\t\tthrow new Error(`defineSpriteAnimations: clips object must have at least one key`);\n\t}\n\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze(builtClips),\n\t\tdefaultClip: options?.defaultClip ?? firstKey,\n\t});\n}\n\n/**\n * Create a spriteAnimation component from an animation set.\n *\n * @param set The animation set to use\n * @param options Optional configuration (initial clip, speed, onComplete event)\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createSpriteAnimation<A extends string>(\n\tset: SpriteAnimationSet<A>,\n\toptions?: {\n\t\tinitial?: A;\n\t\tspeed?: number;\n\t\ttotalLoops?: number;\n\t\tonComplete?: (data: SpriteAnimationEventData) => void;\n\t},\n): Pick<SpriteAnimationComponentTypes<A>, 'spriteAnimation'> {\n\tconst initial = options?.initial ?? set.defaultClip;\n\treturn {\n\t\tspriteAnimation: {\n\t\t\tset,\n\t\t\tcurrent: initial,\n\t\t\tcurrentFrame: 0,\n\t\t\telapsed: 0,\n\t\t\tplaying: true,\n\t\t\tspeed: options?.speed ?? 1,\n\t\t\tdirection: 1,\n\t\t\ttotalLoops: options?.totalLoops ?? -1,\n\t\t\tcompletedLoops: 0,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Switch an entity's current animation at runtime.\n * Resets state if switching to a different animation (or restart=true).\n *\n * @returns false if entity has no spriteAnimation or animation name doesn't exist\n */\nexport function playAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n\tanimation: string,\n\toptions?: { restart?: boolean; speed?: number },\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\tif (!(animation in anim.set.clips)) return false;\n\n\tconst shouldReset = animation !== anim.current || options?.restart === true;\n\n\tif (shouldReset) {\n\t\tanim.current = animation;\n\t\tanim.currentFrame = 0;\n\t\tanim.elapsed = 0;\n\t\tanim.direction = 1;\n\t\tanim.completedLoops = 0;\n\t\tanim.justFinished = false;\n\t}\n\n\tanim.playing = true;\n\n\tif (options?.speed !== undefined) {\n\t\tanim.speed = options.speed;\n\t}\n\n\tecs.markChanged(entityId, 'spriteAnimation');\n\treturn true;\n}\n\n/**\n * Pause an entity's animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function stopAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = false;\n\treturn true;\n}\n\n/**\n * Resume a paused animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function resumeAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = true;\n\treturn true;\n}\n\n// ==================== Animation Processing Helpers ====================\n\nfunction completeAnimation(\n\tanim: SpriteAnimation,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\tanim.playing = false;\n\tanim.justFinished = true;\n\n\tanim.onComplete?.({ entityId, animation: anim.current });\n\n\tecs.commands.removeComponent(entityId, 'spriteAnimation');\n}\n\nfunction handleBoundary(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tanim.completedLoops++;\n\n\tif (clip.loop === 'once') {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\t// Check finite loop count\n\tif (anim.totalLoops > 0 && anim.completedLoops >= anim.totalLoops) {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\tif (clip.loop === 'pingPong') {\n\t\tanim.direction = anim.direction === 1 ? -1 : 1;\n\t\t// Step one frame in the new direction from the boundary\n\t\tanim.currentFrame += anim.direction;\n\t\treturn anim.elapsed > 0;\n\t}\n\n\t// loop mode: wrap to frame 0\n\tanim.currentFrame = 0;\n\treturn anim.elapsed > 0;\n}\n\n/**\n * Advance to next frame. Returns true if processing should continue (more overflow),\n * false if animation completed or reached a boundary.\n */\nfunction advanceFrame(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tconst nextFrame = anim.currentFrame + anim.direction;\n\n\t// Check boundary\n\tif (nextFrame >= clip.frames.length || nextFrame < 0) {\n\t\treturn handleBoundary(anim, clip, entityId, ecs);\n\t}\n\n\tanim.currentFrame = nextFrame;\n\treturn true;\n}\n\nfunction processFrameAdvancement(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\t// Process frame overflow\n\t// eslint-disable-next-line no-constant-condition\n\twhile (true) {\n\t\tconst frameDuration = clip.frameDurations !== null\n\t\t\t? (clip.frameDurations[anim.currentFrame] ?? clip.frameDuration)\n\t\t\t: clip.frameDuration;\n\n\t\tif (frameDuration <= 0) {\n\t\t\t// Zero-duration frame: advance immediately\n\t\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Floating-point-safe comparison: treat elapsed within 1μs of\n\t\t// frameDuration as having reached the boundary.\n\t\tconst remaining = frameDuration - anim.elapsed;\n\t\tif (remaining > 1e-6) return;\n\n\t\t// Frame complete — carry overflow (clamp negative remainders to 0)\n\t\tanim.elapsed = remaining < 0 ? -remaining : 0;\n\n\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t}\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a sprite animation plugin for ECSpresso.\n *\n * Provides:\n * - Frame-based animation system processing spriteAnimation components\n * - Loop modes: once, loop, pingPong\n * - justFinished one-frame flag for completion detection\n * - onComplete event publishing\n * - Sprite texture sync via structural cross-plugin access\n * - Change detection via markChanged\n */\nexport function createSpriteAnimationPlugin<\n\tG extends string = 'spriteAnimation',\n>(\n\toptions?: SpriteAnimationPluginOptions<G>,\n): Plugin<WorldConfigFrom<SpriteAnimationComponentTypes>, EmptyConfig, 'sprite-animation-update', G> {\n\tconst {\n\t\tsystemGroup = 'spriteAnimation',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin<WorldConfigFrom<SpriteAnimationComponentTypes>, EmptyConfig, 'sprite-animation-update', G>({\n\t\tid: 'spriteAnimation',\n\t\tinstall(world) {\n\t\t\tworld\n\t\t\t\t.addSystem('sprite-animation-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('animations', {\n\t\t\t\t\twith: ['spriteAnimation'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.animations) {\n\t\t\t\t\t\tconst anim = entity.components.spriteAnimation as SpriteAnimation;\n\t\t\t\t\t\tconst clip = anim.set.clips[anim.current];\n\t\t\t\t\t\tif (!clip) continue;\n\n\t\t\t\t\t\t// Clear justFinished from previous frame\n\t\t\t\t\t\tif (anim.justFinished) {\n\t\t\t\t\t\t\tanim.justFinished = false;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Skip paused animations\n\t\t\t\t\t\tif (!anim.playing) continue;\n\n\t\t\t\t\t\t// Skip single-frame clips\n\t\t\t\t\t\tif (clip.frames.length <= 1) continue;\n\n\t\t\t\t\t\tconst previousFrame = anim.currentFrame;\n\t\t\t\t\t\tanim.elapsed += dt * anim.speed;\n\n\t\t\t\t\t\tprocessFrameAdvancement(anim, clip, entity.id, ecs);\n\n\t\t\t\t\t\t// Sync sprite texture if frame changed\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame || previousFrame === 0) {\n\t\t\t\t\t\t\tsyncSpriteTexture(entity.components as Record<string, unknown>, anim, clip);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame) {\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'spriteAnimation');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t},\n\t});\n}\n\n// ==================== Internal: Sprite Texture Sync ====================\n\n/**\n * Sync the sprite's texture to the current frame. Uses structural access\n * following the tween plugin's cross-component pattern.\n */\nfunction syncSpriteTexture(\n\tentityComponents: Record<string, unknown>,\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n): void {\n\tconst sprite = entityComponents['sprite'];\n\tif (sprite && typeof sprite === 'object' && 'texture' in sprite) {\n\t\t(sprite as { texture: unknown }).texture = clip.frames[anim.currentFrame];\n\t}\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "C75605DB29C1814064756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var Y=((b)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(b,{get:(j,z)=>(typeof require<"u"?require:j)[z]}):b)(function(b){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+b+'" is not supported')});import{definePlugin as W}from"ecspresso";function X(b,j){return Object.freeze({id:b,initial:j.initial,states:Object.freeze(j.states)})}function $(b,j){let z=j?.initial??b.initial;return{stateMachine:{definition:b,current:z,previous:null,stateTime:0}}}function L(b,j,z,A){let H=z.definition.states,D=H[z.current],B=H[A];if(!B)return!1;return D?.onExit?.({ecs:b,entityId:j}),z.previous=z.current,z.current=A,z.stateTime=0,B.onEnter?.({ecs:b,entityId:j}),b.markChanged(j,"stateMachine"),b.eventBus.publish("stateTransition",{entityId:j,from:z.previous,to:z.current,definitionId:z.definition.id}),!0}function E(b,j,z){let A=b.getComponent(j,"stateMachine");if(!A)return!1;return L(b,j,A,z)}function G(b,j,z){let A=b.getComponent(j,"stateMachine");if(!A)return!1;let D=A.definition.states[A.current];if(!D?.on)return!1;let B=D.on[z];if(B===void 0)return!1;if(typeof B==="string")return L(b,j,A,B);if(!B.guard({ecs:b,entityId:j}))return!1;return L(b,j,A,B.target)}function M(b,j){return b.getComponent(j,"stateMachine")?.current}function P(b){return{defineStateMachine:X}}function q(b){let{systemGroup:j="stateMachine",priority:z=0,phase:A="update"}=b??{};return W({id:"stateMachine",install(H){H.addSystem("state-machine-update").setPriority(z).inPhase(A).inGroup(j).addQuery("machines",{with:["stateMachine"]}).setOnEntityEnter("machines",({entity:D,ecs:B})=>{let K=D.components.stateMachine,Q=K.definition.states,F=B;Q[K.current]?.onEnter?.({ecs:F,entityId:D.id})}).setProcess(({queries:D,dt:B,ecs:K})=>{let F={ecs:K,entityId:0,dt:0};for(let O of D.machines){let J=O.components.stateMachine,R=J.definition.states;F.entityId=O.id,F.dt=B,J.stateTime+=B,R[J.current]?.onUpdate?.(F);let U=R[J.current];if(U?.transitions){for(let V of U.transitions)if(V.guard(F)){L(F.ecs,O.id,J,V.target);break}}}})}})}export{E as transitionTo,G as sendEvent,M as getStateMachineState,X as defineStateMachine,q as createStateMachinePlugin,P as createStateMachineHelpers,$ as createStateMachine};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=995F6042C9E1B1C064756E2164756E21
|
|
4
4
|
//# sourceMappingURL=state-machine.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * State Machine Plugin for ECSpresso\n *\n * Provides ECS-native finite state machines with guard-based transitions,\n * event-driven transitions, and lifecycle hooks (onEnter, onExit, onUpdate).\n *\n * Each entity gets a `stateMachine` component referencing a shared definition.\n * One system processes all state machine entities each tick.\n */\n\nimport { definePlugin, type Plugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../type-utils';\n\n/** BaseWorld narrowed to state-machine components for typed access in helpers. */\ntype StateMachineWorld = BaseWorld<StateMachineComponentTypes>;\n\n// ==================== State Config ====================\n\n/**\n * Configuration for a single state in a state machine definition.\n *\n * @template S - Union of state name strings\n * @template W - World interface type for hooks/guards (default: StateMachineWorld)\n */\nexport interface StateConfig<S extends string, W extends BaseWorld<StateMachineComponentTypes> = StateMachineWorld> {\n\t/** Called when entering this state */\n\tonEnter?(ctx: { ecs: W; entityId: number }): void;\n\t/** Called when exiting this state */\n\tonExit?(ctx: { ecs: W; entityId: number }): void;\n\t/** Called each tick while in this state */\n\tonUpdate?(ctx: { ecs: W; entityId: number; dt: number }): void;\n\t/** Guard-based transitions evaluated each tick. First passing guard wins. */\n\ttransitions?: ReadonlyArray<{\n\t\ttarget: S;\n\t\tguard(ctx: { ecs: W; entityId: number }): boolean;\n\t}>;\n\t/** Event-based transition map: eventName → target state or guarded transition */\n\ton?: Record<string, S | { target: S; guard(ctx: { ecs: W; entityId: number }): boolean }>;\n}\n\n// ==================== State Machine Definition ====================\n\n/**\n * Immutable definition of a state machine. Shared across entities.\n *\n * @template S - Union of state name strings\n */\nexport interface StateMachineDefinition<S extends string> {\n\treadonly id: string;\n\treadonly initial: S;\n\treadonly states: { readonly [K in S]: StateConfig<S> };\n}\n\n// ==================== Component ====================\n\n/**\n * Runtime state machine component stored on each entity.\n *\n * @template S - Union of state name strings (default: string)\n */\nexport interface StateMachine<S extends string = string> {\n\treadonly definition: StateMachineDefinition<string>;\n\tcurrent: S;\n\tprevious: S | null;\n\tstateTime: number;\n}\n\n/**\n * Component types provided by the state machine plugin.\n *\n * @template S - Union of state name strings (default: string)\n */\nexport interface StateMachineComponentTypes<S extends string = string> {\n\tstateMachine: StateMachine<S>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event published on every state transition.\n *\n * @template S - Union of state name strings (default: string)\n */\nexport interface StateTransitionEvent<S extends string = string> {\n\tentityId: number;\n\tfrom: S;\n\tto: S;\n\tdefinitionId: string;\n}\n\n/**\n * Event types provided by the state machine plugin.\n *\n * @template S - Union of state name strings (default: string)\n */\nexport interface StateMachineEventTypes<S extends string = string> {\n\tstateTransition: StateTransitionEvent<S>;\n}\n\n/**\n * Extract the state name union from a StateMachineDefinition.\n *\n * @example\n * ```typescript\n * const enemyFSM = defineStateMachine('enemy', { initial: 'idle', states: { idle: {}, chase: {} } });\n * type EnemyStates = StatesOf<typeof enemyFSM>; // 'idle' | 'chase'\n * type AllStates = StatesOf<typeof enemyFSM> | StatesOf<typeof playerFSM>;\n * ```\n */\nexport type StatesOf<D> = D extends StateMachineDefinition<infer S> ? S : never;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the state machine plugin.\n */\nexport interface StateMachinePluginOptions<G extends string = 'stateMachine'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Define a state machine with type-safe state names.\n *\n * @template S - Union of state name strings, inferred from `states` keys\n * @param id - Unique identifier for this definition\n * @param config - Initial state and state configurations\n * @returns A frozen StateMachineDefinition\n *\n * @example\n * ```typescript\n * const enemyFSM = defineStateMachine('enemy', {\n * initial: 'idle',\n * states: {\n * idle: {\n * onEnter: ({ ecs, entityId }) => { ... },\n * transitions: [{ target: 'chase', guard: ({ ecs, entityId }) => playerNearby(ecs, entityId) }],\n * },\n * chase: {\n * onUpdate: ({ ecs, entityId, dt }) => { ... },\n * on: { playerLost: 'idle' },\n * },\n * },\n * });\n * ```\n */\nexport function defineStateMachine<S extends string>(\n\tid: string,\n\tconfig: { initial: NoInfer<S>; states: Record<S, StateConfig<NoInfer<S>>> },\n): StateMachineDefinition<S> {\n\treturn Object.freeze({\n\t\tid,\n\t\tinitial: config.initial,\n\t\tstates: Object.freeze(config.states),\n\t}) as StateMachineDefinition<S>;\n}\n\n/**\n * Create a stateMachine component from a definition.\n *\n * @param definition - The state machine definition to use\n * @param options - Optional overrides (e.g., initial state)\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createStateMachine(enemyFSM),\n * position: { x: 100, y: 200 },\n * });\n * ```\n */\nexport function createStateMachine<S extends string>(\n\tdefinition: StateMachineDefinition<S>,\n\toptions?: { initial?: S },\n): Pick<StateMachineComponentTypes<S>, 'stateMachine'> {\n\tconst initial = options?.initial ?? definition.initial;\n\treturn {\n\t\tstateMachine: {\n\t\t\tdefinition,\n\t\t\tcurrent: initial,\n\t\t\tprevious: null,\n\t\t\tstateTime: 0,\n\t\t},\n\t};\n}\n\n// ==================== Internal: Shared Transition Logic ====================\n\n/**\n * Perform a state transition: onExit → update fields → onEnter → markChanged → publish event.\n * Returns true if the target state exists, false otherwise.\n */\nfunction performTransition(\n\tecs: StateMachineWorld,\n\tentityId: number,\n\tsm: StateMachine,\n\ttargetState: string,\n): boolean {\n\tconst states = sm.definition.states as Record<string, StateConfig<string>>;\n\tconst currentConfig = states[sm.current];\n\tconst targetConfig = states[targetState];\n\n\tif (!targetConfig) return false;\n\n\tcurrentConfig?.onExit?.({ ecs, entityId });\n\n\tsm.previous = sm.current;\n\tsm.current = targetState;\n\tsm.stateTime = 0;\n\n\ttargetConfig.onEnter?.({ ecs, entityId });\n\n\tecs.markChanged(entityId, 'stateMachine');\n\tecs.eventBus.publish('stateTransition', {\n\t\tentityId,\n\t\tfrom: sm.previous,\n\t\tto: sm.current,\n\t\tdefinitionId: sm.definition.id,\n\t} satisfies StateTransitionEvent);\n\n\treturn true;\n}\n\n// ==================== Utility Functions ====================\n\n/**\n * Directly transition an entity's state machine to a target state.\n * Fires onExit, onEnter hooks and publishes stateTransition event.\n *\n * @param ecs - ECS instance (structural typing)\n * @param entityId - Entity to transition\n * @param targetState - State to transition to\n * @returns true if transition succeeded, false if entity has no stateMachine or target state doesn't exist\n */\nexport function transitionTo(\n\tecs: StateMachineWorld,\n\tentityId: number,\n\ttargetState: string,\n): boolean {\n\tconst sm = ecs.getComponent(entityId, 'stateMachine');\n\tif (!sm) return false;\n\treturn performTransition(ecs, entityId, sm, targetState);\n}\n\n/**\n * Send a named event to an entity's state machine.\n * Checks the current state's `on` handlers for a matching event.\n *\n * @param ecs - ECS instance (structural typing)\n * @param entityId - Entity to send event to\n * @param eventName - Event name to match against `on` handlers\n * @returns true if a transition occurred, false otherwise\n */\nexport function sendEvent(\n\tecs: StateMachineWorld,\n\tentityId: number,\n\teventName: string,\n): boolean {\n\tconst sm = ecs.getComponent(entityId, 'stateMachine');\n\tif (!sm) return false;\n\n\tconst states = sm.definition.states as Record<string, StateConfig<string>>;\n\tconst currentConfig = states[sm.current];\n\tif (!currentConfig?.on) return false;\n\n\tconst handler = currentConfig.on[eventName];\n\tif (handler === undefined) return false;\n\n\tif (typeof handler === 'string') {\n\t\treturn performTransition(ecs, entityId, sm, handler);\n\t}\n\n\tif (!handler.guard({ ecs, entityId })) return false;\n\treturn performTransition(ecs, entityId, sm, handler.target);\n}\n\n/**\n * Get the current state of an entity's state machine.\n *\n * @param ecs - ECS instance (structural typing)\n * @param entityId - Entity to query\n * @returns The current state string, or undefined if entity has no stateMachine\n */\nexport function getStateMachineState(\n\tecs: StateMachineWorld,\n\tentityId: number,\n): string | undefined {\n\tconst sm = ecs.getComponent(entityId, 'stateMachine');\n\treturn sm?.current;\n}\n\n// ==================== State Machine Helpers ====================\n\n/**\n * Typed helpers for the state machine plugin.\n * Creates helpers that validate hook parameters against the world type W.\n * Call after .build() using typeof ecs.\n */\nexport interface StateMachineHelpers<W extends BaseWorld<StateMachineComponentTypes>> {\n\tdefineStateMachine: <S extends string>(\n\t\tid: string,\n\t\tconfig: { initial: NoInfer<S>; states: Record<S, StateConfig<NoInfer<S>, W>> },\n\t) => StateMachineDefinition<S>;\n}\n\nexport function createStateMachineHelpers<W extends BaseWorld<StateMachineComponentTypes> = StateMachineWorld>(_world?: W): StateMachineHelpers<W> {\n\treturn {\n\t\tdefineStateMachine: defineStateMachine as StateMachineHelpers<W>['defineStateMachine'],\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a state machine plugin for ECSpresso.\n *\n * Provides:\n * - Lifecycle hooks (onEnter, onExit, onUpdate) per state\n * - Guard-based automatic transitions evaluated each tick\n * - Event-based transitions via `sendEvent()`\n * - Direct transitions via `transitionTo()`\n * - stateTransition events published on every transition\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createStateMachinePlugin())\n * .build();\n *\n * const fsm = defineStateMachine('enemy', {\n * initial: 'idle',\n * states: {\n * idle: {\n * transitions: [{ target: 'chase', guard: ({ ecs, entityId }) => playerNearby(ecs, entityId) }],\n * },\n * chase: {\n * onUpdate: ({ ecs, entityId, dt }) => moveTowardPlayer(ecs, entityId, dt),\n * on: { playerLost: 'idle' },\n * },\n * },\n * });\n *\n * ecs.spawn({\n * ...createStateMachine(fsm),\n * position: { x: 0, y: 0 },\n * });\n * ```\n */\nexport function createStateMachinePlugin<S extends string = string, G extends string = 'stateMachine'>(\n\toptions?: StateMachinePluginOptions<G>,\n): Plugin<WorldConfigFrom<StateMachineComponentTypes<S>, StateMachineEventTypes<S>>, EmptyConfig, 'state-machine-update', G> {\n\tconst {\n\t\tsystemGroup = 'stateMachine',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin<WorldConfigFrom<StateMachineComponentTypes<S>, StateMachineEventTypes<S>>, EmptyConfig, 'state-machine-update', G>({\n\t\tid: 'stateMachine',\n\t\tinstall(world) {\n\t\t\tworld\n\t\t\t\t.addSystem('state-machine-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('machines', {\n\t\t\t\t\twith: ['stateMachine'],\n\t\t\t\t})\n\t\t\t\t.setOnEntityEnter('machines', ({ entity, ecs }) => {\n\t\t\t\t\tconst sm = entity.components.stateMachine;\n\t\t\t\t\tconst states = sm.definition.states as Record<string, StateConfig<string>>;\n\t\t\t\t\tconst world: StateMachineWorld = ecs;\n\t\t\t\t\tstates[sm.current]?.onEnter?.({ ecs: world, entityId: entity.id });\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\t// Pre-allocated context reused across entities to avoid per-entity-per-frame allocations\n\t\t\t\t\tconst world: StateMachineWorld = ecs;\n\t\t\t\t\tconst hookCtx = { ecs: world, entityId: 0, dt: 0 };\n\n\t\t\t\t\tfor (const entity of queries.machines) {\n\t\t\t\t\t\tconst sm = entity.components.stateMachine;\n\t\t\t\t\t\tconst states = sm.definition.states as Record<string, StateConfig<string>>;\n\n\t\t\t\t\t\thookCtx.entityId = entity.id;\n\t\t\t\t\t\thookCtx.dt = dt;\n\n\t\t\t\t\t\t// Accumulate state time\n\t\t\t\t\t\tsm.stateTime += dt;\n\n\t\t\t\t\t\t// onUpdate hook\n\t\t\t\t\t\tstates[sm.current]?.onUpdate?.(hookCtx);\n\n\t\t\t\t\t\t// Evaluate guard transitions (first passing guard wins)\n\t\t\t\t\t\tconst currentConfig = states[sm.current];\n\t\t\t\t\t\tif (currentConfig?.transitions) {\n\t\t\t\t\t\t\tfor (const transition of currentConfig.transitions) {\n\t\t\t\t\t\t\t\tif (transition.guard(hookCtx)) {\n\t\t\t\t\t\t\t\t\tperformTransition(hookCtx.ecs, entity.id, sm, transition.target);\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t},\n\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "995F6042C9E1B1C064756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/plugins/timers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var J=((k)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(k,{get:(x,A)=>(typeof require<"u"?require:x)[A]}):k)(function(k){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+k+'" is not supported')});import{definePlugin as H}from"ecspresso";function M(k,x){return{timer:{elapsed:0,duration:k,repeat:!1,active:!0,justFinished:!1,onComplete:x?.onComplete}}}function N(k,x){return{timer:{elapsed:0,duration:k,repeat:!0,active:!0,justFinished:!1,onComplete:x?.onComplete}}}function Q(k){let{systemGroup:x="timers",priority:A=0,phase:B="preUpdate"}=k??{};return H({id:"timers",install(C){C.addSystem("timer-update").setPriority(A).inPhase(B).inGroup(x).addQuery("timers",{with:["timer"]}).setProcess(({queries:D,dt:E,ecs:F})=>{for(let z of D.timers){let{timer:j}=z.components;if(j.justFinished=!1,!j.active)continue;if(j.elapsed+=E,j.elapsed<j.duration)continue;if(j.repeat)while(j.elapsed>=j.duration)j.justFinished=!0,j.onComplete?.({entityId:z.id,duration:j.duration,elapsed:j.elapsed}),j.elapsed-=j.duration;else j.justFinished=!0,j.onComplete?.({entityId:z.id,duration:j.duration,elapsed:j.elapsed}),j.active=!1,F.commands.removeEntity(z.id)}})}})}export{Q as createTimerPlugin,M as createTimer,N as createRepeatingTimer};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=8D3E01379540788564756E2164756E21
|
|
4
4
|
//# sourceMappingURL=timers.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * Timer Plugin for ECSpresso\n *\n * Provides ECS-native timers following the \"data, not callbacks\" philosophy.\n * Timers are components processed each frame, automatically cleaned up when entities are removed.\n */\n\nimport { definePlugin, type Plugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../type-utils';\n\n// ==================== Event Types ====================\n\n/**\n * Data structure passed to onComplete callbacks when a timer completes.\n *\n * @example\n * ```typescript\n * createTimer(1.5, {\n * onComplete: (data) => {\n * console.log(`Timer on entity ${data.entityId} finished after ${data.elapsed}s`);\n * }\n * });\n * ```\n */\nexport interface TimerEventData {\n\t/** The entity ID that the timer belongs to */\n\tentityId: number;\n\t/** The timer's configured duration in seconds */\n\tduration: number;\n\t/** The actual elapsed time (may exceed duration slightly) */\n\telapsed: number;\n}\n\n// ==================== Component Types ====================\n\n\n/**\n * Timer component data structure.\n * Use `justFinished` to detect timer completion in your systems.\n */\nexport interface Timer {\n\t/** Time accumulated so far (seconds) */\n\telapsed: number;\n\t/** Target duration (seconds) */\n\tduration: number;\n\t/** Whether timer repeats after completion */\n\trepeat: boolean;\n\t/** Whether timer is currently running */\n\tactive: boolean;\n\t/** True for one frame after timer completes */\n\tjustFinished: boolean;\n\t/** Optional callback invoked when timer completes */\n\tonComplete?: (data: TimerEventData) => void;\n}\n\n/**\n * Component types provided by the timer plugin.\n * Included automatically via `.withPlugin(createTimerPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTimerPlugin())\n * .withComponentTypes<{ velocity: { x: number; y: number }; player: true }>()\n * .build();\n * ```\n */\nexport interface TimerComponentTypes {\n\ttimer: Timer;\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the timer plugin.\n */\nexport interface TimerPluginOptions<G extends string = 'timers'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Options for timer creation\n */\nexport interface TimerOptions {\n\t/** Callback invoked when timer completes */\n\tonComplete?: (data: TimerEventData) => void;\n}\n\n/**\n * Create a one-shot timer that fires once after the specified duration.\n *\n * @param duration Duration in seconds until the timer completes\n * @param options Optional configuration including onComplete callback\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * // Timer without callback\n * ecs.spawn({\n * ...createTimer(2),\n * explosion: true,\n * });\n *\n * // Timer with onComplete callback\n * ecs.spawn({\n * ...createTimer(1.5, { onComplete: (data) => console.log('done', data.entityId) }),\n * });\n * ```\n */\nexport function createTimer(\n\tduration: number,\n\toptions?: TimerOptions\n): Pick<TimerComponentTypes, 'timer'> {\n\treturn {\n\t\ttimer: {\n\t\t\telapsed: 0,\n\t\t\tduration,\n\t\t\trepeat: false,\n\t\t\tactive: true,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Create a repeating timer that fires every `duration` seconds.\n *\n * @param duration Duration in seconds between each timer completion\n * @param options Optional configuration including onComplete callback\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * // Timer without callback\n * ecs.spawn({\n * ...createRepeatingTimer(5),\n * spawner: true,\n * });\n *\n * // Repeating timer with onComplete callback\n * ecs.spawn({\n * ...createRepeatingTimer(3, { onComplete: (data) => console.log('cycle', data.elapsed) }),\n * });\n * ```\n */\nexport function createRepeatingTimer(\n\tduration: number,\n\toptions?: TimerOptions\n): Pick<TimerComponentTypes, 'timer'> {\n\treturn {\n\t\ttimer: {\n\t\t\telapsed: 0,\n\t\t\tduration,\n\t\t\trepeat: true,\n\t\t\tactive: true,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a timer plugin for ECSpresso.\n *\n * This plugin provides:\n * - Timer update system that processes all timer components each frame\n * - `justFinished` flag pattern for one-frame completion detection\n * - Automatic cleanup when entities are removed\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withPlugin(createTimerPlugin())\n * .build();\n *\n * // Spawn entity with timer\n * ecs.spawn({\n * ...createRepeatingTimer(5),\n * spawner: true,\n * });\n *\n * // React to timer completion in a system\n * ecs.addSystem('spawn-on-timer')\n * .addQuery('spawners', { with: ['timer', 'spawner'] })\n * .setProcess((queries, _dt, ecs) => {\n * for (const { components } of queries.spawners) {\n * if (components.timer.justFinished) {\n * ecs.spawn({ enemy: true });\n * }\n * }\n * });\n * ```\n */\nexport function createTimerPlugin<G extends string = 'timers'>(\n\toptions?: TimerPluginOptions<G>\n): Plugin<WorldConfigFrom<TimerComponentTypes>, EmptyConfig, 'timer-update', G> {\n\tconst {\n\t\tsystemGroup = 'timers',\n\t\tpriority = 0,\n\t\tphase = 'preUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin<WorldConfigFrom<TimerComponentTypes>, EmptyConfig, 'timer-update', G>({\n\t\tid: 'timers',\n\t\tinstall(world) {\n\t\t\tworld\n\t\t\t\t.addSystem('timer-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('timers', {\n\t\t\t\t\twith: ['timer'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.timers) {\n\t\t\t\t\t\tconst { timer } = entity.components;\n\n\t\t\t\t\t\t// Reset justFinished flag from previous frame\n\t\t\t\t\t\ttimer.justFinished = false;\n\n\t\t\t\t\t\t// Skip inactive timers\n\t\t\t\t\t\tif (!timer.active) continue;\n\n\t\t\t\t\t\t// Accumulate time\n\t\t\t\t\t\ttimer.elapsed += dt;\n\n\t\t\t\t\t\t// Check if timer completed\n\t\t\t\t\t\tif (timer.elapsed < timer.duration) continue;\n\n\t\t\t\t\t\t// Timer completed - handle based on repeat mode\n\t\t\t\t\t\tif (timer.repeat) {\n\t\t\t\t\t\t\t// Handle multiple cycles in one frame\n\t\t\t\t\t\t\twhile (timer.elapsed >= timer.duration) {\n\t\t\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\t\t\ttimer.onComplete?.({ entityId: entity.id, duration: timer.duration, elapsed: timer.elapsed });\n\t\t\t\t\t\t\t\ttimer.elapsed -= timer.duration;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// One-shot timer\n\t\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\t\ttimer.onComplete?.({ entityId: entity.id, duration: timer.duration, elapsed: timer.elapsed });\n\t\t\t\t\t\t\ttimer.active = false;\n\t\t\t\t\t\t\t// Auto-remove one-shot timer entities after completion.\n\t\t\t\t\t\t\t// If configurability is needed in the future, add an autoRemove option to TimerOptions.\n\t\t\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t},\n\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "2PAOA,uBAAS,kBAsGF,SAAS,CAAW,CAC1B,EACA,EACqC,CACrC,MAAO,CACN,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,CACD,EAwBM,SAAS,CAAoB,CACnC,EACA,EACqC,CACrC,MAAO,CACN,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,CACD,EAsCM,SAAS,CAA8C,CAC7D,EAC+E,CAC/E,IACC,cAAc,SACd,WAAW,EACX,QAAQ,aACL,GAAW,CAAC,EAEhB,OAAO,EAAmF,CACzF,GAAI,SACJ,OAAO,CAAC,EAAO,CACd,EACE,UAAU,cAAc,EACxB,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CACnB,KAAM,CAAC,OAAO,CACf,CAAC,EACA,WAAW,EAAG,UAAS,KAAI,SAAU,CACrC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAQ,SAAU,EAAO,WAMzB,GAHA,EAAM,aAAe,GAGjB,CAAC,EAAM,OAAQ,SAMnB,GAHA,EAAM,SAAW,EAGb,EAAM,QAAU,EAAM,SAAU,SAGpC,GAAI,EAAM,OAET,MAAO,EAAM,SAAW,EAAM,SAC7B,EAAM,aAAe,GACrB,EAAM,aAAa,CAAE,SAAU,EAAO,GAAI,SAAU,EAAM,SAAU,QAAS,EAAM,OAAQ,CAAC,EAC5F,EAAM,SAAW,EAAM,SAIxB,OAAM,aAAe,GACrB,EAAM,aAAa,CAAE,SAAU,EAAO,GAAI,SAAU,EAAM,SAAU,QAAS,EAAM,OAAQ,CAAC,EAC5F,EAAM,OAAS,GAGf,EAAI,SAAS,aAAa,EAAO,EAAE,GAGrC,EAEJ,CAAC",
|
|
8
|
+
"debugId": "8D3E01379540788564756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var N=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,q)=>(typeof require<"u"?require:k)[q]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as J}from"ecspresso";var S={x:0,y:0,rotation:0,scaleX:1,scaleY:1},U={x:0,y:0,rotation:0,scaleX:1,scaleY:1};function V(j,k){return{localTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function Z(j,k){return{worldTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function _(j,k,q){let z=q?.scale??q?.scaleX??1,D=q?.scale??q?.scaleY??1,C=q?.rotation??0,v={x:j,y:k,rotation:C,scaleX:z,scaleY:D};return{localTransform:{...v},worldTransform:{...v}}}function $(j){let{systemGroup:k="transform",priority:q=500,phase:z="postUpdate"}=j??{};return J({id:"transform",install(D){D.registerRequired("localTransform","worldTransform",(v)=>({x:v.x,y:v.y,rotation:v.rotation,scaleX:v.scaleX,scaleY:v.scaleY}));let C=[];D.addSystem("transform-propagation").setPriority(q).inPhase(z).inGroup(k).setProcess(({ecs:v})=>{K(v,C)})}})}function K(j,k){let q=j.entityManager;j.forEachInHierarchy((z,D)=>{let C=q.getComponent(z,"localTransform"),v=q.getComponent(z,"worldTransform");if(!C||!v)return;if(D===null)F(C,v);else{let E=q.getComponent(D,"worldTransform");if(E)M(E,C,v);else F(C,v)}j.markChanged(z,"worldTransform")}),q.getEntitiesWithQueryInto(k,["localTransform","worldTransform"]);for(let z of k)if(j.getParent(z.id)===null&&j.getChildren(z.id).length===0){let{localTransform:C,worldTransform:v}=z.components;F(C,v),j.markChanged(z.id,"worldTransform")}}function F(j,k){k.x=j.x,k.y=j.y,k.rotation=j.rotation,k.scaleX=j.scaleX,k.scaleY=j.scaleY}function M(j,k,q){let z=k.x*j.scaleX,D=k.y*j.scaleY,C=Math.cos(j.rotation),v=Math.sin(j.rotation),E=z*C-D*v,H=z*v+D*C;q.x=j.x+E,q.y=j.y+H,q.rotation=j.rotation+k.rotation,q.scaleX=j.scaleX*k.scaleX,q.scaleY=j.scaleY*k.scaleY}export{Z as createWorldTransform,$ as createTransformPlugin,_ as createTransform,V as createLocalTransform,U as DEFAULT_WORLD_TRANSFORM,S as DEFAULT_LOCAL_TRANSFORM};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=41A87FD60C5375A764756E2164756E21
|
|
4
4
|
//# sourceMappingURL=transform.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * Transform Plugin for ECSpresso\n *\n * Provides hierarchical transform propagation following Bevy's Transform/GlobalTransform pattern.\n * LocalTransform is modified by user code; WorldTransform is computed automatically.\n *\n * @see https://docs.rs/bevy/latest/bevy/transform/components/struct.GlobalTransform.html\n */\n\nimport { definePlugin, type Plugin, type BasePluginOptions } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../type-utils';\n\n// ==================== Component Types ====================\n\n/**\n * Local transform relative to parent (or world if no parent).\n * This is the transform you modify directly.\n */\nexport interface LocalTransform {\n\tx: number;\n\ty: number;\n\trotation: number;\n\tscaleX: number;\n\tscaleY: number;\n}\n\n/**\n * Computed world transform (accumulated from parent chain).\n * Read-only - managed by the transform propagation system.\n */\nexport interface WorldTransform {\n\tx: number;\n\ty: number;\n\trotation: number;\n\tscaleX: number;\n\tscaleY: number;\n}\n\n/**\n * Component types provided by the transform plugin.\n * Included automatically via `.withPlugin(createTransformPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withComponentTypes<{ sprite: Sprite; velocity: { x: number; y: number } }>()\n * .build();\n * ```\n */\nexport interface TransformComponentTypes {\n\tlocalTransform: LocalTransform;\n\tworldTransform: WorldTransform;\n}\n\n/**\n * WorldConfig representing the transform plugin's provided components.\n * Used as the `Requires` type parameter by plugins that depend on transform.\n */\nexport type TransformWorldConfig = WorldConfigFrom<TransformComponentTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the transform plugin.\n */\nexport interface TransformPluginOptions<G extends string = 'transform'> extends BasePluginOptions<G> {}\n\n// ==================== Default Values ====================\n\n/**\n * Default local transform values.\n */\nexport const DEFAULT_LOCAL_TRANSFORM: Readonly<LocalTransform> = {\n\tx: 0,\n\ty: 0,\n\trotation: 0,\n\tscaleX: 1,\n\tscaleY: 1,\n};\n\n/**\n * Default world transform values.\n */\nexport const DEFAULT_WORLD_TRANSFORM: Readonly<WorldTransform> = {\n\tx: 0,\n\ty: 0,\n\trotation: 0,\n\tscaleX: 1,\n\tscaleY: 1,\n};\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a local transform component with position only.\n * Uses default rotation (0) and scale (1, 1).\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createLocalTransform(100, 200),\n * sprite,\n * });\n * ```\n */\nexport function createLocalTransform(x: number, y: number): Pick<TransformComponentTypes, 'localTransform'> {\n\treturn {\n\t\tlocalTransform: {\n\t\t\tx,\n\t\t\ty,\n\t\t\trotation: 0,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t},\n\t};\n}\n\n/**\n * Create a world transform component with position only.\n * Typically used alongside createLocalTransform for initial state.\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createWorldTransform(x: number, y: number): Pick<TransformComponentTypes, 'worldTransform'> {\n\treturn {\n\t\tworldTransform: {\n\t\t\tx,\n\t\t\ty,\n\t\t\trotation: 0,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t},\n\t};\n}\n\n/**\n * Options for creating a full transform.\n */\nexport interface TransformOptions {\n\trotation?: number;\n\tscaleX?: number;\n\tscaleY?: number;\n\t/** Uniform scale (overrides scaleX/scaleY if provided) */\n\tscale?: number;\n}\n\n/**\n * Create both local and world transform components.\n * World transform is initialized to match local transform.\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @param options Optional rotation and scale\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * sprite,\n * });\n *\n * // With rotation and scale\n * ecs.spawn({\n * ...createTransform(100, 200, { rotation: Math.PI / 4, scale: 2 }),\n * sprite,\n * });\n * ```\n */\nexport function createTransform(\n\tx: number,\n\ty: number,\n\toptions?: TransformOptions\n): TransformComponentTypes {\n\tconst scaleX = options?.scale ?? options?.scaleX ?? 1;\n\tconst scaleY = options?.scale ?? options?.scaleY ?? 1;\n\tconst rotation = options?.rotation ?? 0;\n\n\tconst transform = {\n\t\tx,\n\t\ty,\n\t\trotation,\n\t\tscaleX,\n\t\tscaleY,\n\t};\n\n\treturn {\n\t\tlocalTransform: { ...transform },\n\t\tworldTransform: { ...transform },\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a transform plugin for ECSpresso.\n *\n * This plugin provides:\n * - Transform propagation system that computes world transforms from local transforms\n * - Parent-first traversal ensures parents are processed before children\n * - Supports full transform hierarchy (position, rotation, scale)\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createPhysics2DPlugin())\n * .build();\n *\n * // Spawn entity with transform\n * ecs.spawn({\n * ...createTransform(100, 200),\n * velocity: { x: 50, y: 0 },\n * });\n * ```\n */\nexport function createTransformPlugin<G extends string = 'transform'>(\n\toptions?: TransformPluginOptions<G>\n): Plugin<WorldConfigFrom<TransformComponentTypes>, EmptyConfig, 'transform-propagation', G> {\n\tconst {\n\t\tsystemGroup = 'transform',\n\t\tpriority = 500,\n\t\tphase = 'postUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin<WorldConfigFrom<TransformComponentTypes>, EmptyConfig, 'transform-propagation', G>({\n\t\tid: 'transform',\n\t\tinstall(world) {\n\t\t\t// localTransform requires worldTransform — initialize from localTransform values\n\t\t\tworld.registerRequired('localTransform', 'worldTransform', (lt) => ({\n\t\t\t\tx: lt.x, y: lt.y, rotation: lt.rotation, scaleX: lt.scaleX, scaleY: lt.scaleY,\n\t\t\t}));\n\n\t\t\tconst orphanBuffer: Array<import('../types').FilteredEntity<TransformComponentTypes, 'localTransform' | 'worldTransform'>> = [];\n\n\t\t\tworld\n\t\t\t\t.addSystem('transform-propagation')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tpropagateTransforms(ecs, orphanBuffer);\n\t\t\t\t});\n\t\t},\n\t});\n}\n\n/**\n * Propagate transforms through the hierarchy.\n * Parent-first traversal ensures parents are computed before children.\n *\n * Runs unconditionally for all entities with transforms — user code can\n * freely mutate localTransform without needing to call markChanged.\n * Marks worldTransform as changed so downstream systems (e.g. renderer\n * sync) pick up the updated values.\n */\nfunction propagateTransforms(\n\tecs: ECSpresso<WorldConfigFrom<TransformComponentTypes>>,\n\torphanBuffer: Array<import('../types').FilteredEntity<TransformComponentTypes, 'localTransform' | 'worldTransform'>>,\n): void {\n\tconst em = ecs.entityManager;\n\n\t// Use parent-first traversal for entities in hierarchy\n\tecs.forEachInHierarchy((entityId, parentId) => {\n\t\tconst localTransform = em.getComponent(entityId, 'localTransform');\n\t\tconst worldTransform = em.getComponent(entityId, 'worldTransform');\n\n\t\tif (!localTransform || !worldTransform) return;\n\n\t\tif (parentId === null) {\n\t\t\t// Root entity: world transform equals local transform\n\t\t\tcopyTransform(localTransform, worldTransform);\n\t\t} else {\n\t\t\t// Child entity: combine with parent's world transform\n\t\t\tconst parentWorld = em.getComponent(parentId, 'worldTransform');\n\t\t\tif (parentWorld) {\n\t\t\t\tcombineTransforms(parentWorld, localTransform, worldTransform);\n\t\t\t} else {\n\t\t\t\t// Parent has no world transform, treat as root\n\t\t\t\tcopyTransform(localTransform, worldTransform);\n\t\t\t}\n\t\t}\n\n\t\tecs.markChanged(entityId, 'worldTransform');\n\t});\n\n\t// Process orphaned entities (not in hierarchy but have transforms)\n\tem.getEntitiesWithQueryInto(orphanBuffer, ['localTransform', 'worldTransform']);\n\tfor (const entity of orphanBuffer) {\n\t\tconst parentId = ecs.getParent(entity.id);\n\t\t// Only process if truly orphaned (no parent and not a root with children)\n\t\tif (parentId === null && ecs.getChildren(entity.id).length === 0) {\n\t\t\tconst { localTransform, worldTransform } = entity.components;\n\t\t\tcopyTransform(localTransform, worldTransform);\n\t\t\tecs.markChanged(entity.id, 'worldTransform');\n\t\t}\n\t}\n}\n\n/**\n * Copy transform values from source to destination.\n */\nfunction copyTransform(src: LocalTransform, dest: WorldTransform): void {\n\tdest.x = src.x;\n\tdest.y = src.y;\n\tdest.rotation = src.rotation;\n\tdest.scaleX = src.scaleX;\n\tdest.scaleY = src.scaleY;\n}\n\n/**\n * Combine parent world transform with child local transform into child world transform.\n */\nfunction combineTransforms(\n\tparent: WorldTransform,\n\tlocal: LocalTransform,\n\tworld: WorldTransform\n): void {\n\t// Apply parent's scale to local position\n\tconst scaledLocalX = local.x * parent.scaleX;\n\tconst scaledLocalY = local.y * parent.scaleY;\n\n\t// Rotate local position by parent's rotation\n\tconst cos = Math.cos(parent.rotation);\n\tconst sin = Math.sin(parent.rotation);\n\tconst rotatedX = scaledLocalX * cos - scaledLocalY * sin;\n\tconst rotatedY = scaledLocalX * sin + scaledLocalY * cos;\n\n\t// Add to parent's position\n\tworld.x = parent.x + rotatedX;\n\tworld.y = parent.y + rotatedY;\n\tworld.rotation = parent.rotation + local.rotation;\n\tworld.scaleX = parent.scaleX * local.scaleX;\n\tworld.scaleY = parent.scaleY * local.scaleY;\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "41A87FD60C5375A764756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/plugins/tween.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var B=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(H,D)=>(typeof require<"u"?require:H)[D]}):z)(function(z){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+z+'" is not supported')});import{definePlugin as F}from"ecspresso";function $(z){return z}var V=1.70158,v=V*1.525,S=V+1;var f=2*Math.PI/3,g=2*Math.PI/4.5;function Q(z,H,D,J,M){let{from:N,easing:U=$,loop:W="once",loops:Y=1,onComplete:X}=M??{};return{tween:{steps:[{targets:[{component:z,path:H.split("."),from:N??null,to:D}],duration:J,easing:U}],currentStep:0,elapsed:0,loop:W,totalLoops:Y,completedLoops:0,direction:1,state:"pending",onComplete:X,justFinished:!1}}}function b(z,H){let{loop:D="once",loops:J=1,onComplete:M}=H??{};return{tween:{steps:z.map((N)=>({targets:N.targets.map((U)=>({component:U.component,path:U.field.split("."),from:U.from??null,to:U.to})),duration:N.duration,easing:N.easing??$})),currentStep:0,elapsed:0,loop:D,totalLoops:J,completedLoops:0,direction:1,state:"pending",onComplete:M,justFinished:!1}}}function d(z){return{createTween:Q,createTweenSequence:b}}var G={parent:{},key:""};function L(z,H){let D=H.length-1,J=z;for(let N=0;N<D;N++){let U=H[N];if(U===void 0)return null;let W=J[U];if(W===null||W===void 0||typeof W!=="object")return null;J=W}let M=H[D];if(M===void 0)return null;if(!(M in J))return null;return G.parent=J,G.key=M,G}function R(z,H){let D=L(z,H);if(!D)return null;let J=D.parent[D.key];return typeof J==="number"?J:null}function j(z,H,D){let J=L(z,H);if(!J)return!1;return J.parent[J.key]=D,!0}function h(z,H,D){return z<H?H:z>D?D:z}function E(z,H){for(let D of z.steps)for(let J of D.targets){if(J.from!==null)continue;let M=H[J.component];if(!M||typeof M!=="object")continue;let N=R(M,J.path);if(N!==null)J.from=N;else J.from=0}}function K(z,H,D,J,M){let N=z.easing(H);for(let U of z.targets){let W=D[U.component];if(!W||typeof W!=="object")continue;let Y=U.from??0,X=Y+(U.to-Y)*N;if(j(W,U.path,X))M.markChanged(J,U.component)}}function q(z,H,D,J){for(let M of z.targets){let N=H[M.component];if(!N||typeof N!=="object")continue;if(j(N,M.path,M.to))J.markChanged(D,M.component)}}function T(z){for(let H of z.steps)for(let D of H.targets){let J=D.from??0;D.from=D.to,D.to=J}}function P(z,H,D){z.state="complete",z.justFinished=!0,z.onComplete?.({entityId:H,stepCount:z.steps.length}),D.commands.removeComponent(H,"tween")}function A(z,H,D){if(z.completedLoops++,z.loop==="once")return P(z,H,D),!1;if(z.totalLoops>0&&z.completedLoops>=z.totalLoops)return P(z,H,D),!1;if(z.loop==="yoyo")z.direction=z.direction===1?-1:1,T(z);return z.currentStep=0,z.elapsed>0}function k(z,H,D,J){let M=z.currentStep+1;if(M<z.steps.length){z.currentStep=M;let N=z.steps[M];if(N)for(let U of N.targets){if(U.from!==null)continue;let W=H[U.component];if(!W||typeof W!=="object")continue;let Y=R(W,U.path);U.from=Y??0}return!0}return A(z,D,J)}function x(z,H,D,J){while(!0){let M=z.steps[z.currentStep];if(!M)return;if(M.duration<=0){if(q(M,H,D,J),z.elapsed=0,!k(z,H,D,J))return;continue}if(z.elapsed>=M.duration){q(M,H,D,J);let U=z.elapsed-M.duration;if(z.elapsed=U,!k(z,H,D,J))return;continue}let N=h(z.elapsed/M.duration,0,1);K(M,N,H,D,J);return}}function m(z){let{systemGroup:H="tweens",priority:D=0,phase:J="update"}=z??{};return F({id:"tweens",install(M){M.addSystem("tween-update").setPriority(D).inPhase(J).inGroup(H).addQuery("tweens",{with:["tween"]}).setProcess(({queries:N,dt:U,ecs:W})=>{for(let Y of N.tweens){let X=Y.components.tween,Z=Y.components;if(X.justFinished){X.justFinished=!1;continue}if(X.state==="complete")continue;if(X.state==="pending")E(X,Z),X.state="active";if(!X.steps[X.currentStep])continue;X.elapsed+=U,x(X,Z,Y.id,W)}})}})}export{b as createTweenSequence,m as createTweenPlugin,d as createTweenHelpers,Q as createTween};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=7ABCB10A70F33E4664756E2164756E21
|
|
4
4
|
//# sourceMappingURL=tween.js.map
|