ecspresso 0.13.0 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/ai/detection.d.ts +118 -0
- package/dist/plugins/ai/detection.js +4 -0
- package/dist/plugins/ai/detection.js.map +10 -0
- package/dist/plugins/{audio.js → audio/audio.js} +1 -1
- package/dist/plugins/{audio.js.map → audio/audio.js.map} +2 -2
- package/dist/plugins/combat/health.d.ts +98 -0
- package/dist/plugins/combat/health.js +4 -0
- package/dist/plugins/combat/health.js.map +10 -0
- package/dist/plugins/combat/projectile.d.ts +115 -0
- package/dist/plugins/combat/projectile.js +4 -0
- package/dist/plugins/combat/projectile.js.map +10 -0
- package/dist/plugins/{diagnostics.js → debug/diagnostics.js} +1 -1
- package/dist/plugins/{diagnostics.js.map → debug/diagnostics.js.map} +2 -2
- package/dist/plugins/{input.js → input/input.js} +1 -1
- package/dist/plugins/{input.js.map → input/input.js.map} +2 -2
- package/dist/plugins/input/selection.d.ts +114 -0
- package/dist/plugins/input/selection.js +4 -0
- package/dist/plugins/input/selection.js.map +11 -0
- package/dist/plugins/isometric/depth-sort.d.ts +44 -0
- package/dist/plugins/isometric/depth-sort.js +4 -0
- package/dist/plugins/isometric/depth-sort.js.map +10 -0
- package/dist/plugins/isometric/projection.d.ts +102 -0
- package/dist/plugins/isometric/projection.js +4 -0
- package/dist/plugins/isometric/projection.js.map +10 -0
- package/dist/plugins/{collision.d.ts → physics/collision.d.ts} +1 -1
- package/dist/plugins/{collision.js → physics/collision.js} +1 -1
- package/dist/plugins/{collision.js.map → physics/collision.js.map} +3 -3
- package/dist/plugins/{physics2D.d.ts → physics/physics2D.d.ts} +1 -1
- package/dist/plugins/{physics2D.js → physics/physics2D.js} +1 -1
- package/dist/plugins/{physics2D.js.map → physics/physics2D.js.map} +3 -3
- package/dist/plugins/physics/steering.d.ts +102 -0
- package/dist/plugins/physics/steering.js +4 -0
- package/dist/plugins/physics/steering.js.map +10 -0
- package/dist/plugins/{particles.d.ts → rendering/particles.d.ts} +2 -2
- package/dist/plugins/{particles.js → rendering/particles.js} +1 -1
- package/dist/plugins/rendering/particles.js.map +10 -0
- package/dist/plugins/{renderers → rendering}/renderer2D.d.ts +9 -5
- package/dist/plugins/rendering/renderer2D.js +4 -0
- package/dist/plugins/rendering/renderer2D.js.map +10 -0
- package/dist/plugins/{sprite-animation.js → rendering/sprite-animation.js} +1 -1
- package/dist/plugins/{sprite-animation.js.map → rendering/sprite-animation.js.map} +2 -2
- package/dist/plugins/{coroutine.js → scripting/coroutine.js} +1 -1
- package/dist/plugins/{coroutine.js.map → scripting/coroutine.js.map} +2 -2
- package/dist/plugins/{state-machine.js → scripting/state-machine.js} +1 -1
- package/dist/plugins/{state-machine.js.map → scripting/state-machine.js.map} +2 -2
- package/dist/plugins/{timers.js → scripting/timers.js} +1 -1
- package/dist/plugins/{timers.js.map → scripting/timers.js.map} +2 -2
- package/dist/plugins/{tween.d.ts → scripting/tween.d.ts} +1 -1
- package/dist/plugins/{tween.js → scripting/tween.js} +1 -1
- package/dist/plugins/scripting/tween.js.map +11 -0
- package/dist/plugins/{bounds.js → spatial/bounds.js} +1 -1
- package/dist/plugins/{bounds.js.map → spatial/bounds.js.map} +2 -2
- package/dist/plugins/{camera.d.ts → spatial/camera.d.ts} +52 -12
- package/dist/plugins/spatial/camera.js +4 -0
- package/dist/plugins/spatial/camera.js.map +10 -0
- package/dist/plugins/{spatial-index.d.ts → spatial/spatial-index.d.ts} +2 -2
- package/dist/plugins/{spatial-index.js → spatial/spatial-index.js} +1 -1
- package/dist/plugins/{spatial-index.js.map → spatial/spatial-index.js.map} +3 -3
- package/dist/plugins/{transform.d.ts → spatial/transform.d.ts} +1 -1
- package/dist/plugins/{transform.js → spatial/transform.js} +1 -1
- package/dist/plugins/spatial/transform.js.map +10 -0
- package/package.json +78 -50
- package/dist/plugins/camera.js +0 -4
- package/dist/plugins/camera.js.map +0 -10
- package/dist/plugins/particles.js.map +0 -10
- package/dist/plugins/renderers/renderer2D.js +0 -4
- package/dist/plugins/renderers/renderer2D.js.map +0 -10
- package/dist/plugins/transform.js.map +0 -10
- package/dist/plugins/tween.js.map +0 -11
- /package/dist/plugins/{audio.d.ts → audio/audio.d.ts} +0 -0
- /package/dist/plugins/{diagnostics.d.ts → debug/diagnostics.d.ts} +0 -0
- /package/dist/plugins/{input.d.ts → input/input.d.ts} +0 -0
- /package/dist/plugins/{sprite-animation.d.ts → rendering/sprite-animation.d.ts} +0 -0
- /package/dist/plugins/{coroutine.d.ts → scripting/coroutine.d.ts} +0 -0
- /package/dist/plugins/{state-machine.d.ts → scripting/state-machine.d.ts} +0 -0
- /package/dist/plugins/{timers.d.ts → scripting/timers.d.ts} +0 -0
- /package/dist/plugins/{bounds.d.ts → spatial/bounds.d.ts} +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/plugins/physics2D.ts", "../src/utils/narrowphase.ts"],
|
|
3
|
+
"sources": ["../src/plugins/physics/physics2D.ts", "../src/utils/narrowphase.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Physics 2D Plugin for ECSpresso\n *\n * Provides ECS-native arcade physics: gravity, forces, drag, semi-implicit Euler\n * integration, and impulse-based collision response with friction.\n *\n * Reuses collider types from the collision plugin for shape definitions.\n * Has its own collision detection in fixedUpdate for physics response;\n * the existing collision plugin can still run in postUpdate for game logic events.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { TransformComponentTypes, TransformWorldConfig } from './transform';\nimport type { CollisionComponentTypes, LayerFactories } from './collision';\nimport type { Vector2D } from 'ecspresso';\nimport { fillBaseColliderInfo, detectCollisions, tryGetSpatialIndex, AABB_SHAPE, type Contact, type BaseColliderInfo } from '../utils/narrowphase';\n\n// ==================== Component Types ====================\n\n/**\n * Rigid body types for physics simulation.\n * - 'dynamic': Fully simulated (gravity, forces, collisions)\n * - 'kinematic': Moves via velocity only (ignores gravity/forces, immovable in collisions)\n * - 'static': Immovable (ignores gravity, forces, and velocity)\n */\nexport type BodyType = 'dynamic' | 'kinematic' | 'static';\n\n/**\n * Rigid body component controlling physics behavior.\n */\nexport interface RigidBody {\n\ttype: BodyType;\n\t/** Mass in arbitrary units. Affects force→acceleration. Infinity = immovable. */\n\tmass: number;\n\t/** Linear velocity damping coefficient (units/sec, 0 = none) */\n\tdrag: number;\n\t/** Bounciness 0–1 (0 = no bounce, 1 = perfectly elastic) */\n\trestitution: number;\n\t/** Surface friction coefficient 0–1 */\n\tfriction: number;\n\t/** Per-entity gravity multiplier (0 = no gravity) */\n\tgravityScale: number;\n}\n\n/**\n * Component types directly provided by the physics plugin.\n */\nexport interface Physics2DOwnComponentTypes {\n\trigidBody: RigidBody;\n\tvelocity: Vector2D;\n\tforce: Vector2D;\n}\n\n/**\n * Full component types available when using the physics plugin\n * (own components + transform + collision dependencies).\n * Convenience alias for consumer code.\n */\nexport interface Physics2DComponentTypes<L extends string = never> extends TransformComponentTypes, CollisionComponentTypes<L>, Physics2DOwnComponentTypes {}\n\n// ==================== Resource Types ====================\n\n/**\n * Physics configuration resource.\n */\nexport interface Physics2DConfig {\n\tgravity: Vector2D;\n}\n\nexport interface Physics2DResourceTypes {\n\tphysicsConfig: Physics2DConfig;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event emitted for each physics collision pair.\n *\n * Normal components are flattened (`normalX`/`normalY`) rather than nested\n * in a `Vector2D` to avoid a per-event allocation in the physics hot path.\n */\nexport interface Physics2DCollisionEvent {\n\tentityA: number;\n\tentityB: number;\n\t/** Unit normal X, pointing from A toward B */\n\tnormalX: number;\n\t/** Unit normal Y, pointing from A toward B */\n\tnormalY: number;\n\t/** Penetration depth (positive) */\n\tdepth: number;\n}\n\nexport interface Physics2DEventTypes {\n\tphysicsCollision: Physics2DCollisionEvent;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface Physics2DPluginOptions<G extends string = 'physics2D', CG extends string = never> {\n\t/** World gravity vector (default: {x: 0, y: 0}) */\n\tgravity?: Vector2D;\n\t/** System group name (default: 'physics2D') */\n\tsystemGroup?: G;\n\t/** Additional group for the collision system only (default: none).\n\t * When set, the collision system belongs to both `systemGroup` and this group,\n\t * allowing independent enable/disable of collision detection. */\n\tcollisionSystemGroup?: CG;\n\t/** Priority for integration system (default: 1000) */\n\tintegrationPriority?: number;\n\t/** Priority for collision system (default: 900) */\n\tcollisionPriority?: number;\n\t/** Execution phase (default: 'fixedUpdate') */\n\tphase?: SystemPhase;\n}\n\n// ==================== Helper Functions ====================\n\nexport interface RigidBodyOptions {\n\tmass?: number;\n\tdrag?: number;\n\trestitution?: number;\n\tfriction?: number;\n\tgravityScale?: number;\n}\n\n/**\n * Create a rigid body + force component pair.\n * Static bodies automatically get mass=Infinity.\n */\nexport function createRigidBody(\n\ttype: BodyType,\n\toptions?: RigidBodyOptions,\n): { rigidBody: RigidBody; force: Vector2D } {\n\treturn {\n\t\trigidBody: {\n\t\t\ttype,\n\t\t\tmass: type === 'static' ? Infinity : (options?.mass ?? 1),\n\t\t\tdrag: options?.drag ?? 0,\n\t\t\trestitution: options?.restitution ?? 0,\n\t\t\tfriction: options?.friction ?? 0,\n\t\t\tgravityScale: options?.gravityScale ?? 1,\n\t\t},\n\t\tforce: { x: 0, y: 0 },\n\t};\n}\n\n/**\n * Create a force component with initial values.\n */\nexport function createForce(x: number, y: number): { force: Vector2D } {\n\treturn { force: { x, y } };\n}\n\n/**\n * Accumulate a force onto an entity's force component.\n */\nexport function applyForce(\n\tecs: { getComponent(id: number, name: 'force'): Vector2D | undefined },\n\tentityId: number,\n\tfx: number,\n\tfy: number,\n): void {\n\tconst force = ecs.getComponent(entityId, 'force');\n\tif (!force) return;\n\tforce.x += fx;\n\tforce.y += fy;\n}\n\n/**\n * Apply an instantaneous impulse: velocity += impulse / mass.\n */\nexport function applyImpulse(\n\tecs: {\n\t\tgetComponent(id: number, name: 'velocity'): Vector2D | undefined;\n\t\tgetComponent(id: number, name: 'rigidBody'): RigidBody | undefined;\n\t},\n\tentityId: number,\n\tix: number,\n\tiy: number,\n): void {\n\tconst velocity = ecs.getComponent(entityId, 'velocity');\n\tconst rigidBody = ecs.getComponent(entityId, 'rigidBody');\n\tif (!velocity || !rigidBody) return;\n\tif (rigidBody.mass === Infinity || rigidBody.mass === 0) return;\n\tvelocity.x += ix / rigidBody.mass;\n\tvelocity.y += iy / rigidBody.mass;\n}\n\n/**\n * Directly set an entity's velocity.\n */\nexport function setVelocity(\n\tecs: { getComponent(id: number, name: 'velocity'): Vector2D | undefined },\n\tentityId: number,\n\tvx: number,\n\tvy: number,\n): void {\n\tconst velocity = ecs.getComponent(entityId, 'velocity');\n\tif (!velocity) return;\n\tvelocity.x = vx;\n\tvelocity.y = vy;\n}\n\n// ==================== Internal: Collider Info ====================\n\ninterface Physics2DColliderInfo<L extends string = string> extends BaseColliderInfo<L> {\n\trigidBody: RigidBody;\n\tvelocity: Vector2D;\n}\n\n// ==================== Collision Response ====================\n\ninterface PhysicsEcsLike {\n\tgetComponent(id: number, name: 'localTransform'): { x: number; y: number } | undefined;\n\teventBus: { publish(event: 'physicsCollision', data: Physics2DCollisionEvent): void };\n\tmarkChanged(entityId: number, componentName: 'localTransform' | 'velocity'): void;\n}\n\n/**\n * Resolve a physics collision pair: position correction, impulse response, event.\n */\nfunction resolvePhysicsContact(\n\ta: Physics2DColliderInfo,\n\tb: Physics2DColliderInfo,\n\tcontact: Contact,\n\tecs: PhysicsEcsLike,\n): void {\n\tconst invMassA = (a.rigidBody.type === 'dynamic' && a.rigidBody.mass > 0 && a.rigidBody.mass !== Infinity)\n\t\t? 1 / a.rigidBody.mass\n\t\t: 0;\n\tconst invMassB = (b.rigidBody.type === 'dynamic' && b.rigidBody.mass > 0 && b.rigidBody.mass !== Infinity)\n\t\t? 1 / b.rigidBody.mass\n\t\t: 0;\n\tconst totalInvMass = invMassA + invMassB;\n\n\t// Position correction\n\tif (totalInvMass > 0) {\n\t\tconst correctionScale = contact.depth / totalInvMass;\n\n\t\tif (invMassA > 0) {\n\t\t\tconst ltA = ecs.getComponent(a.entityId, 'localTransform');\n\t\t\tif (!ltA) return;\n\t\t\tltA.x -= correctionScale * invMassA * contact.normalX;\n\t\t\tltA.y -= correctionScale * invMassA * contact.normalY;\n\t\t\t// Update cached position for subsequent pairs (collider offset already baked in)\n\t\t\ta.x = ltA.x;\n\t\t\tecs.markChanged(a.entityId, 'localTransform');\n\t\t}\n\n\t\tif (invMassB > 0) {\n\t\t\tconst ltB = ecs.getComponent(b.entityId, 'localTransform');\n\t\t\tif (!ltB) return;\n\t\t\tltB.x += correctionScale * invMassB * contact.normalX;\n\t\t\tltB.y += correctionScale * invMassB * contact.normalY;\n\t\t\tecs.markChanged(b.entityId, 'localTransform');\n\t\t}\n\n\t\t// Velocity response (impulse-based)\n\t\tconst relVelX = b.velocity.x - a.velocity.x;\n\t\tconst relVelY = b.velocity.y - a.velocity.y;\n\t\tconst velAlongNormal = relVelX * contact.normalX + relVelY * contact.normalY;\n\n\t\tif (velAlongNormal < 0) {\n\t\t\tconst restitution = Math.min(a.rigidBody.restitution, b.rigidBody.restitution);\n\t\t\tconst normalImpulse = -(1 + restitution) * velAlongNormal / totalInvMass;\n\n\t\t\ta.velocity.x -= normalImpulse * invMassA * contact.normalX;\n\t\t\ta.velocity.y -= normalImpulse * invMassA * contact.normalY;\n\t\t\tb.velocity.x += normalImpulse * invMassB * contact.normalX;\n\t\t\tb.velocity.y += normalImpulse * invMassB * contact.normalY;\n\n\t\t\t// Friction (tangential impulse)\n\t\t\tconst tangentX = relVelX - velAlongNormal * contact.normalX;\n\t\t\tconst tangentY = relVelY - velAlongNormal * contact.normalY;\n\t\t\tconst tangentSpeed = Math.sqrt(tangentX * tangentX + tangentY * tangentY);\n\n\t\t\tif (tangentSpeed > 1e-6) {\n\t\t\t\tconst tangentNX = tangentX / tangentSpeed;\n\t\t\t\tconst tangentNY = tangentY / tangentSpeed;\n\t\t\t\tconst friction = Math.sqrt(a.rigidBody.friction * b.rigidBody.friction);\n\t\t\t\tconst maxFrictionImpulse = friction * Math.abs(normalImpulse);\n\t\t\t\tconst tangentImpulse = Math.min(tangentSpeed / totalInvMass, maxFrictionImpulse);\n\n\t\t\t\ta.velocity.x += tangentImpulse * invMassA * tangentNX;\n\t\t\t\ta.velocity.y += tangentImpulse * invMassA * tangentNY;\n\t\t\t\tb.velocity.x -= tangentImpulse * invMassB * tangentNX;\n\t\t\t\tb.velocity.y -= tangentImpulse * invMassB * tangentNY;\n\t\t\t}\n\t\t}\n\n\t\tecs.markChanged(a.entityId, 'velocity');\n\t\tecs.markChanged(b.entityId, 'velocity');\n\t}\n\n\tecs.eventBus.publish('physicsCollision', {\n\t\tentityA: a.entityId,\n\t\tentityB: b.entityId,\n\t\tnormalX: contact.normalX,\n\t\tnormalY: contact.normalY,\n\t\tdepth: contact.depth,\n\t});\n}\n\n// ==================== Module-level Physics Callback ====================\n\nfunction onPhysicsContact(\n\ta: Physics2DColliderInfo,\n\tb: Physics2DColliderInfo,\n\tcontact: Contact,\n\tecs: PhysicsEcsLike,\n): void {\n\tresolvePhysicsContact(a, b, contact, ecs);\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a 2D physics plugin for ECSpresso.\n *\n * Provides:\n * - Semi-implicit Euler integration (gravity, forces, drag → velocity → position)\n * - Impulse-based collision response with restitution and friction\n * - physicsCollision events with contact normal and depth\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createPhysics2DPlugin({ gravity: { x: 0, y: 980 } }))\n * .withFixedTimestep(1/60)\n * .build();\n *\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createRigidBody('dynamic', { mass: 1, restitution: 0.5 }),\n * velocity: { x: 0, y: 0 },\n * ...createAABBCollider(32, 32),\n * ...createCollisionLayer('player', ['ground']),\n * });\n * ```\n */\n\ntype Physics2DProvides<L extends string = never> = Physics2DOwnComponentTypes & CollisionComponentTypes<L>;\n\nexport function createPhysics2DPlugin<L extends string = never, G extends string = 'physics2D', CG extends string = never>(\n\toptions?: Physics2DPluginOptions<G, CG> & { layers?: LayerFactories<Record<L, readonly string[]>> },\n) {\n\tconst {\n\t\tgravity = { x: 0, y: 0 },\n\t\tsystemGroup = 'physics2D',\n\t\tcollisionSystemGroup,\n\t\tintegrationPriority = 1000,\n\t\tcollisionPriority = 900,\n\t\tphase = 'fixedUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('physics2D')\n\t\t.withComponentTypes<Physics2DProvides<L>>()\n\t\t.withEventTypes<Physics2DEventTypes>()\n\t\t.withResourceTypes<Physics2DResourceTypes>()\n\t\t.withLabels<'physics2D-integration' | 'physics2D-collision'>()\n\t\t.withGroups<G | CG>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// rigidBody requires velocity and force — auto-add with zero defaults\n\t\t\tworld.registerRequired('rigidBody', 'velocity', () => ({ x: 0, y: 0 }));\n\t\t\tworld.registerRequired('rigidBody', 'force', () => ({ x: 0, y: 0 }));\n\n\t\t\tworld.addResource('physicsConfig', { gravity: { x: gravity.x, y: gravity.y } });\n\n\t\t\t// ==================== Integration System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('physics2D-integration')\n\t\t\t\t.setPriority(integrationPriority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('bodies', {\n\t\t\t\t\twith: ['localTransform', 'velocity', 'rigidBody', 'force'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tconst config = ecs.getResource('physicsConfig');\n\t\t\t\t\tconst gx = config.gravity.x;\n\t\t\t\t\tconst gy = config.gravity.y;\n\n\t\t\t\t\tfor (const entity of queries.bodies) {\n\t\t\t\t\t\tconst { localTransform, velocity, rigidBody, force } = entity.components;\n\n\t\t\t\t\t\t// Static bodies: skip entirely\n\t\t\t\t\t\tif (rigidBody.type === 'static') continue;\n\n\t\t\t\t\t\t// Dynamic bodies: apply gravity, forces, drag\n\t\t\t\t\t\tif (rigidBody.type === 'dynamic') {\n\t\t\t\t\t\t\t// 1. Gravity\n\t\t\t\t\t\t\tvelocity.x += gx * rigidBody.gravityScale * dt;\n\t\t\t\t\t\t\tvelocity.y += gy * rigidBody.gravityScale * dt;\n\n\t\t\t\t\t\t\t// 2. Forces (F = ma → a = F/m)\n\t\t\t\t\t\t\tif (rigidBody.mass > 0 && rigidBody.mass !== Infinity) {\n\t\t\t\t\t\t\t\tvelocity.x += (force.x / rigidBody.mass) * dt;\n\t\t\t\t\t\t\t\tvelocity.y += (force.y / rigidBody.mass) * dt;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// 3. Drag\n\t\t\t\t\t\t\tif (rigidBody.drag > 0) {\n\t\t\t\t\t\t\t\tconst damping = Math.max(0, 1 - rigidBody.drag * dt);\n\t\t\t\t\t\t\t\tvelocity.x *= damping;\n\t\t\t\t\t\t\t\tvelocity.y *= damping;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Both dynamic and kinematic: integrate position\n\t\t\t\t\t\tlocalTransform.x += velocity.x * dt;\n\t\t\t\t\t\tlocalTransform.y += velocity.y * dt;\n\n\t\t\t\t\t\t// Clear accumulated forces\n\t\t\t\t\t\tforce.x = 0;\n\t\t\t\t\t\tforce.y = 0;\n\n\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Collision System ====================\n\n\t\t\tconst collisionSystem = world\n\t\t\t\t.addSystem('physics2D-collision')\n\t\t\t\t.setPriority(collisionPriority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup);\n\n\t\t\tif (collisionSystemGroup) {\n\t\t\t\tcollisionSystem.inGroup(collisionSystemGroup);\n\t\t\t}\n\n\t\t\t// Grow-only pool of Physics2DColliderInfo slots reused across frames.\n\t\t\t// Steady-state: zero allocations per frame once the pool is warm.\n\t\t\tconst colliderPool: Physics2DColliderInfo<L>[] = [];\n\t\t\t// Reusable entityId → collider lookup for the broadphase path.\n\t\t\tconst broadphaseMap = new Map<number, Physics2DColliderInfo<L>>();\n\n\t\t\tcollisionSystem\n\t\t\t\t.addQuery('collidables', {\n\t\t\t\t\twith: ['localTransform', 'rigidBody', 'velocity', 'collisionLayer'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tlet count = 0;\n\n\t\t\t\t\tfor (const entity of queries.collidables) {\n\t\t\t\t\t\tconst { localTransform, rigidBody, velocity, collisionLayer } = entity.components;\n\t\t\t\t\t\tconst aabb = ecs.getComponent(entity.id, 'aabbCollider');\n\t\t\t\t\t\tconst circle = ecs.getComponent(entity.id, 'circleCollider');\n\t\t\t\t\t\tif (!aabb && !circle) continue;\n\n\t\t\t\t\t\tlet slot = colliderPool[count];\n\t\t\t\t\t\tif (!slot) {\n\t\t\t\t\t\t\tslot = {\n\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\tx: localTransform.x,\n\t\t\t\t\t\t\t\ty: localTransform.y,\n\t\t\t\t\t\t\t\tlayer: collisionLayer.layer,\n\t\t\t\t\t\t\t\tcollidesWith: collisionLayer.collidesWith,\n\t\t\t\t\t\t\t\tshape: AABB_SHAPE,\n\t\t\t\t\t\t\t\thalfWidth: 0,\n\t\t\t\t\t\t\t\thalfHeight: 0,\n\t\t\t\t\t\t\t\tradius: 0,\n\t\t\t\t\t\t\t\trigidBody,\n\t\t\t\t\t\t\t\tvelocity,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tcolliderPool[count] = slot;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tslot.rigidBody = rigidBody;\n\t\t\t\t\t\t\tslot.velocity = velocity;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!fillBaseColliderInfo(\n\t\t\t\t\t\t\tslot,\n\t\t\t\t\t\t\tentity.id, localTransform.x, localTransform.y,\n\t\t\t\t\t\t\tcollisionLayer.layer, collisionLayer.collidesWith,\n\t\t\t\t\t\t\taabb, circle,\n\t\t\t\t\t\t)) continue;\n\n\t\t\t\t\t\tcount++;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst si = tryGetSpatialIndex(ecs.tryGetResource.bind(ecs));\n\t\t\t\t\tdetectCollisions(colliderPool, count, broadphaseMap, si, onPhysicsContact, ecs);\n\t\t\t\t});\n\t\t});\n}\n",
|
|
5
|
+
"/**\n * Physics 2D Plugin for ECSpresso\n *\n * Provides ECS-native arcade physics: gravity, forces, drag, semi-implicit Euler\n * integration, and impulse-based collision response with friction.\n *\n * Reuses collider types from the collision plugin for shape definitions.\n * Has its own collision detection in fixedUpdate for physics response;\n * the existing collision plugin can still run in postUpdate for game logic events.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { TransformComponentTypes, TransformWorldConfig } from '../spatial/transform';\nimport type { CollisionComponentTypes, LayerFactories } from './collision';\nimport type { Vector2D } from 'ecspresso';\nimport { fillBaseColliderInfo, detectCollisions, tryGetSpatialIndex, AABB_SHAPE, type Contact, type BaseColliderInfo } from '../../utils/narrowphase';\n\n// ==================== Component Types ====================\n\n/**\n * Rigid body types for physics simulation.\n * - 'dynamic': Fully simulated (gravity, forces, collisions)\n * - 'kinematic': Moves via velocity only (ignores gravity/forces, immovable in collisions)\n * - 'static': Immovable (ignores gravity, forces, and velocity)\n */\nexport type BodyType = 'dynamic' | 'kinematic' | 'static';\n\n/**\n * Rigid body component controlling physics behavior.\n */\nexport interface RigidBody {\n\ttype: BodyType;\n\t/** Mass in arbitrary units. Affects force→acceleration. Infinity = immovable. */\n\tmass: number;\n\t/** Linear velocity damping coefficient (units/sec, 0 = none) */\n\tdrag: number;\n\t/** Bounciness 0–1 (0 = no bounce, 1 = perfectly elastic) */\n\trestitution: number;\n\t/** Surface friction coefficient 0–1 */\n\tfriction: number;\n\t/** Per-entity gravity multiplier (0 = no gravity) */\n\tgravityScale: number;\n}\n\n/**\n * Component types directly provided by the physics plugin.\n */\nexport interface Physics2DOwnComponentTypes {\n\trigidBody: RigidBody;\n\tvelocity: Vector2D;\n\tforce: Vector2D;\n}\n\n/**\n * Full component types available when using the physics plugin\n * (own components + transform + collision dependencies).\n * Convenience alias for consumer code.\n */\nexport interface Physics2DComponentTypes<L extends string = never> extends TransformComponentTypes, CollisionComponentTypes<L>, Physics2DOwnComponentTypes {}\n\n// ==================== Resource Types ====================\n\n/**\n * Physics configuration resource.\n */\nexport interface Physics2DConfig {\n\tgravity: Vector2D;\n}\n\nexport interface Physics2DResourceTypes {\n\tphysicsConfig: Physics2DConfig;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event emitted for each physics collision pair.\n *\n * Normal components are flattened (`normalX`/`normalY`) rather than nested\n * in a `Vector2D` to avoid a per-event allocation in the physics hot path.\n */\nexport interface Physics2DCollisionEvent {\n\tentityA: number;\n\tentityB: number;\n\t/** Unit normal X, pointing from A toward B */\n\tnormalX: number;\n\t/** Unit normal Y, pointing from A toward B */\n\tnormalY: number;\n\t/** Penetration depth (positive) */\n\tdepth: number;\n}\n\nexport interface Physics2DEventTypes {\n\tphysicsCollision: Physics2DCollisionEvent;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface Physics2DPluginOptions<G extends string = 'physics2D', CG extends string = never> {\n\t/** World gravity vector (default: {x: 0, y: 0}) */\n\tgravity?: Vector2D;\n\t/** System group name (default: 'physics2D') */\n\tsystemGroup?: G;\n\t/** Additional group for the collision system only (default: none).\n\t * When set, the collision system belongs to both `systemGroup` and this group,\n\t * allowing independent enable/disable of collision detection. */\n\tcollisionSystemGroup?: CG;\n\t/** Priority for integration system (default: 1000) */\n\tintegrationPriority?: number;\n\t/** Priority for collision system (default: 900) */\n\tcollisionPriority?: number;\n\t/** Execution phase (default: 'fixedUpdate') */\n\tphase?: SystemPhase;\n}\n\n// ==================== Helper Functions ====================\n\nexport interface RigidBodyOptions {\n\tmass?: number;\n\tdrag?: number;\n\trestitution?: number;\n\tfriction?: number;\n\tgravityScale?: number;\n}\n\n/**\n * Create a rigid body + force component pair.\n * Static bodies automatically get mass=Infinity.\n */\nexport function createRigidBody(\n\ttype: BodyType,\n\toptions?: RigidBodyOptions,\n): { rigidBody: RigidBody; force: Vector2D } {\n\treturn {\n\t\trigidBody: {\n\t\t\ttype,\n\t\t\tmass: type === 'static' ? Infinity : (options?.mass ?? 1),\n\t\t\tdrag: options?.drag ?? 0,\n\t\t\trestitution: options?.restitution ?? 0,\n\t\t\tfriction: options?.friction ?? 0,\n\t\t\tgravityScale: options?.gravityScale ?? 1,\n\t\t},\n\t\tforce: { x: 0, y: 0 },\n\t};\n}\n\n/**\n * Create a force component with initial values.\n */\nexport function createForce(x: number, y: number): { force: Vector2D } {\n\treturn { force: { x, y } };\n}\n\n/**\n * Accumulate a force onto an entity's force component.\n */\nexport function applyForce(\n\tecs: { getComponent(id: number, name: 'force'): Vector2D | undefined },\n\tentityId: number,\n\tfx: number,\n\tfy: number,\n): void {\n\tconst force = ecs.getComponent(entityId, 'force');\n\tif (!force) return;\n\tforce.x += fx;\n\tforce.y += fy;\n}\n\n/**\n * Apply an instantaneous impulse: velocity += impulse / mass.\n */\nexport function applyImpulse(\n\tecs: {\n\t\tgetComponent(id: number, name: 'velocity'): Vector2D | undefined;\n\t\tgetComponent(id: number, name: 'rigidBody'): RigidBody | undefined;\n\t},\n\tentityId: number,\n\tix: number,\n\tiy: number,\n): void {\n\tconst velocity = ecs.getComponent(entityId, 'velocity');\n\tconst rigidBody = ecs.getComponent(entityId, 'rigidBody');\n\tif (!velocity || !rigidBody) return;\n\tif (rigidBody.mass === Infinity || rigidBody.mass === 0) return;\n\tvelocity.x += ix / rigidBody.mass;\n\tvelocity.y += iy / rigidBody.mass;\n}\n\n/**\n * Directly set an entity's velocity.\n */\nexport function setVelocity(\n\tecs: { getComponent(id: number, name: 'velocity'): Vector2D | undefined },\n\tentityId: number,\n\tvx: number,\n\tvy: number,\n): void {\n\tconst velocity = ecs.getComponent(entityId, 'velocity');\n\tif (!velocity) return;\n\tvelocity.x = vx;\n\tvelocity.y = vy;\n}\n\n// ==================== Internal: Collider Info ====================\n\ninterface Physics2DColliderInfo<L extends string = string> extends BaseColliderInfo<L> {\n\trigidBody: RigidBody;\n\tvelocity: Vector2D;\n}\n\n// ==================== Collision Response ====================\n\ninterface PhysicsEcsLike {\n\tgetComponent(id: number, name: 'localTransform'): { x: number; y: number } | undefined;\n\teventBus: { publish(event: 'physicsCollision', data: Physics2DCollisionEvent): void };\n\tmarkChanged(entityId: number, componentName: 'localTransform' | 'velocity'): void;\n}\n\n/**\n * Resolve a physics collision pair: position correction, impulse response, event.\n */\nfunction resolvePhysicsContact(\n\ta: Physics2DColliderInfo,\n\tb: Physics2DColliderInfo,\n\tcontact: Contact,\n\tecs: PhysicsEcsLike,\n): void {\n\tconst invMassA = (a.rigidBody.type === 'dynamic' && a.rigidBody.mass > 0 && a.rigidBody.mass !== Infinity)\n\t\t? 1 / a.rigidBody.mass\n\t\t: 0;\n\tconst invMassB = (b.rigidBody.type === 'dynamic' && b.rigidBody.mass > 0 && b.rigidBody.mass !== Infinity)\n\t\t? 1 / b.rigidBody.mass\n\t\t: 0;\n\tconst totalInvMass = invMassA + invMassB;\n\n\t// Position correction\n\tif (totalInvMass > 0) {\n\t\tconst correctionScale = contact.depth / totalInvMass;\n\n\t\tif (invMassA > 0) {\n\t\t\tconst ltA = ecs.getComponent(a.entityId, 'localTransform');\n\t\t\tif (!ltA) return;\n\t\t\tltA.x -= correctionScale * invMassA * contact.normalX;\n\t\t\tltA.y -= correctionScale * invMassA * contact.normalY;\n\t\t\t// Update cached position for subsequent pairs (collider offset already baked in)\n\t\t\ta.x = ltA.x;\n\t\t\tecs.markChanged(a.entityId, 'localTransform');\n\t\t}\n\n\t\tif (invMassB > 0) {\n\t\t\tconst ltB = ecs.getComponent(b.entityId, 'localTransform');\n\t\t\tif (!ltB) return;\n\t\t\tltB.x += correctionScale * invMassB * contact.normalX;\n\t\t\tltB.y += correctionScale * invMassB * contact.normalY;\n\t\t\tecs.markChanged(b.entityId, 'localTransform');\n\t\t}\n\n\t\t// Velocity response (impulse-based)\n\t\tconst relVelX = b.velocity.x - a.velocity.x;\n\t\tconst relVelY = b.velocity.y - a.velocity.y;\n\t\tconst velAlongNormal = relVelX * contact.normalX + relVelY * contact.normalY;\n\n\t\tif (velAlongNormal < 0) {\n\t\t\tconst restitution = Math.min(a.rigidBody.restitution, b.rigidBody.restitution);\n\t\t\tconst normalImpulse = -(1 + restitution) * velAlongNormal / totalInvMass;\n\n\t\t\ta.velocity.x -= normalImpulse * invMassA * contact.normalX;\n\t\t\ta.velocity.y -= normalImpulse * invMassA * contact.normalY;\n\t\t\tb.velocity.x += normalImpulse * invMassB * contact.normalX;\n\t\t\tb.velocity.y += normalImpulse * invMassB * contact.normalY;\n\n\t\t\t// Friction (tangential impulse)\n\t\t\tconst tangentX = relVelX - velAlongNormal * contact.normalX;\n\t\t\tconst tangentY = relVelY - velAlongNormal * contact.normalY;\n\t\t\tconst tangentSpeed = Math.sqrt(tangentX * tangentX + tangentY * tangentY);\n\n\t\t\tif (tangentSpeed > 1e-6) {\n\t\t\t\tconst tangentNX = tangentX / tangentSpeed;\n\t\t\t\tconst tangentNY = tangentY / tangentSpeed;\n\t\t\t\tconst friction = Math.sqrt(a.rigidBody.friction * b.rigidBody.friction);\n\t\t\t\tconst maxFrictionImpulse = friction * Math.abs(normalImpulse);\n\t\t\t\tconst tangentImpulse = Math.min(tangentSpeed / totalInvMass, maxFrictionImpulse);\n\n\t\t\t\ta.velocity.x += tangentImpulse * invMassA * tangentNX;\n\t\t\t\ta.velocity.y += tangentImpulse * invMassA * tangentNY;\n\t\t\t\tb.velocity.x -= tangentImpulse * invMassB * tangentNX;\n\t\t\t\tb.velocity.y -= tangentImpulse * invMassB * tangentNY;\n\t\t\t}\n\t\t}\n\n\t\tecs.markChanged(a.entityId, 'velocity');\n\t\tecs.markChanged(b.entityId, 'velocity');\n\t}\n\n\tecs.eventBus.publish('physicsCollision', {\n\t\tentityA: a.entityId,\n\t\tentityB: b.entityId,\n\t\tnormalX: contact.normalX,\n\t\tnormalY: contact.normalY,\n\t\tdepth: contact.depth,\n\t});\n}\n\n// ==================== Module-level Physics Callback ====================\n\nfunction onPhysicsContact(\n\ta: Physics2DColliderInfo,\n\tb: Physics2DColliderInfo,\n\tcontact: Contact,\n\tecs: PhysicsEcsLike,\n): void {\n\tresolvePhysicsContact(a, b, contact, ecs);\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a 2D physics plugin for ECSpresso.\n *\n * Provides:\n * - Semi-implicit Euler integration (gravity, forces, drag → velocity → position)\n * - Impulse-based collision response with restitution and friction\n * - physicsCollision events with contact normal and depth\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createPhysics2DPlugin({ gravity: { x: 0, y: 980 } }))\n * .withFixedTimestep(1/60)\n * .build();\n *\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createRigidBody('dynamic', { mass: 1, restitution: 0.5 }),\n * velocity: { x: 0, y: 0 },\n * ...createAABBCollider(32, 32),\n * ...createCollisionLayer('player', ['ground']),\n * });\n * ```\n */\n\ntype Physics2DProvides<L extends string = never> = Physics2DOwnComponentTypes & CollisionComponentTypes<L>;\n\nexport function createPhysics2DPlugin<L extends string = never, G extends string = 'physics2D', CG extends string = never>(\n\toptions?: Physics2DPluginOptions<G, CG> & { layers?: LayerFactories<Record<L, readonly string[]>> },\n) {\n\tconst {\n\t\tgravity = { x: 0, y: 0 },\n\t\tsystemGroup = 'physics2D',\n\t\tcollisionSystemGroup,\n\t\tintegrationPriority = 1000,\n\t\tcollisionPriority = 900,\n\t\tphase = 'fixedUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('physics2D')\n\t\t.withComponentTypes<Physics2DProvides<L>>()\n\t\t.withEventTypes<Physics2DEventTypes>()\n\t\t.withResourceTypes<Physics2DResourceTypes>()\n\t\t.withLabels<'physics2D-integration' | 'physics2D-collision'>()\n\t\t.withGroups<G | CG>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// rigidBody requires velocity and force — auto-add with zero defaults\n\t\t\tworld.registerRequired('rigidBody', 'velocity', () => ({ x: 0, y: 0 }));\n\t\t\tworld.registerRequired('rigidBody', 'force', () => ({ x: 0, y: 0 }));\n\n\t\t\tworld.addResource('physicsConfig', { gravity: { x: gravity.x, y: gravity.y } });\n\n\t\t\t// ==================== Integration System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('physics2D-integration')\n\t\t\t\t.setPriority(integrationPriority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('bodies', {\n\t\t\t\t\twith: ['localTransform', 'velocity', 'rigidBody', 'force'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tconst config = ecs.getResource('physicsConfig');\n\t\t\t\t\tconst gx = config.gravity.x;\n\t\t\t\t\tconst gy = config.gravity.y;\n\n\t\t\t\t\tfor (const entity of queries.bodies) {\n\t\t\t\t\t\tconst { localTransform, velocity, rigidBody, force } = entity.components;\n\n\t\t\t\t\t\t// Static bodies: skip entirely\n\t\t\t\t\t\tif (rigidBody.type === 'static') continue;\n\n\t\t\t\t\t\t// Dynamic bodies: apply gravity, forces, drag\n\t\t\t\t\t\tif (rigidBody.type === 'dynamic') {\n\t\t\t\t\t\t\t// 1. Gravity\n\t\t\t\t\t\t\tvelocity.x += gx * rigidBody.gravityScale * dt;\n\t\t\t\t\t\t\tvelocity.y += gy * rigidBody.gravityScale * dt;\n\n\t\t\t\t\t\t\t// 2. Forces (F = ma → a = F/m)\n\t\t\t\t\t\t\tif (rigidBody.mass > 0 && rigidBody.mass !== Infinity) {\n\t\t\t\t\t\t\t\tvelocity.x += (force.x / rigidBody.mass) * dt;\n\t\t\t\t\t\t\t\tvelocity.y += (force.y / rigidBody.mass) * dt;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// 3. Drag\n\t\t\t\t\t\t\tif (rigidBody.drag > 0) {\n\t\t\t\t\t\t\t\tconst damping = Math.max(0, 1 - rigidBody.drag * dt);\n\t\t\t\t\t\t\t\tvelocity.x *= damping;\n\t\t\t\t\t\t\t\tvelocity.y *= damping;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Both dynamic and kinematic: integrate position\n\t\t\t\t\t\tlocalTransform.x += velocity.x * dt;\n\t\t\t\t\t\tlocalTransform.y += velocity.y * dt;\n\n\t\t\t\t\t\t// Clear accumulated forces\n\t\t\t\t\t\tforce.x = 0;\n\t\t\t\t\t\tforce.y = 0;\n\n\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Collision System ====================\n\n\t\t\tconst collisionSystem = world\n\t\t\t\t.addSystem('physics2D-collision')\n\t\t\t\t.setPriority(collisionPriority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup);\n\n\t\t\tif (collisionSystemGroup) {\n\t\t\t\tcollisionSystem.inGroup(collisionSystemGroup);\n\t\t\t}\n\n\t\t\t// Grow-only pool of Physics2DColliderInfo slots reused across frames.\n\t\t\t// Steady-state: zero allocations per frame once the pool is warm.\n\t\t\tconst colliderPool: Physics2DColliderInfo<L>[] = [];\n\t\t\t// Reusable entityId → collider lookup for the broadphase path.\n\t\t\tconst broadphaseMap = new Map<number, Physics2DColliderInfo<L>>();\n\n\t\t\tcollisionSystem\n\t\t\t\t.addQuery('collidables', {\n\t\t\t\t\twith: ['localTransform', 'rigidBody', 'velocity', 'collisionLayer'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tlet count = 0;\n\n\t\t\t\t\tfor (const entity of queries.collidables) {\n\t\t\t\t\t\tconst { localTransform, rigidBody, velocity, collisionLayer } = entity.components;\n\t\t\t\t\t\tconst aabb = ecs.getComponent(entity.id, 'aabbCollider');\n\t\t\t\t\t\tconst circle = ecs.getComponent(entity.id, 'circleCollider');\n\t\t\t\t\t\tif (!aabb && !circle) continue;\n\n\t\t\t\t\t\tlet slot = colliderPool[count];\n\t\t\t\t\t\tif (!slot) {\n\t\t\t\t\t\t\tslot = {\n\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\tx: localTransform.x,\n\t\t\t\t\t\t\t\ty: localTransform.y,\n\t\t\t\t\t\t\t\tlayer: collisionLayer.layer,\n\t\t\t\t\t\t\t\tcollidesWith: collisionLayer.collidesWith,\n\t\t\t\t\t\t\t\tshape: AABB_SHAPE,\n\t\t\t\t\t\t\t\thalfWidth: 0,\n\t\t\t\t\t\t\t\thalfHeight: 0,\n\t\t\t\t\t\t\t\tradius: 0,\n\t\t\t\t\t\t\t\trigidBody,\n\t\t\t\t\t\t\t\tvelocity,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tcolliderPool[count] = slot;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tslot.rigidBody = rigidBody;\n\t\t\t\t\t\t\tslot.velocity = velocity;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!fillBaseColliderInfo(\n\t\t\t\t\t\t\tslot,\n\t\t\t\t\t\t\tentity.id, localTransform.x, localTransform.y,\n\t\t\t\t\t\t\tcollisionLayer.layer, collisionLayer.collidesWith,\n\t\t\t\t\t\t\taabb, circle,\n\t\t\t\t\t\t)) continue;\n\n\t\t\t\t\t\tcount++;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst si = tryGetSpatialIndex(ecs.tryGetResource.bind(ecs));\n\t\t\t\t\tdetectCollisions(colliderPool, count, broadphaseMap, si, onPhysicsContact, ecs);\n\t\t\t\t});\n\t\t});\n}\n",
|
|
6
6
|
"/**\n * Shared Narrowphase Module\n *\n * Provides contact-computing narrowphase tests and a generic collision\n * iteration pipeline used by both the collision plugin (event-only) and\n * the physics2D plugin (impulse response).\n */\n\nimport type { SpatialIndex } from './spatial-hash';\n\n// ==================== Contact ====================\n\n/**\n * Contact result from a narrowphase test. Normal points from A toward B.\n *\n * Narrowphase functions use this as an out-parameter: the caller owns the\n * struct, the function writes fields in place and returns `true` on hit.\n * The `onContact` callback in `detectCollisions` receives a shared module-\n * level instance — **subscribers must consume it synchronously and must not\n * retain the reference across frames**.\n */\nexport interface Contact {\n\tnormalX: number;\n\tnormalY: number;\n\t/** Penetration depth (positive = overlapping) */\n\tdepth: number;\n}\n\n/**\n * Module-level reusable Contact passed down from `detectCollisions` into\n * narrowphase tests and forwarded to the `onContact` callback. Reused across\n * every pair in every frame — zero allocation in the narrowphase hot path.\n */\nconst _sharedContact: Contact = { normalX: 0, normalY: 0, depth: 0 };\n\n// ==================== BaseColliderInfo ====================\n\n/** Collider shape discriminator for the flattened BaseColliderInfo layout. */\nexport const AABB_SHAPE = 0;\nexport const CIRCLE_SHAPE = 1;\nexport type ColliderShape = typeof AABB_SHAPE | typeof CIRCLE_SHAPE;\n\n/**\n * Minimum collider data shared by collision and physics bundles.\n *\n * Flat layout (no nested `aabb` / `circle` sub-objects): the `shape`\n * discriminator tells you whether to read `halfWidth`/`halfHeight`\n * (AABB) or `radius` (Circle). Unused fields are set to 0.\n *\n * This shape is pool-friendly — all fields are assigned in place each\n * frame without allocating nested objects.\n */\nexport interface BaseColliderInfo<L extends string = string> {\n\tentityId: number;\n\tx: number;\n\ty: number;\n\tlayer: L;\n\tcollidesWith: readonly L[];\n\tshape: ColliderShape;\n\thalfWidth: number;\n\thalfHeight: number;\n\tradius: number;\n}\n\n// ==================== Collider Construction ====================\n\n/**\n * Populate a `BaseColliderInfo` slot in place from raw component data.\n * Returns `true` if the slot was filled, `false` if the entity has no\n * collider (caller should skip it).\n *\n * If an entity has both AABB and circle colliders, AABB wins and only\n * the AABB offset is applied. This matches the dispatch precedence in\n * `computeContact`; the previous implementation stacked both offsets,\n * which was a bug.\n */\nexport function fillBaseColliderInfo<L extends string>(\n\tinfo: BaseColliderInfo<L>,\n\tentityId: number,\n\tx: number,\n\ty: number,\n\tlayer: L,\n\tcollidesWith: readonly L[],\n\taabb: { width: number; height: number; offsetX?: number; offsetY?: number } | undefined,\n\tcircle: { radius: number; offsetX?: number; offsetY?: number } | undefined,\n): boolean {\n\tinfo.entityId = entityId;\n\tinfo.layer = layer;\n\tinfo.collidesWith = collidesWith;\n\n\tif (aabb) {\n\t\tinfo.x = x + (aabb.offsetX ?? 0);\n\t\tinfo.y = y + (aabb.offsetY ?? 0);\n\t\tinfo.shape = AABB_SHAPE;\n\t\tinfo.halfWidth = aabb.width / 2;\n\t\tinfo.halfHeight = aabb.height / 2;\n\t\tinfo.radius = 0;\n\t\treturn true;\n\t}\n\n\tif (circle) {\n\t\tinfo.x = x + (circle.offsetX ?? 0);\n\t\tinfo.y = y + (circle.offsetY ?? 0);\n\t\tinfo.shape = CIRCLE_SHAPE;\n\t\tinfo.halfWidth = 0;\n\t\tinfo.halfHeight = 0;\n\t\tinfo.radius = circle.radius;\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\n// ==================== Spatial Index Lookup ====================\n\n/**\n * Retrieve the optional spatialIndex resource, returning undefined when absent.\n * Centralizes the cross-plugin typed lookup so individual plugins don't each\n * need to import SpatialIndex or repeat the tryGetResource pattern.\n */\nexport function tryGetSpatialIndex(\n\ttryGetResource: <T>(key: string) => T | undefined,\n): SpatialIndex | undefined {\n\treturn tryGetResource<SpatialIndex>('spatialIndex');\n}\n\n// ==================== Narrowphase Tests ====================\n\n/**\n * Write an AABB-AABB contact into `out`. Returns `true` if the shapes\n * overlap (out was filled), `false` otherwise.\n */\nexport function computeAABBvsAABB(\n\tax: number, ay: number, ahw: number, ahh: number,\n\tbx: number, by: number, bhw: number, bhh: number,\n\tout: Contact,\n): boolean {\n\tconst dx = bx - ax;\n\tconst dy = by - ay;\n\tconst overlapX = (ahw + bhw) - Math.abs(dx);\n\tconst overlapY = (ahh + bhh) - Math.abs(dy);\n\n\tif (overlapX <= 0 || overlapY <= 0) return false;\n\n\tif (overlapX < overlapY) {\n\t\tout.normalX = dx >= 0 ? 1 : -1;\n\t\tout.normalY = 0;\n\t\tout.depth = overlapX;\n\t\treturn true;\n\t}\n\tout.normalX = 0;\n\tout.normalY = dy >= 0 ? 1 : -1;\n\tout.depth = overlapY;\n\treturn true;\n}\n\nexport function computeCircleVsCircle(\n\tax: number, ay: number, ar: number,\n\tbx: number, by: number, br: number,\n\tout: Contact,\n): boolean {\n\tconst dx = bx - ax;\n\tconst dy = by - ay;\n\tconst distSq = dx * dx + dy * dy;\n\tconst radiusSum = ar + br;\n\n\tif (distSq >= radiusSum * radiusSum) return false;\n\n\tconst dist = Math.sqrt(distSq);\n\tif (dist === 0) {\n\t\tout.normalX = 1;\n\t\tout.normalY = 0;\n\t\tout.depth = radiusSum;\n\t\treturn true;\n\t}\n\tout.normalX = dx / dist;\n\tout.normalY = dy / dist;\n\tout.depth = radiusSum - dist;\n\treturn true;\n}\n\nexport function computeAABBvsCircle(\n\taabbX: number, aabbY: number, ahw: number, ahh: number,\n\tcircleX: number, circleY: number, radius: number,\n\tout: Contact,\n): boolean {\n\tconst closestX = Math.max(aabbX - ahw, Math.min(circleX, aabbX + ahw));\n\tconst closestY = Math.max(aabbY - ahh, Math.min(circleY, aabbY + ahh));\n\n\tconst dx = circleX - closestX;\n\tconst dy = circleY - closestY;\n\tconst distSq = dx * dx + dy * dy;\n\n\tif (distSq >= radius * radius) return false;\n\n\t// Circle center inside AABB\n\tif (distSq === 0) {\n\t\tconst pushLeft = (circleX - (aabbX - ahw));\n\t\tconst pushRight = ((aabbX + ahw) - circleX);\n\t\tconst pushUp = (circleY - (aabbY - ahh));\n\t\tconst pushDown = ((aabbY + ahh) - circleY);\n\t\tconst minPush = Math.min(pushLeft, pushRight, pushUp, pushDown);\n\n\t\tif (minPush === pushRight) {\n\t\t\tout.normalX = 1; out.normalY = 0; out.depth = pushRight + radius;\n\t\t\treturn true;\n\t\t}\n\t\tif (minPush === pushLeft) {\n\t\t\tout.normalX = -1; out.normalY = 0; out.depth = pushLeft + radius;\n\t\t\treturn true;\n\t\t}\n\t\tif (minPush === pushDown) {\n\t\t\tout.normalX = 0; out.normalY = 1; out.depth = pushDown + radius;\n\t\t\treturn true;\n\t\t}\n\t\tout.normalX = 0; out.normalY = -1; out.depth = pushUp + radius;\n\t\treturn true;\n\t}\n\n\tconst dist = Math.sqrt(distSq);\n\tout.normalX = dx / dist;\n\tout.normalY = dy / dist;\n\tout.depth = radius - dist;\n\treturn true;\n}\n\n// ==================== Contact Dispatcher ====================\n\n/**\n * Dispatch to the correct narrowphase function for the given pair and\n * write the contact into `out`. Returns `true` if the pair overlaps.\n */\nexport function computeContact(a: BaseColliderInfo, b: BaseColliderInfo, out: Contact): boolean {\n\tif (a.shape === AABB_SHAPE && b.shape === AABB_SHAPE) {\n\t\treturn computeAABBvsAABB(\n\t\t\ta.x, a.y, a.halfWidth, a.halfHeight,\n\t\t\tb.x, b.y, b.halfWidth, b.halfHeight,\n\t\t\tout,\n\t\t);\n\t}\n\n\tif (a.shape === CIRCLE_SHAPE && b.shape === CIRCLE_SHAPE) {\n\t\treturn computeCircleVsCircle(\n\t\t\ta.x, a.y, a.radius,\n\t\t\tb.x, b.y, b.radius,\n\t\t\tout,\n\t\t);\n\t}\n\n\tif (a.shape === AABB_SHAPE && b.shape === CIRCLE_SHAPE) {\n\t\treturn computeAABBvsCircle(\n\t\t\ta.x, a.y, a.halfWidth, a.halfHeight,\n\t\t\tb.x, b.y, b.radius,\n\t\t\tout,\n\t\t);\n\t}\n\n\t// a is Circle, b is AABB — compute as AABB-vs-Circle, then flip normal in place\n\tif (!computeAABBvsCircle(\n\t\tb.x, b.y, b.halfWidth, b.halfHeight,\n\t\ta.x, a.y, a.radius,\n\t\tout,\n\t)) return false;\n\tout.normalX = -out.normalX;\n\tout.normalY = -out.normalY;\n\treturn true;\n}\n\n// ==================== Collision Iteration ====================\n\n/** Module-level reusable set for broadphase candidates. */\nconst _broadphaseCandidates = new Set<number>();\n\nlet _bruteForceWarned = false;\nconst BRUTE_FORCE_WARN_THRESHOLD = 50;\n\n/**\n * Generic collision detection pipeline: brute-force or broadphase,\n * with layer filtering and contact computation.\n *\n * `count` is the number of live entries at the front of `colliders`.\n * The array itself may be a grow-only pool — only indices `[0, count)`\n * are iterated, so trailing pool slots are ignored.\n *\n * `workingMap` is a caller-owned `Map<number, I>` used by the broadphase\n * path as an entityId → collider lookup. It is cleared and repopulated on\n * each call; callers should allocate it once and pass the same instance\n * every frame. Unused by the brute-force path but still required so that\n * typed reuse is the default, not an opt-in.\n *\n * Uses a context parameter forwarded to the callback to avoid\n * per-frame closure allocation.\n */\nexport function detectCollisions<I extends BaseColliderInfo, C>(\n\tcolliders: I[],\n\tcount: number,\n\tworkingMap: Map<number, I>,\n\tspatialIndex: SpatialIndex | undefined,\n\tonContact: (a: I, b: I, contact: Contact, context: C) => void,\n\tcontext: C,\n): void {\n\tif (spatialIndex) {\n\t\tbroadphaseDetect(colliders, count, workingMap, spatialIndex, onContact, context);\n\t} else {\n\t\tbruteForceDetect(colliders, count, onContact, context);\n\t}\n}\n\nfunction bruteForceDetect<I extends BaseColliderInfo, C>(\n\tcolliders: I[],\n\tcount: number,\n\tonContact: (a: I, b: I, contact: Contact, context: C) => void,\n\tcontext: C,\n): void {\n\tif (!_bruteForceWarned && count >= BRUTE_FORCE_WARN_THRESHOLD) {\n\t\t_bruteForceWarned = true;\n\t\tconsole.warn(\n\t\t\t`[ecspresso] Collision detection is using O(n²) brute force with ${count} colliders. ` +\n\t\t\t`For better performance, install createSpatialIndexPlugin() alongside your collision or physics2D plugin.`,\n\t\t);\n\t}\n\n\tfor (let i = 0; i < count; i++) {\n\t\tconst a = colliders[i];\n\t\tif (!a) continue;\n\n\t\tfor (let j = i + 1; j < count; j++) {\n\t\t\tconst b = colliders[j];\n\t\t\tif (!b) continue;\n\n\t\t\tif (!a.collidesWith.includes(b.layer) && !b.collidesWith.includes(a.layer)) continue;\n\n\t\t\tif (!computeContact(a, b, _sharedContact)) continue;\n\n\t\t\tonContact(a, b, _sharedContact, context);\n\t\t}\n\t}\n}\n\nfunction broadphaseDetect<I extends BaseColliderInfo, C>(\n\tcolliders: I[],\n\tcount: number,\n\tcolliderMap: Map<number, I>,\n\tspatialIndex: SpatialIndex,\n\tonContact: (a: I, b: I, contact: Contact, context: C) => void,\n\tcontext: C,\n): void {\n\tcolliderMap.clear();\n\tfor (let i = 0; i < count; i++) {\n\t\tconst c = colliders[i];\n\t\tif (!c) continue;\n\t\tcolliderMap.set(c.entityId, c);\n\t}\n\n\tfor (let i = 0; i < count; i++) {\n\t\tconst a = colliders[i];\n\t\tif (!a) continue;\n\n\t\tconst aHalfW = a.shape === AABB_SHAPE ? a.halfWidth : a.radius;\n\t\tconst aHalfH = a.shape === AABB_SHAPE ? a.halfHeight : a.radius;\n\n\t\t_broadphaseCandidates.clear();\n\t\tspatialIndex.queryRectInto(\n\t\t\ta.x - aHalfW, a.y - aHalfH,\n\t\t\ta.x + aHalfW, a.y + aHalfH,\n\t\t\t_broadphaseCandidates,\n\t\t);\n\n\t\tfor (const bId of _broadphaseCandidates) {\n\t\t\tif (bId <= a.entityId) continue;\n\n\t\t\tconst b = colliderMap.get(bId);\n\t\t\tif (!b) continue;\n\n\t\t\tif (!a.collidesWith.includes(b.layer) && !b.collidesWith.includes(a.layer)) continue;\n\n\t\t\tif (!computeContact(a, b, _sharedContact)) continue;\n\n\t\t\tonContact(a, b, _sharedContact, context);\n\t\t}\n\t}\n}\n"
|
|
7
7
|
],
|
|
8
8
|
"mappings": "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",
|
|
9
|
-
"debugId": "
|
|
9
|
+
"debugId": "B8850A0A0D8A257364756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Steering Plugin for ECSpresso
|
|
3
|
+
*
|
|
4
|
+
* Provides simple move-to-target behavior with arrival detection.
|
|
5
|
+
* Entities with a `moveTarget` component move toward the target position
|
|
6
|
+
* at their configured `moveSpeed`. The `moveTarget` component is removed
|
|
7
|
+
* on arrival and an `arriveAtTarget` event is published.
|
|
8
|
+
*/
|
|
9
|
+
import { type BasePluginOptions } from 'ecspresso';
|
|
10
|
+
import type { WorldConfigFrom } from 'ecspresso';
|
|
11
|
+
import type { TransformWorldConfig } from '../spatial/transform';
|
|
12
|
+
/**
|
|
13
|
+
* Target position for an entity to move toward.
|
|
14
|
+
* Removed automatically when the entity arrives.
|
|
15
|
+
*/
|
|
16
|
+
export interface MoveTarget {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Component types provided by the steering plugin.
|
|
22
|
+
*/
|
|
23
|
+
export interface SteeringComponentTypes {
|
|
24
|
+
moveTarget: MoveTarget;
|
|
25
|
+
moveSpeed: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Event fired when an entity arrives at its move target.
|
|
29
|
+
*/
|
|
30
|
+
export interface ArriveAtTargetEvent {
|
|
31
|
+
entityId: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Event types provided by the steering plugin.
|
|
35
|
+
*/
|
|
36
|
+
export interface SteeringEventTypes {
|
|
37
|
+
arriveAtTarget: ArriveAtTargetEvent;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* WorldConfig representing the steering plugin's provided types.
|
|
41
|
+
* Used as the `Requires` type parameter by plugins that depend on steering.
|
|
42
|
+
*/
|
|
43
|
+
export type SteeringWorldConfig = WorldConfigFrom<SteeringComponentTypes, SteeringEventTypes>;
|
|
44
|
+
/**
|
|
45
|
+
* Configuration options for the steering plugin.
|
|
46
|
+
*/
|
|
47
|
+
export interface SteeringPluginOptions<G extends string = 'steering'> extends BasePluginOptions<G> {
|
|
48
|
+
/** Distance threshold to consider arrival (default: 2) */
|
|
49
|
+
arrivalThreshold?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create a moveTarget component.
|
|
53
|
+
*
|
|
54
|
+
* @param x Target x position
|
|
55
|
+
* @param y Target y position
|
|
56
|
+
* @returns Component object suitable for spreading into spawn()
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* ecs.addComponent(entityId, 'moveTarget', createMoveTarget(200, 300).moveTarget);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function createMoveTarget(x: number, y: number): Pick<SteeringComponentTypes, 'moveTarget'>;
|
|
64
|
+
/**
|
|
65
|
+
* Create a moveSpeed component.
|
|
66
|
+
*
|
|
67
|
+
* @param speed Movement speed in pixels per second
|
|
68
|
+
* @returns Component object suitable for spreading into spawn()
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* ecs.spawn({
|
|
73
|
+
* ...createTransform(100, 200),
|
|
74
|
+
* ...createMoveSpeed(150),
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare function createMoveSpeed(speed: number): Pick<SteeringComponentTypes, 'moveSpeed'>;
|
|
79
|
+
/**
|
|
80
|
+
* Create a steering plugin for ECSpresso.
|
|
81
|
+
*
|
|
82
|
+
* Provides a `move-to-target` system that moves entities with `moveTarget`
|
|
83
|
+
* and `moveSpeed` components toward their target position. On arrival,
|
|
84
|
+
* the `moveTarget` component is removed and an `arriveAtTarget` event is published.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* const ecs = ECSpresso.create()
|
|
89
|
+
* .withPlugin(createTransformPlugin())
|
|
90
|
+
* .withPlugin(createSteeringPlugin())
|
|
91
|
+
* .build();
|
|
92
|
+
*
|
|
93
|
+
* await ecs.initialize();
|
|
94
|
+
*
|
|
95
|
+
* ecs.spawn({
|
|
96
|
+
* ...createTransform(0, 0),
|
|
97
|
+
* ...createMoveSpeed(100),
|
|
98
|
+
* ...createMoveTarget(200, 200),
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare function createSteeringPlugin<G extends string = 'steering'>(options?: SteeringPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, SteeringComponentTypes>, SteeringEventTypes>, TransformWorldConfig, "move-to-target", G, never, never>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var $=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(B,I)=>(typeof require<"u"?require:B)[I]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as _}from"ecspresso";function G(j,B){return{moveTarget:{x:j,y:B}}}function O(j){return{moveSpeed:j}}function b(j){let{systemGroup:B="steering",priority:I=100,phase:U="update",arrivalThreshold:N=2}=j??{},V=N*N;return _("steering").withComponentTypes().withEventTypes().withLabels().withGroups().requires().install((W)=>{W.addSystem("move-to-target").setPriority(I).inPhase(U).inGroup(B).addQuery("movers",{with:["localTransform","moveTarget","moveSpeed"]}).setProcess(({queries:X,ecs:F,dt:Y})=>{for(let D of X.movers){let{localTransform:k,moveTarget:H,moveSpeed:Z}=D.components,J=H.x-k.x,K=H.y-k.y,Q=J*J+K*K;if(Q<=V){k.x=H.x,k.y=H.y,F.markChanged(D.id,"localTransform"),F.commands.removeComponent(D.id,"moveTarget"),F.eventBus.publish("arriveAtTarget",{entityId:D.id});continue}let L=Math.sqrt(Q),R=Math.min(Z*Y,L);k.x+=J/L*R,k.y+=K/L*R,F.markChanged(D.id,"localTransform")}})})}export{b as createSteeringPlugin,G as createMoveTarget,O as createMoveSpeed};
|
|
2
|
+
|
|
3
|
+
//# debugId=A245A409F4202B3864756E2164756E21
|
|
4
|
+
//# sourceMappingURL=steering.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/plugins/physics/steering.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Steering Plugin for ECSpresso\n *\n * Provides simple move-to-target behavior with arrival detection.\n * Entities with a `moveTarget` component move toward the target position\n * at their configured `moveSpeed`. The `moveTarget` component is removed\n * on arrival and an `arriveAtTarget` event is published.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { TransformWorldConfig } from '../spatial/transform';\n\n// ==================== Component Types ====================\n\n/**\n * Target position for an entity to move toward.\n * Removed automatically when the entity arrives.\n */\nexport interface MoveTarget {\n\tx: number;\n\ty: number;\n}\n\n/**\n * Component types provided by the steering plugin.\n */\nexport interface SteeringComponentTypes {\n\tmoveTarget: MoveTarget;\n\tmoveSpeed: number;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when an entity arrives at its move target.\n */\nexport interface ArriveAtTargetEvent {\n\tentityId: number;\n}\n\n/**\n * Event types provided by the steering plugin.\n */\nexport interface SteeringEventTypes {\n\tarriveAtTarget: ArriveAtTargetEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the steering plugin's provided types.\n * Used as the `Requires` type parameter by plugins that depend on steering.\n */\nexport type SteeringWorldConfig = WorldConfigFrom<SteeringComponentTypes, SteeringEventTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the steering plugin.\n */\nexport interface SteeringPluginOptions<G extends string = 'steering'> extends BasePluginOptions<G> {\n\t/** Distance threshold to consider arrival (default: 2) */\n\tarrivalThreshold?: number;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a moveTarget component.\n *\n * @param x Target x position\n * @param y Target y position\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.addComponent(entityId, 'moveTarget', createMoveTarget(200, 300).moveTarget);\n * ```\n */\nexport function createMoveTarget(x: number, y: number): Pick<SteeringComponentTypes, 'moveTarget'> {\n\treturn { moveTarget: { x, y } };\n}\n\n/**\n * Create a moveSpeed component.\n *\n * @param speed Movement speed in pixels per second\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createMoveSpeed(150),\n * });\n * ```\n */\nexport function createMoveSpeed(speed: number): Pick<SteeringComponentTypes, 'moveSpeed'> {\n\treturn { moveSpeed: speed };\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a steering plugin for ECSpresso.\n *\n * Provides a `move-to-target` system that moves entities with `moveTarget`\n * and `moveSpeed` components toward their target position. On arrival,\n * the `moveTarget` component is removed and an `arriveAtTarget` event is published.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createSteeringPlugin())\n * .build();\n *\n * await ecs.initialize();\n *\n * ecs.spawn({\n * ...createTransform(0, 0),\n * ...createMoveSpeed(100),\n * ...createMoveTarget(200, 200),\n * });\n * ```\n */\nexport function createSteeringPlugin<G extends string = 'steering'>(\n\toptions?: SteeringPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'steering',\n\t\tpriority = 100,\n\t\tphase = 'update',\n\t\tarrivalThreshold = 2,\n\t} = options ?? {};\n\n\tconst arrivalThresholdSq = arrivalThreshold * arrivalThreshold;\n\n\treturn definePlugin('steering')\n\t\t.withComponentTypes<SteeringComponentTypes>()\n\t\t.withEventTypes<SteeringEventTypes>()\n\t\t.withLabels<'move-to-target'>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('move-to-target')\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('movers', {\n\t\t\t\t\twith: ['localTransform', 'moveTarget', 'moveSpeed'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs, dt }) => {\n\t\t\t\t\tfor (const entity of queries.movers) {\n\t\t\t\t\t\tconst { localTransform, moveTarget, moveSpeed } = entity.components;\n\n\t\t\t\t\t\tconst dx = moveTarget.x - localTransform.x;\n\t\t\t\t\t\tconst dy = moveTarget.y - localTransform.y;\n\t\t\t\t\t\tconst distSq = dx * dx + dy * dy;\n\n\t\t\t\t\t\tif (distSq <= arrivalThresholdSq) {\n\t\t\t\t\t\t\tlocalTransform.x = moveTarget.x;\n\t\t\t\t\t\t\tlocalTransform.y = moveTarget.y;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t\t\tecs.commands.removeComponent(entity.id, 'moveTarget');\n\t\t\t\t\t\t\tecs.eventBus.publish('arriveAtTarget', { entityId: entity.id });\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst dist = Math.sqrt(distSq);\n\t\t\t\t\t\tconst step = Math.min(moveSpeed * dt, dist);\n\t\t\t\t\t\tlocalTransform.x += (dx / dist) * step;\n\t\t\t\t\t\tlocalTransform.y += (dy / dist) * step;\n\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "2PASA,uBAAS,kBAuEF,SAAS,CAAgB,CAAC,EAAW,EAAuD,CAClG,MAAO,CAAE,WAAY,CAAE,IAAG,GAAE,CAAE,EAiBxB,SAAS,CAAe,CAAC,EAA0D,CACzF,MAAO,CAAE,UAAW,CAAM,EA4BpB,SAAS,CAAmD,CAClE,EACC,CACD,IACC,cAAc,WACd,WAAW,IACX,QAAQ,SACR,mBAAmB,GAChB,GAAW,CAAC,EAEV,EAAqB,EAAmB,EAE9C,OAAO,EAAa,UAAU,EAC5B,mBAA2C,EAC3C,eAAmC,EACnC,WAA6B,EAC7B,WAAc,EACd,SAA+B,EAC/B,QAAQ,CAAC,IAAU,CACnB,EACE,UAAU,gBAAgB,EAC1B,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CACnB,KAAM,CAAC,iBAAkB,aAAc,WAAW,CACnD,CAAC,EACA,WAAW,EAAG,UAAS,MAAK,QAAS,CACrC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAQ,iBAAgB,aAAY,aAAc,EAAO,WAEnD,EAAK,EAAW,EAAI,EAAe,EACnC,EAAK,EAAW,EAAI,EAAe,EACnC,EAAS,EAAK,EAAK,EAAK,EAE9B,GAAI,GAAU,EAAoB,CACjC,EAAe,EAAI,EAAW,EAC9B,EAAe,EAAI,EAAW,EAC9B,EAAI,YAAY,EAAO,GAAI,gBAAgB,EAC3C,EAAI,SAAS,gBAAgB,EAAO,GAAI,YAAY,EACpD,EAAI,SAAS,QAAQ,iBAAkB,CAAE,SAAU,EAAO,EAAG,CAAC,EAC9D,SAGD,IAAM,EAAO,KAAK,KAAK,CAAM,EACvB,EAAO,KAAK,IAAI,EAAY,EAAI,CAAI,EAC1C,EAAe,GAAM,EAAK,EAAQ,EAClC,EAAe,GAAM,EAAK,EAAQ,EAClC,EAAI,YAAY,EAAO,GAAI,gBAAgB,GAE5C,EACF",
|
|
8
|
+
"debugId": "A245A409F4202B3864756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type BasePluginOptions } from 'ecspresso';
|
|
13
13
|
import type { BaseWorld } from 'ecspresso';
|
|
14
|
-
import type { WorldConfigFrom } from '
|
|
15
|
-
import type { TransformComponentTypes } from 'ecspresso/plugins/transform';
|
|
14
|
+
import type { WorldConfigFrom } from '../../type-utils';
|
|
15
|
+
import type { TransformComponentTypes } from 'ecspresso/plugins/spatial/transform';
|
|
16
16
|
/** BaseWorld narrowed to particle components for typed access in helpers. */
|
|
17
17
|
type ParticleWorld = BaseWorld<ParticleComponentTypes>;
|
|
18
18
|
/** Fixed value or random range [min, max] */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
var b=((k)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(k,{get:(A,F)=>(typeof require<"u"?require:A)[F]}):k)(function(k){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+k+'" is not supported')});import{definePlugin as E}from"ecspresso";function M(k){if(typeof k==="number")return k;let[A,F]=k;return A+Math.random()*(F-A)}function D(k,A,F){if(k===A)return k;let L=k>>16&255,j=k>>8&255,q=k&255,H=A>>16&255,$=A>>8&255,Z=A&255,Q=L+(H-L)*F|0,V=j+($-j)*F|0,J=q+(Z-q)*F|0;return Q<<16|V<<8|J}var O=Math.PI*2;function G(k){let A=k.startSize??1,F=k.startTint??16777215;return Object.freeze({maxParticles:k.maxParticles,texture:k.texture,spawnRate:k.spawnRate??10,burstCount:k.burstCount??0,duration:k.duration??-1,lifetime:k.lifetime??1,speed:k.speed??100,angle:k.angle??[0,O],emissionShape:k.emissionShape??"point",emissionRadius:k.emissionRadius??0,gravity:Object.freeze(k.gravity??{x:0,y:0}),startSize:A,endSize:k.endSize??A,startAlpha:k.startAlpha??1,endAlpha:k.endAlpha??0,startTint:F,endTint:k.endTint??F,startRotation:k.startRotation??0,rotationSpeed:k.rotationSpeed??0,blendMode:k.blendMode??"normal",worldSpace:k.worldSpace??!0})}function C(k,A){return{particleEmitter:{config:k,activeCount:0,spawnAccumulator:0,elapsed:0,playing:A?.playing??!0,pendingBurst:0,finished:!1,onComplete:A?.onComplete}}}function R(k,A,F){let L=k.getComponent(A,"particleEmitter");if(!L)return!1;return L.pendingBurst+=F??L.config.burstCount,k.markChanged(A,"particleEmitter"),!0}function w(k,A){let F=k.getComponent(A,"particleEmitter");if(!F)return!1;return F.playing=!1,!0}function v(k,A){let F=k.getComponent(A,"particleEmitter");if(!F)return!1;return F.playing=!0,!0}function W(k,A,F,L,j){k.active=!0;let q=M(A.lifetime);if(k.life=q,k.maxLife=q,A.emissionShape==="circle"&&A.emissionRadius>0){let Z=Math.random()*O,Q=Math.random()*A.emissionRadius;k.x=F+Math.cos(Z)*Q,k.y=L+Math.sin(Z)*Q}else k.x=F,k.y=L;let H=M(A.speed),$=M(A.angle)+j;k.vx=Math.cos($)*H,k.vy=Math.sin($)*H,k.startSize=M(A.startSize),k.endSize=M(A.endSize),k.size=k.startSize,k.startAlpha=M(A.startAlpha),k.endAlpha=M(A.endAlpha),k.alpha=k.startAlpha,k.tint=A.startTint,k.rotation=M(A.startRotation),k.rotationSpeed=M(A.rotationSpeed)}function X(k,A,F,L,j,q){let H=k.config;k.elapsed+=F;let $=H.duration>=0&&k.elapsed>=H.duration;if(k.playing&&!$&&H.spawnRate>0){k.spawnAccumulator+=H.spawnRate*F;let B=Math.floor(k.spawnAccumulator);k.spawnAccumulator-=B;for(let U=0;U<B;U++){if(k.activeCount>=H.maxParticles)break;let z=A.particles[k.activeCount];if(!z)break;W(z,H,L,j,q),k.activeCount++}}if(k.pendingBurst>0){let B=Math.min(k.pendingBurst,H.maxParticles-k.activeCount);for(let U=0;U<B;U++){let z=A.particles[k.activeCount];if(!z)break;W(z,H,L,j,q),k.activeCount++}k.pendingBurst-=B}let Q=H.gravity.x,V=H.gravity.y,J=Q!==0||V!==0,N=H.startTint!==H.endTint,K=0;while(K<k.activeCount){let B=A.particles[K];if(!B)break;if(B.life-=F,B.life<=0){if(k.activeCount--,K<k.activeCount){let z=A.particles[k.activeCount];if(z){A.particles[K]=z,A.particles[k.activeCount]=B;let h=A.pixiParticles[K];A.pixiParticles[K]=A.pixiParticles[k.activeCount],A.pixiParticles[k.activeCount]=h}}B.active=!1;continue}if(J)B.vx+=Q*F,B.vy+=V*F;B.x+=B.vx*F,B.y+=B.vy*F;let U=1-B.life/B.maxLife;if(B.size=B.startSize+(B.endSize-B.startSize)*U,B.alpha=B.startAlpha+(B.endAlpha-B.startAlpha)*U,N)B.tint=D(H.startTint,H.endTint,U);B.rotation+=B.rotationSpeed*F,K++}}function _(k){let A=Array(k);for(let F=0;F<k;F++)A[F]={active:!1,x:0,y:0,vx:0,vy:0,life:0,maxLife:0,size:0,startSize:0,endSize:0,alpha:0,startAlpha:0,endAlpha:0,tint:16777215,rotation:0,rotationSpeed:0};return A}var I={explosion(k,A){return G({maxParticles:50,texture:k,spawnRate:0,burstCount:30,duration:1,lifetime:[0.3,0.8],speed:[100,300],angle:[0,O],startSize:[0.5,1.5],endSize:[0.1,0.3],startAlpha:1,endAlpha:0,...A})},smoke(k,A){return G({maxParticles:60,texture:k,spawnRate:15,duration:-1,lifetime:[1,3],speed:[20,60],angle:[-Math.PI/2-0.3,-Math.PI/2+0.3],startSize:[0.3,0.6],endSize:[1,2],startAlpha:0.4,endAlpha:0,...A})},fire(k,A){return G({maxParticles:80,texture:k,spawnRate:30,duration:-1,lifetime:[0.3,1],speed:[40,120],angle:[-Math.PI/2-0.5,-Math.PI/2+0.5],startSize:[0.5,1],endSize:[0.1,0.3],startAlpha:1,endAlpha:0,startTint:16746496,endTint:16720384,blendMode:"add",...A})},sparkle(k,A){return G({maxParticles:30,texture:k,spawnRate:10,duration:-1,lifetime:[0.5,1.5],speed:[10,40],angle:[0,O],startSize:[0.2,0.8],endSize:[0.1,0.4],startAlpha:[0.5,1],endAlpha:0,...A})},trail(k,A){return G({maxParticles:40,texture:k,spawnRate:20,duration:-1,lifetime:[0.3,0.8],speed:0,startSize:[0.5,1],endSize:[0.05,0.2],startAlpha:0.8,endAlpha:0,...A})}};function P(k){let{systemGroup:A="particles",priority:F=0,phase:L="update"}=k??{},j=new Map;return E("particles").withComponentTypes().withLabels().withGroups().withReactiveQueryNames().requires().install((q)=>{q.registerRequired("particleEmitter","localTransform",()=>({x:0,y:0,rotation:0,scaleX:1,scaleY:1})),q.registerDispose("particleEmitter",({entityId:H})=>{let $=j.get(H);if($){let Z=$.pixiContainer;if(Z)Z.removeFromParent?.(),Z.destroy?.();j.delete(H)}}),q.addSystem("particle-update").setPriority(F).inPhase(L).inGroup(A).addQuery("emitters",{with:["particleEmitter"]}).setProcess(({queries:H,dt:$,ecs:Z})=>{for(let Q of H.emitters){let V=Q.components.particleEmitter,J=j.get(Q.id);if(!J)J={particles:_(V.config.maxParticles),pixiContainer:null,pixiParticles:[]},j.set(Q.id,J);let N=Z.getComponent(Q.id,"worldTransform"),K=N?.x??0,B=N?.y??0,U=N?.rotation??0;X(V,J,$,K,B,U);let z=V.config;if(z.duration>=0&&V.elapsed>=z.duration&&V.activeCount===0&&!V.finished){if(V.finished=!0,V.onComplete)V.onComplete({entityId:Q.id});Z.commands.removeComponent(Q.id,"particleEmitter")}}}),q.addSystem("particle-render-sync").setPriority(400).inPhase("render").inGroup(A).setOnInitialize(async(H)=>{let $=await import("pixi.js"),Z=$.ParticleContainer,Q=$.Particle,V=H.tryGetResource("rootContainer");H.addReactiveQuery("particle-emitters",{with:["particleEmitter"],onEnter:(J)=>{let K=J.components.particleEmitter.config,B=new Z({dynamicProperties:{position:!0,rotation:!0,color:!0,vertex:!0}});B.blendMode=K.blendMode;let U=[];for(let h=0;h<K.maxParticles;h++){let S=new Q({texture:K.texture});S.alpha=0,U.push(S),B.addParticle(S)}let z=_(K.maxParticles);if(V)if(H.getComponent(J.id,"renderLayer"))V.addChild(B);else V.addChild(B);j.set(J.id,{particles:z,pixiContainer:B,pixiParticles:U})},onExit:(J)=>{let N=j.get(J);if(N){let K=N.pixiContainer;if(K)K.removeFromParent?.(),K.destroy?.();j.delete(J)}}})}).setProcess(({ecs:H})=>{for(let[$,Z]of j){let Q=H.getComponent($,"particleEmitter");if(!Q)continue;let V=Q.config;if(!V.worldSpace){let J=H.getComponent($,"worldTransform");if(J){let N=Z.pixiContainer;N.position.set(J.x,J.y),N.rotation=J.rotation,N.scale.set(J.scaleX,J.scaleY)}}for(let J=0;J<Q.activeCount;J++){let N=Z.particles[J],K=Z.pixiParticles[J];if(!N||!K)continue;K.x=N.x,K.y=N.y,K.scaleX=N.size,K.scaleY=N.size,K.rotation=N.rotation,K.tint=N.tint,K.alpha=N.alpha}for(let J=Q.activeCount;J<V.maxParticles;J++){let N=Z.pixiParticles[J];if(N)N.alpha=0}}})})}function x(k,A){return k.get(A)}export{w as stopEmitter,M as sampleRange,v as resumeEmitter,I as particlePresets,D as lerpTint,x as getEmitterData,G as defineParticleEffect,P as createParticlePlugin,C as createParticleEmitter,R as burstParticles};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=60D36D4494C6248064756E2164756E21
|
|
4
4
|
//# sourceMappingURL=particles.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/plugins/rendering/particles.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Particle System Plugin for ECSpresso\n *\n * High-performance particle system where particles live outside the ECS in\n * pre-allocated pools. Renders via PixiJS v8's ParticleContainer + Particle API.\n * Renderer2D is a required dependency.\n *\n * Follows the established plugin pattern: immutable shared config\n * (ParticleEffectConfig) + mutable per-entity state (ParticleEmitter) component,\n * side-storage Map for PixiJS objects, kit pattern for typed helpers.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\nimport type { TransformComponentTypes, LocalTransform } from 'ecspresso/plugins/spatial/transform';\n\n/** BaseWorld narrowed to particle components for typed access in helpers. */\ntype ParticleWorld = BaseWorld<ParticleComponentTypes>;\n\n// ==================== Value Types ====================\n\n/** Fixed value or random range [min, max] */\nexport type ParticleValue = number | readonly [number, number];\n\n/** Emission geometry */\nexport type EmissionShape = 'point' | 'circle';\n\n/** Blend modes for particle rendering */\nexport type ParticleBlendMode = 'normal' | 'add' | 'multiply' | 'screen';\n\n// ==================== Config Types ====================\n\n/**\n * User-facing config input for defining a particle effect.\n * All properties optional except maxParticles and texture.\n */\nexport interface ParticleEffectInput {\n\t/** Pool size — maximum simultaneous particles */\n\tmaxParticles: number;\n\t/** PixiJS Texture for particles */\n\ttexture: unknown;\n\t/** Particles per second (0 = burst-only, default: 10) */\n\tspawnRate?: number;\n\t/** Particles per burst (default: 0) */\n\tburstCount?: number;\n\t/** Emitter lifetime in seconds (-1 = infinite, default: -1) */\n\tduration?: number;\n\t/** Per-particle lifetime in seconds (default: 1) */\n\tlifetime?: ParticleValue;\n\t/** Initial speed in pixels/second (default: 100) */\n\tspeed?: ParticleValue;\n\t/** Emission direction in radians (default: [0, 2*PI]) */\n\tangle?: ParticleValue;\n\t/** Spawn geometry (default: 'point') */\n\temissionShape?: EmissionShape;\n\t/** Radius for 'circle' shape (default: 0) */\n\temissionRadius?: number;\n\t/** Acceleration in pixels/second^2 (default: {x: 0, y: 0}) */\n\tgravity?: { readonly x: number; readonly y: number };\n\t/** Initial scale (default: 1) */\n\tstartSize?: ParticleValue;\n\t/** Final scale (default: same as startSize) */\n\tendSize?: ParticleValue;\n\t/** Initial opacity (default: 1) */\n\tstartAlpha?: ParticleValue;\n\t/** Final opacity (default: 0) */\n\tendAlpha?: ParticleValue;\n\t/** Initial hex color (default: 0xffffff) */\n\tstartTint?: number;\n\t/** Final hex color (default: same as startTint) */\n\tendTint?: number;\n\t/** Initial rotation in radians (default: 0) */\n\tstartRotation?: ParticleValue;\n\t/** Rotation velocity in rad/s (default: 0) */\n\trotationSpeed?: ParticleValue;\n\t/** Blend mode (default: 'normal') */\n\tblendMode?: ParticleBlendMode;\n\t/** Particles in world coordinates (default: true) */\n\tworldSpace?: boolean;\n}\n\n/**\n * Frozen, fully-resolved particle effect config.\n * Output of defineParticleEffect.\n */\nexport interface ParticleEffectConfig {\n\treadonly maxParticles: number;\n\treadonly texture: unknown;\n\treadonly spawnRate: number;\n\treadonly burstCount: number;\n\treadonly duration: number;\n\treadonly lifetime: ParticleValue;\n\treadonly speed: ParticleValue;\n\treadonly angle: ParticleValue;\n\treadonly emissionShape: EmissionShape;\n\treadonly emissionRadius: number;\n\treadonly gravity: { readonly x: number; readonly y: number };\n\treadonly startSize: ParticleValue;\n\treadonly endSize: ParticleValue;\n\treadonly startAlpha: ParticleValue;\n\treadonly endAlpha: ParticleValue;\n\treadonly startTint: number;\n\treadonly endTint: number;\n\treadonly startRotation: ParticleValue;\n\treadonly rotationSpeed: ParticleValue;\n\treadonly blendMode: ParticleBlendMode;\n\treadonly worldSpace: boolean;\n}\n\n// ==================== Per-Particle Pool Element ====================\n\n/**\n * Mutable per-particle state. Pre-allocated, never GC'd.\n */\nexport interface ParticleState {\n\tactive: boolean;\n\tx: number;\n\ty: number;\n\tvx: number;\n\tvy: number;\n\tlife: number;\n\tmaxLife: number;\n\tsize: number;\n\tstartSize: number;\n\tendSize: number;\n\talpha: number;\n\tstartAlpha: number;\n\tendAlpha: number;\n\ttint: number;\n\trotation: number;\n\trotationSpeed: number;\n}\n\n// ==================== ECS Component ====================\n\n/**\n * Per-entity emitter state stored as an ECS component.\n */\nexport interface ParticleEmitter {\n\treadonly config: ParticleEffectConfig;\n\tactiveCount: number;\n\tspawnAccumulator: number;\n\telapsed: number;\n\tplaying: boolean;\n\tpendingBurst: number;\n\tfinished: boolean;\n\tonComplete?: (data: ParticleEmitterEventData) => void;\n}\n\n/**\n * Component types provided by the particle plugin.\n */\nexport interface ParticleComponentTypes {\n\tparticleEmitter: ParticleEmitter;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Data published when an emitter completes.\n */\nexport interface ParticleEmitterEventData {\n\tentityId: number;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface ParticlePluginOptions<G extends string = 'particles'> extends BasePluginOptions<G> {}\n\n// ==================== Pure Functions (Simulation Engine) ====================\n\n/**\n * Sample a ParticleValue: returns fixed value or random within [min, max].\n */\nexport function sampleRange(value: ParticleValue): number {\n\tif (typeof value === 'number') return value;\n\tconst [min, max] = value;\n\treturn min + Math.random() * (max - min);\n}\n\n/**\n * Linear interpolation between two hex colors (RGB channels).\n */\nexport function lerpTint(start: number, end: number, t: number): number {\n\tif (start === end) return start;\n\tconst sr = (start >> 16) & 0xff;\n\tconst sg = (start >> 8) & 0xff;\n\tconst sb = start & 0xff;\n\tconst er = (end >> 16) & 0xff;\n\tconst eg = (end >> 8) & 0xff;\n\tconst eb = end & 0xff;\n\tconst r = (sr + (er - sr) * t) | 0;\n\tconst g = (sg + (eg - sg) * t) | 0;\n\tconst b = (sb + (eb - sb) * t) | 0;\n\treturn (r << 16) | (g << 8) | b;\n}\n\n// ==================== Config Builder ====================\n\nconst TWO_PI = Math.PI * 2;\n\n/**\n * Define a particle effect config with defaults applied and frozen.\n */\nexport function defineParticleEffect(input: ParticleEffectInput): ParticleEffectConfig {\n\tconst startSize = input.startSize ?? 1;\n\tconst startTint = input.startTint ?? 0xffffff;\n\treturn Object.freeze({\n\t\tmaxParticles: input.maxParticles,\n\t\ttexture: input.texture,\n\t\tspawnRate: input.spawnRate ?? 10,\n\t\tburstCount: input.burstCount ?? 0,\n\t\tduration: input.duration ?? -1,\n\t\tlifetime: input.lifetime ?? 1,\n\t\tspeed: input.speed ?? 100,\n\t\tangle: input.angle ?? [0, TWO_PI] as const,\n\t\temissionShape: input.emissionShape ?? 'point',\n\t\temissionRadius: input.emissionRadius ?? 0,\n\t\tgravity: Object.freeze(input.gravity ?? { x: 0, y: 0 }),\n\t\tstartSize,\n\t\tendSize: input.endSize ?? startSize,\n\t\tstartAlpha: input.startAlpha ?? 1,\n\t\tendAlpha: input.endAlpha ?? 0,\n\t\tstartTint,\n\t\tendTint: input.endTint ?? startTint,\n\t\tstartRotation: input.startRotation ?? 0,\n\t\trotationSpeed: input.rotationSpeed ?? 0,\n\t\tblendMode: input.blendMode ?? 'normal',\n\t\tworldSpace: input.worldSpace ?? true,\n\t});\n}\n\n// ==================== Component Factory ====================\n\n/**\n * Create a particleEmitter component suitable for spreading into spawn().\n */\nexport function createParticleEmitter(\n\tconfig: ParticleEffectConfig,\n\toptions?: {\n\t\tplaying?: boolean;\n\t\tonComplete?: (data: ParticleEmitterEventData) => void;\n\t},\n): Pick<ParticleComponentTypes, 'particleEmitter'> {\n\treturn {\n\t\tparticleEmitter: {\n\t\t\tconfig,\n\t\t\tactiveCount: 0,\n\t\t\tspawnAccumulator: 0,\n\t\t\telapsed: 0,\n\t\t\tplaying: options?.playing ?? true,\n\t\t\tpendingBurst: 0,\n\t\t\tfinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Queue a burst of particles on an emitter.\n * Returns false if entity has no particleEmitter component.\n */\nexport function burstParticles(\n\tecs: ParticleWorld,\n\tentityId: number,\n\tcount?: number,\n): boolean {\n\tconst emitter = ecs.getComponent(entityId, 'particleEmitter');\n\tif (!emitter) return false;\n\temitter.pendingBurst += count ?? emitter.config.burstCount;\n\tecs.markChanged(entityId, 'particleEmitter');\n\treturn true;\n}\n\n/**\n * Stop an emitter from spawning new particles.\n * Existing particles continue their lifecycle.\n */\nexport function stopEmitter(\n\tecs: ParticleWorld,\n\tentityId: number,\n): boolean {\n\tconst emitter = ecs.getComponent(entityId, 'particleEmitter');\n\tif (!emitter) return false;\n\temitter.playing = false;\n\treturn true;\n}\n\n/**\n * Resume a stopped emitter.\n */\nexport function resumeEmitter(\n\tecs: ParticleWorld,\n\tentityId: number,\n): boolean {\n\tconst emitter = ecs.getComponent(entityId, 'particleEmitter');\n\tif (!emitter) return false;\n\temitter.playing = true;\n\treturn true;\n}\n\n// ==================== Side Storage ====================\n\n/**\n * Runtime data stored outside the ECS, keyed by entity ID.\n */\nexport interface EmitterRuntimeData {\n\tparticles: ParticleState[];\n\tpixiContainer: unknown;\n\tpixiParticles: unknown[];\n}\n\n// ==================== Spawn Logic ====================\n\nfunction spawnParticle(\n\tparticle: ParticleState,\n\tconfig: ParticleEffectConfig,\n\temitterX: number,\n\temitterY: number,\n\temitterRotation: number,\n): void {\n\tparticle.active = true;\n\tconst life = sampleRange(config.lifetime);\n\tparticle.life = life;\n\tparticle.maxLife = life;\n\n\t// Position from emission shape\n\tif (config.emissionShape === 'circle' && config.emissionRadius > 0) {\n\t\tconst angle = Math.random() * TWO_PI;\n\t\tconst radius = Math.random() * config.emissionRadius;\n\t\tparticle.x = emitterX + Math.cos(angle) * radius;\n\t\tparticle.y = emitterY + Math.sin(angle) * radius;\n\t} else {\n\t\tparticle.x = emitterX;\n\t\tparticle.y = emitterY;\n\t}\n\n\t// Velocity from speed + angle + emitter rotation\n\tconst speed = sampleRange(config.speed);\n\tconst angle = sampleRange(config.angle) + emitterRotation;\n\tparticle.vx = Math.cos(angle) * speed;\n\tparticle.vy = Math.sin(angle) * speed;\n\n\t// Visual properties\n\tparticle.startSize = sampleRange(config.startSize);\n\tparticle.endSize = sampleRange(config.endSize);\n\tparticle.size = particle.startSize;\n\tparticle.startAlpha = sampleRange(config.startAlpha);\n\tparticle.endAlpha = sampleRange(config.endAlpha);\n\tparticle.alpha = particle.startAlpha;\n\tparticle.tint = config.startTint;\n\tparticle.rotation = sampleRange(config.startRotation);\n\tparticle.rotationSpeed = sampleRange(config.rotationSpeed);\n}\n\n// ==================== Update Logic ====================\n\nfunction updateParticles(\n\temitter: ParticleEmitter,\n\tdata: EmitterRuntimeData,\n\tdt: number,\n\temitterX: number,\n\temitterY: number,\n\temitterRotation: number,\n): void {\n\tconst config = emitter.config;\n\n\t// Update emitter elapsed time\n\temitter.elapsed += dt;\n\n\t// Determine if spawning is allowed\n\tconst durationExpired = config.duration >= 0 && emitter.elapsed >= config.duration;\n\tconst canSpawn = emitter.playing && !durationExpired;\n\n\t// Continuous spawning\n\tif (canSpawn && config.spawnRate > 0) {\n\t\temitter.spawnAccumulator += config.spawnRate * dt;\n\t\tconst toSpawn = Math.floor(emitter.spawnAccumulator);\n\t\temitter.spawnAccumulator -= toSpawn;\n\n\t\tfor (let i = 0; i < toSpawn; i++) {\n\t\t\tif (emitter.activeCount >= config.maxParticles) break;\n\t\t\tconst particle = data.particles[emitter.activeCount];\n\t\t\tif (!particle) break;\n\t\t\tspawnParticle(particle, config, emitterX, emitterY, emitterRotation);\n\t\t\temitter.activeCount++;\n\t\t}\n\t}\n\n\t// Burst spawning\n\tif (emitter.pendingBurst > 0) {\n\t\tconst burstCount = Math.min(\n\t\t\temitter.pendingBurst,\n\t\t\tconfig.maxParticles - emitter.activeCount,\n\t\t);\n\t\tfor (let i = 0; i < burstCount; i++) {\n\t\t\tconst particle = data.particles[emitter.activeCount];\n\t\t\tif (!particle) break;\n\t\t\tspawnParticle(particle, config, emitterX, emitterY, emitterRotation);\n\t\t\temitter.activeCount++;\n\t\t}\n\t\temitter.pendingBurst -= burstCount;\n\t}\n\n\t// Update active particles\n\tconst gravityX = config.gravity.x;\n\tconst gravityY = config.gravity.y;\n\tconst hasGravity = gravityX !== 0 || gravityY !== 0;\n\tconst hasTintLerp = config.startTint !== config.endTint;\n\n\tlet i = 0;\n\twhile (i < emitter.activeCount) {\n\t\tconst p = data.particles[i];\n\t\tif (!p) break;\n\n\t\tp.life -= dt;\n\n\t\tif (p.life <= 0) {\n\t\t\t// Swap-and-pop: move last active particle to this slot\n\t\t\temitter.activeCount--;\n\t\t\tif (i < emitter.activeCount) {\n\t\t\t\tconst last = data.particles[emitter.activeCount];\n\t\t\t\tif (last) {\n\t\t\t\t\t// Copy last particle data to current slot\n\t\t\t\t\tdata.particles[i] = last;\n\t\t\t\t\tdata.particles[emitter.activeCount] = p;\n\t\t\t\t\t// Also swap PixiJS particle refs\n\t\t\t\t\tconst tmpPixi = data.pixiParticles[i];\n\t\t\t\t\tdata.pixiParticles[i] = data.pixiParticles[emitter.activeCount];\n\t\t\t\t\tdata.pixiParticles[emitter.activeCount] = tmpPixi;\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.active = false;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Physics\n\t\tif (hasGravity) {\n\t\t\tp.vx += gravityX * dt;\n\t\t\tp.vy += gravityY * dt;\n\t\t}\n\t\tp.x += p.vx * dt;\n\t\tp.y += p.vy * dt;\n\n\t\t// Interpolation\n\t\tconst t = 1 - p.life / p.maxLife;\n\t\tp.size = p.startSize + (p.endSize - p.startSize) * t;\n\t\tp.alpha = p.startAlpha + (p.endAlpha - p.startAlpha) * t;\n\n\t\tif (hasTintLerp) {\n\t\t\tp.tint = lerpTint(config.startTint, config.endTint, t);\n\t\t}\n\n\t\t// Rotation\n\t\tp.rotation += p.rotationSpeed * dt;\n\n\t\ti++;\n\t}\n}\n\n// ==================== Pool Allocation ====================\n\nfunction createParticlePool(maxParticles: number): ParticleState[] {\n\tconst pool: ParticleState[] = new Array(maxParticles);\n\tfor (let i = 0; i < maxParticles; i++) {\n\t\tpool[i] = {\n\t\t\tactive: false,\n\t\t\tx: 0, y: 0,\n\t\t\tvx: 0, vy: 0,\n\t\t\tlife: 0, maxLife: 0,\n\t\t\tsize: 0,\n\t\t\tstartSize: 0, endSize: 0,\n\t\t\talpha: 0,\n\t\t\tstartAlpha: 0, endAlpha: 0,\n\t\t\ttint: 0xffffff,\n\t\t\trotation: 0,\n\t\t\trotationSpeed: 0,\n\t\t};\n\t}\n\treturn pool;\n}\n\n// ==================== Presets ====================\n\nexport const particlePresets = {\n\texplosion(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 50,\n\t\t\ttexture,\n\t\t\tspawnRate: 0,\n\t\t\tburstCount: 30,\n\t\t\tduration: 1,\n\t\t\tlifetime: [0.3, 0.8],\n\t\t\tspeed: [100, 300],\n\t\t\tangle: [0, TWO_PI],\n\t\t\tstartSize: [0.5, 1.5],\n\t\t\tendSize: [0.1, 0.3],\n\t\t\tstartAlpha: 1,\n\t\t\tendAlpha: 0,\n\t\t\t...overrides,\n\t\t});\n\t},\n\n\tsmoke(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 60,\n\t\t\ttexture,\n\t\t\tspawnRate: 15,\n\t\t\tduration: -1,\n\t\t\tlifetime: [1, 3],\n\t\t\tspeed: [20, 60],\n\t\t\tangle: [-Math.PI / 2 - 0.3, -Math.PI / 2 + 0.3],\n\t\t\tstartSize: [0.3, 0.6],\n\t\t\tendSize: [1, 2],\n\t\t\tstartAlpha: 0.4,\n\t\t\tendAlpha: 0,\n\t\t\t...overrides,\n\t\t});\n\t},\n\n\tfire(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 80,\n\t\t\ttexture,\n\t\t\tspawnRate: 30,\n\t\t\tduration: -1,\n\t\t\tlifetime: [0.3, 1],\n\t\t\tspeed: [40, 120],\n\t\t\tangle: [-Math.PI / 2 - 0.5, -Math.PI / 2 + 0.5],\n\t\t\tstartSize: [0.5, 1],\n\t\t\tendSize: [0.1, 0.3],\n\t\t\tstartAlpha: 1,\n\t\t\tendAlpha: 0,\n\t\t\tstartTint: 0xff8800,\n\t\t\tendTint: 0xff2200,\n\t\t\tblendMode: 'add',\n\t\t\t...overrides,\n\t\t});\n\t},\n\n\tsparkle(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 30,\n\t\t\ttexture,\n\t\t\tspawnRate: 10,\n\t\t\tduration: -1,\n\t\t\tlifetime: [0.5, 1.5],\n\t\t\tspeed: [10, 40],\n\t\t\tangle: [0, TWO_PI],\n\t\t\tstartSize: [0.2, 0.8],\n\t\t\tendSize: [0.1, 0.4],\n\t\t\tstartAlpha: [0.5, 1],\n\t\t\tendAlpha: 0,\n\t\t\t...overrides,\n\t\t});\n\t},\n\n\ttrail(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 40,\n\t\t\ttexture,\n\t\t\tspawnRate: 20,\n\t\t\tduration: -1,\n\t\t\tlifetime: [0.3, 0.8],\n\t\t\tspeed: 0,\n\t\t\tstartSize: [0.5, 1],\n\t\t\tendSize: [0.05, 0.2],\n\t\t\tstartAlpha: 0.8,\n\t\t\tendAlpha: 0,\n\t\t\t...overrides,\n\t\t});\n\t},\n} as const;\n\n// ==================== Plugin Factory ====================\n\ntype ParticleLabels = 'particle-update' | 'particle-render-sync';\n\ntype ParticleRequires = WorldConfigFrom<TransformComponentTypes & { renderLayer: string }>;\n\n/**\n * Create a particle system plugin for ECSpresso.\n *\n * Provides:\n * - Pre-allocated particle pools outside the entity system\n * - Continuous and burst emission modes\n * - Velocity, gravity, lifetime, interpolation (size, alpha, tint, rotation)\n * - World-space and local-space particle emission\n * - PixiJS ParticleContainer rendering (via renderer2D dependency)\n * - Presets for common effects (explosion, smoke, fire, sparkle, trail)\n *\n * Renderer2D is a required dependency.\n */\nexport function createParticlePlugin<\n\tG extends string = 'particles',\n>(\n\toptions?: ParticlePluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'particles',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\t// Side storage for runtime particle data\n\tconst emitterData = new Map<number, EmitterRuntimeData>();\n\n\treturn definePlugin('particles')\n\t\t.withComponentTypes<ParticleComponentTypes>()\n\t\t.withLabels<ParticleLabels>()\n\t\t.withGroups<G>()\n\t\t.withReactiveQueryNames<'particle-emitters'>()\n\t\t.requires<ParticleRequires>()\n\t\t.install((world) => {\n\t\t\t// Required component: particleEmitter needs localTransform\n\t\t\tworld.registerRequired('particleEmitter', 'localTransform', (): LocalTransform => ({\n\t\t\t\tx: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1,\n\t\t\t}));\n\n\t\t\t// Dispose: clean up side storage when particleEmitter removed\n\t\t\tworld.registerDispose('particleEmitter', ({ entityId }: { value: ParticleEmitter; entityId: number }) => {\n\t\t\t\tconst data = emitterData.get(entityId);\n\t\t\t\tif (data) {\n\t\t\t\t\t// Remove PixiJS container from scene graph\n\t\t\t\t\tconst container = data.pixiContainer as { removeFromParent?: () => void; destroy?: () => void } | null;\n\t\t\t\t\tif (container) {\n\t\t\t\t\t\tcontainer.removeFromParent?.();\n\t\t\t\t\t\tcontainer.destroy?.();\n\t\t\t\t\t}\n\t\t\t\t\temitterData.delete(entityId);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// ==================== Particle Update System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('particle-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('emitters', {\n\t\t\t\t\twith: ['particleEmitter'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.emitters) {\n\t\t\t\t\t\tconst emitter = entity.components.particleEmitter;\n\n\t\t\t\t\t\t// Lazily create particle pool on first encounter\n\t\t\t\t\t\tlet data = emitterData.get(entity.id);\n\t\t\t\t\t\tif (!data) {\n\t\t\t\t\t\t\tdata = {\n\t\t\t\t\t\t\t\tparticles: createParticlePool(emitter.config.maxParticles),\n\t\t\t\t\t\t\t\tpixiContainer: null,\n\t\t\t\t\t\t\t\tpixiParticles: [],\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\temitterData.set(entity.id, data);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst worldTransform = ecs.getComponent(entity.id, 'worldTransform');\n\t\t\t\t\t\tconst ex = worldTransform?.x ?? 0;\n\t\t\t\t\t\tconst ey = worldTransform?.y ?? 0;\n\t\t\t\t\t\tconst erot = worldTransform?.rotation ?? 0;\n\n\t\t\t\t\t\tupdateParticles(emitter, data, dt, ex, ey, erot);\n\n\t\t\t\t\t\t// Check completion\n\t\t\t\t\t\tconst config = emitter.config;\n\t\t\t\t\t\tconst durationExpired = config.duration >= 0 && emitter.elapsed >= config.duration;\n\t\t\t\t\t\tif (durationExpired && emitter.activeCount === 0 && !emitter.finished) {\n\t\t\t\t\t\t\temitter.finished = true;\n\n\t\t\t\t\t\t\tif (emitter.onComplete) {\n\t\t\t\t\t\t\t\temitter.onComplete({ entityId: entity.id });\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tecs.commands.removeComponent(entity.id, 'particleEmitter');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Particle Render Sync System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('particle-render-sync')\n\t\t\t\t.setPriority(400)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs) => {\n\t\t\t\t\t// Dynamic import PixiJS\n\t\t\t\t\tconst pixi = await import('pixi.js');\n\t\t\t\t\tconst ParticleContainerClass = pixi.ParticleContainer;\n\t\t\t\t\tconst ParticleClass = pixi.Particle;\n\n\t\t\t\t\t// Get root container\n\t\t\t\t\tconst rootContainer = ecs.tryGetResource<{ addChild(child: unknown): void }>('rootContainer');\n\n\t\t\t\t\t// Reactive query for particleEmitter component\n\t\t\t\t\tecs.addReactiveQuery('particle-emitters', {\n\t\t\t\t\t\twith: ['particleEmitter'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\tconst emitter = entity.components.particleEmitter;\n\t\t\t\t\t\t\tconst config = emitter.config;\n\n\t\t\t\t\t\t\t// Create PixiJS ParticleContainer\n\t\t\t\t\t\t\tconst pixiContainer = new ParticleContainerClass({\n\t\t\t\t\t\t\t\tdynamicProperties: {\n\t\t\t\t\t\t\t\t\tposition: true,\n\t\t\t\t\t\t\t\t\trotation: true,\n\t\t\t\t\t\t\t\t\tcolor: true,\n\t\t\t\t\t\t\t\t\tvertex: true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t// Set blend mode\n\t\t\t\t\t\t\tpixiContainer.blendMode = config.blendMode;\n\n\t\t\t\t\t\t\t// Pre-allocate Particle objects\n\t\t\t\t\t\t\tconst pixiParticles: InstanceType<typeof ParticleClass>[] = [];\n\t\t\t\t\t\t\tfor (let i = 0; i < config.maxParticles; i++) {\n\t\t\t\t\t\t\t\tconst p = new ParticleClass({\n\t\t\t\t\t\t\t\t\ttexture: config.texture,\n\t\t\t\t\t\t\t\t} as ConstructorParameters<typeof ParticleClass>[0]);\n\t\t\t\t\t\t\t\tp.alpha = 0;\n\t\t\t\t\t\t\t\tpixiParticles.push(p);\n\t\t\t\t\t\t\t\tpixiContainer.addParticle(p);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Create pre-allocated pool\n\t\t\t\t\t\t\tconst particles = createParticlePool(config.maxParticles);\n\n\t\t\t\t\t\t\t// Add to scene (cross-plugin structural access for renderLayer)\n\t\t\t\t\t\t\tif (rootContainer) {\n\t\t\t\t\t\t\t\tconst layerName = ecs.getComponent(entity.id, 'renderLayer');\n\t\t\t\t\t\t\t\tif (layerName) {\n\t\t\t\t\t\t\t\t\t(rootContainer as { addChild(child: unknown): void }).addChild(pixiContainer);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t(rootContainer as { addChild(child: unknown): void }).addChild(pixiContainer);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Store in side storage\n\t\t\t\t\t\t\temitterData.set(entity.id, {\n\t\t\t\t\t\t\t\tparticles,\n\t\t\t\t\t\t\t\tpixiContainer,\n\t\t\t\t\t\t\t\tpixiParticles,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tconst data = emitterData.get(entityId);\n\t\t\t\t\t\t\tif (data) {\n\t\t\t\t\t\t\t\tconst container = data.pixiContainer as { removeFromParent?: () => void; destroy?: () => void } | null;\n\t\t\t\t\t\t\t\tif (container) {\n\t\t\t\t\t\t\t\t\tcontainer.removeFromParent?.();\n\t\t\t\t\t\t\t\t\tcontainer.destroy?.();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\temitterData.delete(entityId);\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\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\t// Sync ParticleState -> PixiJS Particle properties\n\t\t\t\t\tfor (const [entityId, data] of emitterData) {\n\t\t\t\t\t\tconst emitter = ecs.getComponent(entityId, 'particleEmitter');\n\t\t\t\t\t\tif (!emitter) continue;\n\n\t\t\t\t\t\tconst config = emitter.config;\n\n\t\t\t\t\t\t// Local-space: sync container position to emitter's worldTransform\n\t\t\t\t\t\tif (!config.worldSpace) {\n\t\t\t\t\t\t\tconst wt = ecs.getComponent(entityId, 'worldTransform');\n\t\t\t\t\t\t\tif (wt) {\n\t\t\t\t\t\t\t\tconst container = data.pixiContainer as {\n\t\t\t\t\t\t\t\t\tposition: { set(x: number, y: number): void };\n\t\t\t\t\t\t\t\t\trotation: number;\n\t\t\t\t\t\t\t\t\tscale: { set(x: number, y: number): void };\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\tcontainer.position.set(wt.x, wt.y);\n\t\t\t\t\t\t\t\tcontainer.rotation = wt.rotation;\n\t\t\t\t\t\t\t\tcontainer.scale.set(wt.scaleX, wt.scaleY);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Sync active particles\n\t\t\t\t\t\tfor (let i = 0; i < emitter.activeCount; i++) {\n\t\t\t\t\t\t\tconst ps = data.particles[i];\n\t\t\t\t\t\t\tconst pp = data.pixiParticles[i] as {\n\t\t\t\t\t\t\t\tx: number;\n\t\t\t\t\t\t\t\ty: number;\n\t\t\t\t\t\t\t\tscaleX: number;\n\t\t\t\t\t\t\t\tscaleY: number;\n\t\t\t\t\t\t\t\trotation: number;\n\t\t\t\t\t\t\t\ttint: number;\n\t\t\t\t\t\t\t\talpha: number;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tif (!ps || !pp) continue;\n\t\t\t\t\t\t\tpp.x = ps.x;\n\t\t\t\t\t\t\tpp.y = ps.y;\n\t\t\t\t\t\t\tpp.scaleX = ps.size;\n\t\t\t\t\t\t\tpp.scaleY = ps.size;\n\t\t\t\t\t\t\tpp.rotation = ps.rotation;\n\t\t\t\t\t\t\tpp.tint = ps.tint;\n\t\t\t\t\t\t\tpp.alpha = ps.alpha;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Hide inactive particles\n\t\t\t\t\t\tfor (let i = emitter.activeCount; i < config.maxParticles; i++) {\n\t\t\t\t\t\t\tconst pp = data.pixiParticles[i] as { alpha: number } | undefined;\n\t\t\t\t\t\t\tif (pp) {\n\t\t\t\t\t\t\t\tpp.alpha = 0;\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}\n\n/**\n * Get the runtime data for an emitter entity.\n * Useful for tests and advanced usage.\n * @internal Exported for testing only.\n */\nexport function getEmitterData(\n\temitterDataMap: Map<number, EmitterRuntimeData>,\n\tentityId: number,\n): EmitterRuntimeData | undefined {\n\treturn emitterDataMap.get(entityId);\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "60D36D4494C6248064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
* 2D Renderer Plugin for ECSpresso
|
|
3
3
|
*
|
|
4
4
|
* An opt-in PixiJS-based 2D rendering plugin that automates scene graph wiring.
|
|
5
|
-
* Import from 'ecspresso/plugins/
|
|
5
|
+
* Import from 'ecspresso/plugins/rendering/renderer2D'
|
|
6
6
|
*
|
|
7
7
|
* This plugin includes transform propagation automatically.
|
|
8
8
|
*/
|
|
9
9
|
import type { Application, ApplicationOptions, Container, Sprite, Graphics } from 'pixi.js';
|
|
10
10
|
import { type Plugin } from 'ecspresso';
|
|
11
11
|
import type { WorldConfigFrom, EmptyConfig } from '../../type-utils';
|
|
12
|
-
import { type LocalTransform, type WorldTransform, type TransformComponentTypes, type TransformPluginOptions } from 'ecspresso/plugins/transform';
|
|
13
|
-
import { type BoundsRect } from 'ecspresso/plugins/bounds';
|
|
14
|
-
import type { CameraResourceTypes } from 'ecspresso/plugins/camera';
|
|
12
|
+
import { type LocalTransform, type WorldTransform, type TransformComponentTypes, type TransformPluginOptions } from 'ecspresso/plugins/spatial/transform';
|
|
13
|
+
import { type BoundsRect } from 'ecspresso/plugins/spatial/bounds';
|
|
14
|
+
import type { CameraResourceTypes } from 'ecspresso/plugins/spatial/camera';
|
|
15
15
|
export type { LocalTransform, WorldTransform, TransformComponentTypes };
|
|
16
16
|
export type { BoundsRect };
|
|
17
|
-
export { createTransform, createLocalTransform, createWorldTransform, DEFAULT_LOCAL_TRANSFORM, DEFAULT_WORLD_TRANSFORM } from 'ecspresso/plugins/transform';
|
|
17
|
+
export { createTransform, createLocalTransform, createWorldTransform, DEFAULT_LOCAL_TRANSFORM, DEFAULT_WORLD_TRANSFORM } from 'ecspresso/plugins/spatial/transform';
|
|
18
18
|
/**
|
|
19
19
|
* Visibility and alpha component
|
|
20
20
|
*/
|
|
@@ -98,6 +98,10 @@ interface Renderer2DPluginCommonOptions<G extends string = 'renderer2d'> {
|
|
|
98
98
|
startLoop?: boolean;
|
|
99
99
|
/** Ordered render layer names (back-to-front). Entities with a renderLayer component are placed in the corresponding container. */
|
|
100
100
|
renderLayers?: string[];
|
|
101
|
+
/** Render layers that should not be affected by camera transforms.
|
|
102
|
+
* These layers are placed outside rootContainer so camera zoom/pan/rotation does not apply.
|
|
103
|
+
* Only relevant when `camera: true`. Layer names listed here must also appear in `renderLayers`. */
|
|
104
|
+
screenSpaceLayers?: string[];
|
|
101
105
|
/** Automatically apply cameraState resource to rootContainer each frame.
|
|
102
106
|
* Requires the camera plugin to be installed. (default: false) */
|
|
103
107
|
camera?: boolean;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var y=((Q)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(Q,{get:(Z,K)=>(typeof require<"u"?require:Z)[K]}):Q)(function(Q){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+Q+'" is not supported')});import{definePlugin as l}from"ecspresso";import{createTransformPlugin as c}from"ecspresso/plugins/spatial/transform";import{createBounds as I}from"ecspresso/plugins/spatial/bounds";import{createTransform as _F,createLocalTransform as $F,createWorldTransform as zF,DEFAULT_LOCAL_TRANSFORM as DF,DEFAULT_WORLD_TRANSFORM as kF}from"ecspresso/plugins/spatial/transform";async function p(Q){let{Application:Z}=await import("pixi.js"),K=new Z;return await K.init(Q),K}function H(Q,Z){let K=Z?.scale,M=typeof K==="number"?K:K?.x??1,q=typeof K==="number"?K:K?.y??1;return{x:Q?.x??0,y:Q?.y??0,rotation:Z?.rotation??0,scaleX:M,scaleY:q}}function u(Q,Z){let K=Z?.scale,M=typeof K==="number"?K:K?.x??1,q=typeof K==="number"?K:K?.y??1;return{x:Q?.x??0,y:Q?.y??0,rotation:Z?.rotation??0,scaleX:M,scaleY:q}}function P(Q){return{visible:Q?.visible??!0,alpha:Q?.alpha}}function i(Q,Z,K){if(K?.anchor)Q.anchor.set(K.anchor.x,K.anchor.y);return{sprite:Q,localTransform:H(Z,K),worldTransform:u(Z,K),visible:P(K)}}function e(Q,Z,K){return{graphics:Q,localTransform:H(Z,K),worldTransform:u(Z,K),visible:P(K)}}function FF(Q,Z,K){return{container:Q,localTransform:H(Z,K),worldTransform:u(Z,K),visible:P(K)}}var a={fit:(Q,Z)=>{let K=Math.min(Q,Z);return{scaleX:K,scaleY:K}},cover:(Q,Z)=>{let K=Math.max(Q,Z);return{scaleX:K,scaleY:K}},stretch:(Q,Z)=>({scaleX:Q,scaleY:Z})};function S(Q,Z,K,M,q){let V=Q/K,A=Z/M,{scaleX:W,scaleY:R}=a[q](V,A);return{scaleX:W,scaleY:R,offsetX:(Q-K*W)/2,offsetY:(Z-M*R)/2,physicalWidth:Q,physicalHeight:Z,mode:q,designWidth:K,designHeight:M}}function s(Q,Z,K){return{x:(Q-K.offsetX)/K.scaleX,y:(Z-K.offsetY)/K.scaleY}}function JF(Q,Z,K,M){let q=K.getBoundingClientRect(),V=(Q-q.left)*(M.physicalWidth/q.width),A=(Z-q.top)*(M.physicalHeight/q.height);return s(V,A,M)}function KF(Q){Q.renderer.emit("resize",Q.screen.width,Q.screen.height,Q.renderer.resolution)}function QF(Q){let{rootContainer:Z,systemGroup:K="renderer2d",renderSyncPriority:M=500,transform:q,startLoop:V=!0,renderLayers:A=[],screenSpaceLayers:W=[],camera:R=!1,screenScale:f}=Q,G=f!==void 0,L=f?.width??0,X=f?.height??0,x=f?.mode??"fit",Y=new Map,v=new Map,C=new Set(W),g=()=>{throw Error("renderer2D: createLayerContainer called before initialization")},O=null;function m($,J){let U=v.get($);if(U)return U;let _=g(`layer:${$}`);return v.set($,_),(O&&C.has($)?O:J).addChild(_),_}function h($,J){let U=J.getResource("rootContainer"),_=J.getComponent($,"renderLayer");if(_)return m(_,U);return U}function T($,J,U){let _=h($,U);if(J.parent!==_)_.addChild(J)}function b($,J){let U=Y.get($);if(!U)return;let _=h($,J);if(U.parent!==_)U.removeFromParent(),_.addChild(U)}let w=!(("app"in Q)&&Q.app!==void 0);return l("renderer2d").withComponentTypes().withEventTypes().withResourceTypes().withLabels().withGroups().withReactiveQueryNames().install(($)=>{if($.installPlugin(c(q)),w){let J=Q,{pixiInit:U,background:_,width:D,height:z}=J,F=J.container??document.body,k=typeof F==="string"?document.querySelector(F):F,B={...U,..._!==void 0&&{background:_},...D!==void 0&&{width:D},...z!==void 0&&{height:z}},E=k!==null&&B.resizeTo===void 0&&B.width===void 0&&B.height===void 0,d={...B,...E&&{resizeTo:k}};if($.addResource("pixiApp",async()=>{let N=await p(d);if(k)k.appendChild(N.canvas);else if(typeof F==="string")console.warn(`Renderer2D plugin: container selector "${F}" not found`);return N}),$.addResource("rootContainer",{dependsOn:["pixiApp"],factory:(N)=>Z??N.getResource("pixiApp").stage}),$.addResource("bounds",{dependsOn:["pixiApp"],factory:(N)=>{if(G)return I(L,X);let j=N.getResource("pixiApp");return I(j.screen.width,j.screen.height)}}),G)$.addResource("viewportScale",{dependsOn:["pixiApp"],factory:(N)=>{let j=N.getResource("pixiApp");return S(j.screen.width,j.screen.height,L,X,x)}})}else{let J=Q.app;if($.addResource("pixiApp",J),$.addResource("rootContainer",Z??J.stage),$.addResource("bounds",G?I(L,X):I(J.screen.width,J.screen.height)),G)$.addResource("viewportScale",S(J.screen.width,J.screen.height,L,X,x))}if($.registerDispose("sprite",({value:J})=>{J.removeFromParent()}),$.registerDispose("graphics",({value:J})=>{J.removeFromParent()}),$.registerDispose("container",({value:J})=>{J.removeFromParent()}),$.registerRequired("sprite","localTransform",()=>H()),$.registerRequired("sprite","visible",()=>P()),$.registerRequired("graphics","localTransform",()=>H()),$.registerRequired("graphics","visible",()=>P()),$.registerRequired("container","localTransform",()=>H()),$.registerRequired("container","visible",()=>P()),$.addSystem("renderer2d-sync").setPriority(M).inPhase("render").inGroup(K).addQuery("sprites",{with:["sprite","worldTransform"],changed:["worldTransform"]}).addQuery("graphics",{with:["graphics","worldTransform"],changed:["worldTransform"]}).addQuery("containers",{with:["container","worldTransform"],changed:["worldTransform"]}).setProcess(({queries:J,ecs:U})=>{for(let _ of J.sprites){let{sprite:D,worldTransform:z}=_.components;D.position.set(z.x,z.y),D.rotation=z.rotation,D.scale.set(z.scaleX,z.scaleY);let F=U.getComponent(_.id,"visible");if(F){if(D.visible=F.visible,F.alpha!==void 0)D.alpha=F.alpha}}for(let _ of J.graphics){let{graphics:D,worldTransform:z}=_.components;D.position.set(z.x,z.y),D.rotation=z.rotation,D.scale.set(z.scaleX,z.scaleY);let F=U.getComponent(_.id,"visible");if(F){if(D.visible=F.visible,F.alpha!==void 0)D.alpha=F.alpha}}for(let _ of J.containers){let{container:D,worldTransform:z}=_.components;D.position.set(z.x,z.y),D.rotation=z.rotation,D.scale.set(z.scaleX,z.scaleY);let F=U.getComponent(_.id,"visible");if(F){if(D.visible=F.visible,F.alpha!==void 0)D.alpha=F.alpha}}}),$.addSystem("renderer2d-scene-graph").setPriority(9999).inGroup(K).setOnInitialize(async(J)=>{let U=J.getResource("pixiApp"),_=J.getResource("rootContainer"),{Container:D}=await import("pixi.js");g=(F)=>{let k=new D;return k.label=F,k};let z;if(G){z=new D,z.label="viewportContainer";let F=J.tryGetResource("viewportScale");if(!F)throw Error("renderer2D: viewportScale resource not found");z.position.set(F.offsetX,F.offsetY),z.scale.set(F.scaleX,F.scaleY);let k=new D;k.label="rootContainer",U.stage.addChild(z),z.addChild(k),J.updateResource("rootContainer",()=>k),_=k}if(R&&C.size>0){if(_===U.stage){let F=new D;F.label="rootContainer",U.stage.addChild(F),J.updateResource("rootContainer",()=>F),_=F}O=_.parent??U.stage}for(let F of A)m(F,_);if(J.addReactiveQuery("renderer2d-sprites",{with:["sprite"],onEnter:(F)=>{let k=F.components.sprite;Y.set(F.id,k),T(F.id,k,J)},onExit:(F)=>{Y.delete(F)}}),J.addReactiveQuery("renderer2d-graphics",{with:["graphics"],onEnter:(F)=>{let k=F.components.graphics;Y.set(F.id,k),T(F.id,k,J)},onExit:(F)=>{Y.delete(F)}}),J.addReactiveQuery("renderer2d-containers",{with:["container"],onEnter:(F)=>{let k=F.components.container;Y.set(F.id,k),T(F.id,k,J)},onExit:(F)=>{Y.delete(F)}}),J.on("hierarchyChanged",({entityId:F})=>{b(F,J)}),J.onComponentAdded("renderLayer",({entity:F})=>{b(F.id,J)}),J.onComponentRemoved("renderLayer",({entity:F})=>{b(F.id,J)}),R){let F=J.tryGetResource("cameraState");if(!F)throw Error("renderer2D: cameraState resource not found");F.viewportWidth=G?L:U.screen.width,F.viewportHeight=G?X:U.screen.height}if(U.renderer.on("resize",(F,k)=>{if(G){let B=J.tryGetResource("viewportScale");if(!B)throw Error("renderer2D: viewportScale resource not found");let E=S(F,k,L,X,B.mode);if(B.scaleX=E.scaleX,B.scaleY=E.scaleY,B.offsetX=E.offsetX,B.offsetY=E.offsetY,B.physicalWidth=F,B.physicalHeight=k,z)z.position.set(E.offsetX,E.offsetY),z.scale.set(E.scaleX,E.scaleY)}else{let B=J.getResource("bounds");if(B.width=F,B.height=k,R){let E=J.tryGetResource("cameraState");if(!E)throw Error("renderer2D: cameraState resource not found");E.viewportWidth=F,E.viewportHeight=k}}}),V)U.ticker.add((F)=>{J.update(F.deltaMS/1000)})}),R)$.addSystem("renderer2d-camera-sync").setPriority(900).inPhase("render").inGroup(K).setProcess(({ecs:J})=>{let U=J.tryGetResource("cameraState");if(!U)throw Error("renderer2D: cameraState resource not found");let _=J.getResource("rootContainer"),[D,z]=G?[L,X]:[J.getResource("pixiApp").screen.width,J.getResource("pixiApp").screen.height];_.position.set(D/2-(U.x+U.shakeOffsetX)*U.zoom,z/2-(U.y+U.shakeOffsetY)*U.zoom),_.scale.set(U.zoom),_.rotation=-(U.rotation+U.shakeRotation)})})}export{KF as reapplyViewportScale,s as physicalToLogical,zF as createWorldTransform,_F as createTransform,i as createSpriteComponents,QF as createRenderer2DPlugin,$F as createLocalTransform,e as createGraphicsComponents,FF as createContainerComponents,S as computeViewportScale,JF as clientToLogical,kF as DEFAULT_WORLD_TRANSFORM,DF as DEFAULT_LOCAL_TRANSFORM};
|
|
2
|
+
|
|
3
|
+
//# debugId=91C76A4DF6430B7F64756E2164756E21
|
|
4
|
+
//# sourceMappingURL=renderer2D.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/plugins/rendering/renderer2D.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
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/rendering/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/spatial/transform';\nimport { createBounds, type BoundsRect } from 'ecspresso/plugins/spatial/bounds';\nimport type { CameraResourceTypes, CameraState } from 'ecspresso/plugins/spatial/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/spatial/transform';\n\n// Dynamic import for Application to avoid requiring pixi.js at plugin creation time\n// when using managed mode (pixiInit 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\t/** Current scale mode. Mutable — call `reapplyViewportScale(pixiApp)` after changing to re-apply immediately. */\n\tmode: ScaleMode;\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/** Render layers that should not be affected by camera transforms.\n\t * These layers are placed outside rootContainer so camera zoom/pan/rotation does not apply.\n\t * Only relevant when `camera: true`. Layer names listed here must also appear in `renderLayers`. */\n\tscreenSpaceLayers?: 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\tpixiInit?: never;\n\tcontainer?: never;\n\tbackground?: never;\n\twidth?: never;\n\theight?: 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/** Container element to append the canvas to (or CSS selector string). Defaults to `document.body`.\n\t * The canvas also auto-resizes to this element unless `width`/`height` are set or `pixiInit.resizeTo` is set explicitly. */\n\tcontainer?: HTMLElement | string;\n\t/** Canvas background color. */\n\tbackground?: ApplicationOptions['background'];\n\t/** Fixed canvas width. When set (with `height`), the canvas is fixed-size and the auto-resize default is suppressed. */\n\twidth?: ApplicationOptions['width'];\n\t/** Fixed canvas height. When set (with `width`), the canvas is fixed-size and the auto-resize default is suppressed. */\n\theight?: ApplicationOptions['height'];\n\t/** Escape hatch for raw PixiJS ApplicationOptions not otherwise exposed at the top level.\n\t * Top-level fields (`background`, `width`, `height`) take precedence when both are set. */\n\tpixiInit?: Partial<ApplicationOptions>;\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**: Omit `app` and the plugin creates the Application during `ecs.initialize()`.\n * The canvas is appended to `container` (defaults to `document.body`) and auto-resizes to\n * match it. Pass `pixiInit: { width, height }` for a fixed-size canvas instead.\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 * background: '#1099bb',\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\tmode,\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/**\n * Convert a DOM pointer event's client coordinates to design-resolution (logical) coordinates.\n * Handles canvas offset, CSS-pixel to physical-pixel scaling, and viewport letterbox/crop offsets.\n * Suitable for wiring into the input plugin's `coordinateTransform` option.\n */\nexport function clientToLogical(\n\tclientX: number,\n\tclientY: number,\n\tcanvas: HTMLCanvasElement,\n\tviewport: ViewportScale,\n): { x: number; y: number } {\n\tconst rect = canvas.getBoundingClientRect();\n\tconst physicalX = (clientX - rect.left) * (viewport.physicalWidth / rect.width);\n\tconst physicalY = (clientY - rect.top) * (viewport.physicalHeight / rect.height);\n\treturn physicalToLogical(physicalX, physicalY, viewport);\n}\n\n/**\n * Re-apply the current viewport scale using the latest `mode` from the `viewportScale` resource.\n * Call after mutating `viewportScale.mode` to take effect immediately without waiting for a window resize.\n */\nexport function reapplyViewportScale(pixiApp: Application): void {\n\tpixiApp.renderer.emit('resize', pixiApp.screen.width, pixiApp.screen.height, pixiApp.renderer.resolution);\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 * background: '#1099bb',\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\tscreenSpaceLayers = [],\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\tconst screenSpaceLayerSet = new Set(screenSpaceLayers);\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// Parent container for screen-space layers (set during init when camera + screenSpaceLayers)\n\tlet screenSpaceParent: Container | null = null;\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\tconst parent = (screenSpaceParent && screenSpaceLayerSet.has(layerName))\n\t\t\t? screenSpaceParent\n\t\t\t: rootCont;\n\t\tparent.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: pre-initialized if an Application instance was provided, otherwise managed\n\tconst isManaged = !('app' in options && options.app !== undefined);\n\n\treturn definePlugin('renderer2d')\n\t\t.withComponentTypes<Renderer2DComponentTypes>()\n\t\t.withEventTypes<Renderer2DEventTypes>()\n\t\t.withResourceTypes<PluginResourceTypes>()\n\t\t.withLabels<Renderer2DLabels>()\n\t\t.withGroups<G>()\n\t\t.withReactiveQueryNames<Renderer2DReactiveQueryNames>()\n\t\t.install((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 managedOptions = options as Renderer2DPluginManagedOptions<G>;\n\t\t\t\tconst { pixiInit, background, width, height } = managedOptions;\n\t\t\t\tconst containerOption = managedOptions.container ?? document.body;\n\n\t\t\t\tconst containerEl: HTMLElement | null = typeof containerOption === 'string'\n\t\t\t\t\t? document.querySelector<HTMLElement>(containerOption)\n\t\t\t\t\t: containerOption;\n\n\t\t\t\t// Top-level background/width/height override pixiInit equivalents.\n\t\t\t\tconst mergedPixiInit: Partial<ApplicationOptions> = {\n\t\t\t\t\t...pixiInit,\n\t\t\t\t\t...(background !== undefined && { background }),\n\t\t\t\t\t...(width !== undefined && { width }),\n\t\t\t\t\t...(height !== undefined && { height }),\n\t\t\t\t};\n\n\t\t\t\t// Default resizeTo to the resolved container unless the caller opted into a\n\t\t\t\t// fixed-size canvas via width/height, or set pixiInit.resizeTo directly.\n\t\t\t\tconst shouldDefaultResizeTo = containerEl !== null\n\t\t\t\t\t&& mergedPixiInit.resizeTo === undefined\n\t\t\t\t\t&& mergedPixiInit.width === undefined\n\t\t\t\t\t&& mergedPixiInit.height === undefined;\n\n\t\t\t\tconst finalInitOptions: Partial<ApplicationOptions> = {\n\t\t\t\t\t...mergedPixiInit,\n\t\t\t\t\t...(shouldDefaultResizeTo && { resizeTo: containerEl }),\n\t\t\t\t};\n\n\t\t\t\tworld.addResource('pixiApp', async () => {\n\t\t\t\t\tconst app = await createPixiApplication(finalInitOptions);\n\n\t\t\t\t\tif (containerEl) {\n\t\t\t\t\t\tcontainerEl.appendChild(app.canvas);\n\t\t\t\t\t} else if (typeof containerOption === 'string') {\n\t\t\t\t\t\tconsole.warn(`Renderer2D plugin: container selector \"${containerOption}\" not found`);\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\t// When camera + screenSpaceLayers are active, ensure rootContainer is\n\t\t\t\t\t// not the stage itself so camera transforms don't affect screen-space layers.\n\t\t\t\t\tif (camera && screenSpaceLayerSet.size > 0) {\n\t\t\t\t\t\tif (rootCont === pixiApp.stage) {\n\t\t\t\t\t\t\tconst worldContainer = new ContainerClass();\n\t\t\t\t\t\t\tworldContainer.label = 'rootContainer';\n\t\t\t\t\t\t\tpixiApp.stage.addChild(worldContainer);\n\t\t\t\t\t\t\tecs.updateResource('rootContainer', () => worldContainer);\n\t\t\t\t\t\t\trootCont = worldContainer;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Screen-space layers are siblings of rootContainer\n\t\t\t\t\t\tscreenSpaceParent = rootCont.parent ?? pixiApp.stage;\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const layerName of renderLayers) {\n\t\t\t\t\t\tgetOrCreateLayerContainer(layerName, rootCont);\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 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\tconst vs = computeViewportScale(width, height, designWidth, designHeight, vpResource.mode);\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}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "91C76A4DF6430B7F64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|