ecspresso 0.16.1 → 0.16.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 +4 -4
- package/dist/plugins/ai/detection.js +2 -2
- package/dist/plugins/ai/detection.js.map +3 -3
- package/dist/plugins/ai/flocking.js +2 -2
- package/dist/plugins/ai/flocking.js.map +7 -6
- package/dist/plugins/physics/collision.js +2 -2
- package/dist/plugins/physics/collision.js.map +6 -5
- package/dist/plugins/physics/collision3D.js +2 -2
- package/dist/plugins/physics/collision3D.js.map +8 -7
- package/dist/plugins/physics/physics2D.js +2 -2
- package/dist/plugins/physics/physics2D.js.map +6 -5
- package/dist/plugins/physics/physics3D.js +2 -2
- package/dist/plugins/physics/physics3D.js.map +6 -5
- package/dist/plugins/scripting/timers.d.ts +36 -11
- package/dist/plugins/scripting/timers.js +2 -2
- package/dist/plugins/scripting/timers.js.map +3 -3
- package/dist/plugins/spatial/spatial-index.js +2 -2
- package/dist/plugins/spatial/spatial-index.js.map +4 -4
- package/dist/plugins/spatial/spatial-index3D.js +2 -2
- package/dist/plugins/spatial/spatial-index3D.js.map +4 -4
- package/dist/query-cache.d.ts +7 -7
- package/dist/utils/layer-bit-registry.d.ts +17 -0
- package/dist/utils/narrowphase.d.ts +31 -6
- package/dist/utils/narrowphase3D.d.ts +10 -0
- package/dist/utils/spatial-hash.d.ts +63 -16
- package/dist/utils/spatial-hash3D.d.ts +63 -16
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var R=((k)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(k,{get:(E,H)=>(typeof require<"u"?require:E)[H]}):k)(function(k){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+k+'" is not supported')});import{definePlugin as q}from"ecspresso";function F(k,E,H=32){return{detector:{range:k,layerFilter:E,maxResults:H}}}function g(k,E){return(k.getComponent(E,"detectedEntities")?.entities.length??0)>0}function C(k,E){return k.distanceSq-E.distanceSq}function x(k){let{systemGroup:E="ai",priority:H=500,phase:P="update"}=k??{},V=new Map,K=new Set,W=[],Z=new WeakMap;return q("detection").withComponentTypes().withEventTypes().withLabels().withGroups().requires().install((_)=>{_.registerDispose("detector",({entityId:X})=>{V.delete(X)}),_.addSystem("detection-scan").setPriority(H).inPhase(P).inGroup(E).addQuery("detectors",{with:["detector","worldTransform"]}).setProcess(({queries:X,ecs:z})=>{let b=z.getResource("spatialIndex");for(let A of X.detectors){let{detector:J,worldTransform:O}=A.components;W.length=0,b.queryRadiusInto(O.x,O.y,J.range,W);let L=[],Q=Z.get(J.layerFilter);if(!Q)Q=new Set(J.layerFilter),Z.set(J.layerFilter,Q);for(let j of W){if(j===A.id)continue;if(!z.getEntity(j))continue;let N=z.getComponent(j,"collisionLayer");if(!N)continue;if(!Q.has(N.layer))continue;let Y=z.getComponent(j,"worldTransform");if(!Y)continue;let D=Y.x-O.x,G=Y.y-O.y;L.push({entityId:j,distanceSq:D*D+G*G})}L.sort(C);let U=L.length>J.maxResults?L.slice(0,J.maxResults):L,$=z.getComponent(A.id,"detectedEntities");if($)$.entities=U,z.markChanged(A.id,"detectedEntities");else z.addComponent(A.id,"detectedEntities",{entities:U});let M=V.get(A.id);K.clear();for(let j of U)K.add(j.entityId);if(M){for(let j of K)if(!M.has(j))z.eventBus.publish("detectionGained",{entityId:A.id,detectedId:j});for(let j of M)if(!K.has(j))z.eventBus.publish("detectionLost",{entityId:A.id,lostId:j});M.clear();for(let j of K)M.add(j)}else{let j=new Set;for(let N of U)j.add(N.entityId),z.eventBus.publish("detectionGained",{entityId:A.id,detectedId:N.entityId});V.set(A.id,j)}}})})}export{g as hasDetectedTargets,F as createDetector,x as createDetectionPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=9C433B1DED88EF8264756E2164756E21
|
|
4
4
|
//# sourceMappingURL=detection.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/ai/detection.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Detection Plugin for ECSpresso\n *\n * Provides automatic proximity detection for entities. Entities with a\n * `detector` component get their `detectedEntities` populated each frame\n * with nearby entities that match the configured collision layer filter,\n * sorted by distance ascending (nearest first).\n *\n * Uses the spatial-index plugin for efficient range queries.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { TransformWorldConfig } from '../spatial/transform';\nimport type { SpatialIndexResourceTypes } from '../spatial/spatial-index';\nimport type { CollisionComponentTypes } from '../physics/collision';\n\n// ==================== Component Types ====================\n\n/**\n * Configures proximity detection for an entity.\n */\nexport interface Detector {\n\t/** Detection radius in world units */\n\trange: number;\n\t/** Only detect entities on these collision layers */\n\tlayerFilter: readonly string[];\n\t/** Maximum number of results to track (default: 32) */\n\tmaxResults: number;\n}\n\n/**\n * A detected entity with its squared distance from the detector.\n */\nexport interface DetectedEntry {\n\tentityId: number;\n\tdistanceSq: number;\n}\n\n/**\n * Auto-populated list of detected entities, sorted by distance ascending.\n */\nexport interface DetectedEntities {\n\tentities: readonly DetectedEntry[];\n}\n\n/**\n * Component types provided by the detection plugin.\n */\nexport interface DetectionComponentTypes {\n\tdetector: Detector;\n\tdetectedEntities: DetectedEntities;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when a new entity enters detection range.\n */\nexport interface DetectionGainedEvent {\n\t/** The entity doing the detecting */\n\tentityId: number;\n\t/** The entity that was detected */\n\tdetectedId: number;\n}\n\n/**\n * Event fired when an entity leaves detection range.\n */\nexport interface DetectionLostEvent {\n\t/** The entity doing the detecting */\n\tentityId: number;\n\t/** The entity that was lost */\n\tlostId: number;\n}\n\n/**\n * Event types provided by the detection plugin.\n */\nexport interface DetectionEventTypes {\n\tdetectionGained: DetectionGainedEvent;\n\tdetectionLost: DetectionLostEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the detection plugin's provided types.\n */\nexport type DetectionWorldConfig = WorldConfigFrom<DetectionComponentTypes, DetectionEventTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface DetectionPluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a detector component.\n *\n * @param range Detection radius in world units\n * @param layerFilter Only detect entities on these collision layers\n * @param maxResults Maximum results to track (default: 32)\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createDetector(300, ['enemy']),\n * ...createLocalTransform(400, 400),\n * });\n * ```\n */\nexport function createDetector(\n\trange: number,\n\tlayerFilter: readonly string[],\n\tmaxResults = 32,\n): Pick<DetectionComponentTypes, 'detector'> {\n\treturn { detector: { range, layerFilter, maxResults } };\n}\n\n/**\n * Check whether an entity has any detected targets.\n *\n * @param ecs ECS world instance\n * @param entityId Entity with a detector component\n * @returns true if detectedEntities contains at least one entry\n *\n * @example\n * ```typescript\n * if (hasDetectedTargets(ecs, guardId)) {\n * // transition to chase\n * }\n * ```\n */\nexport function hasDetectedTargets(\n\tecs: { getComponent(entityId: number, name: 'detectedEntities'): DetectedEntities | undefined },\n\tentityId: number,\n): boolean {\n\tconst detected = ecs.getComponent(entityId, 'detectedEntities');\n\treturn (detected?.entities.length ?? 0) > 0;\n}\n\n// ==================== Plugin Factory ====================\n\nfunction compareByDistance(a: DetectedEntry, b: DetectedEntry): number {\n\treturn a.distanceSq - b.distanceSq;\n}\n\n/**\n * Create a detection plugin for ECSpresso.\n *\n * Populates `detectedEntities` each frame with nearby entities matching\n * the detector's layer filter, sorted by distance (nearest first).\n * Publishes `detectionGained`/`detectionLost` events on transitions.\n *\n * Requires the spatial-index and transform plugins to be installed.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createCollisionPlugin({ layers }))\n * .withPlugin(createSpatialIndexPlugin())\n * .withPlugin(createDetectionPlugin())\n * .build();\n *\n * // Read nearest detected entity:\n * const detected = ecs.getComponent(turretId, 'detectedEntities');\n * const nearest = detected?.entities[0];\n * ```\n */\nexport function createDetectionPlugin<G extends string = 'ai'>(\n\toptions?: DetectionPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'ai',\n\t\tpriority = 500,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\t// Per-detector tracking of previous frame's detected set for event diffing\n\tconst previousSets = new Map<number, Set<number>>();\n\tconst currentSet = new Set<number>();\n\t// Reusable set for spatial index queries (avoids allocation per frame)\n\tconst candidateSet = new Set<number>();\n\t// Cache: layerFilter array → Set for O(1) lookups\n\tconst layerFilterCache = new WeakMap<readonly string[], Set<string>>();\n\n\treturn definePlugin('detection')\n\t\t.withComponentTypes<DetectionComponentTypes>()\n\t\t.withEventTypes<DetectionEventTypes>()\n\t\t.withLabels<'detection-scan'>()\n\t\t.withGroups<G>()\n\t\t.requires<\n\t\t\tTransformWorldConfig &\n\t\t\tWorldConfigFrom<Pick<CollisionComponentTypes<string>, 'collisionLayer'>> &\n\t\t\tWorldConfigFrom<{}, {}, SpatialIndexResourceTypes>\n\t\t>()\n\t\t.install((world) => {\n\t\t\tworld.registerDispose('detector', ({ entityId }) => {\n\t\t\t\tpreviousSets.delete(entityId);\n\t\t\t});\n\n\t\t\tworld\n\t\t\t\t.addSystem('detection-scan')\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('detectors', {\n\t\t\t\t\twith: ['detector', 'worldTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst spatialIndex = ecs.getResource('spatialIndex');\n\n\t\t\t\t\tfor (const entity of queries.detectors) {\n\t\t\t\t\t\tconst { detector, worldTransform } = entity.components;\n\n\t\t\t\t\t\tcandidateSet.clear();\n\t\t\t\t\t\tspatialIndex.queryRadiusInto(worldTransform.x, worldTransform.y, detector.range, candidateSet);\n\n\t\t\t\t\t\t// Build sorted results, filtering by layer and excluding self\n\t\t\t\t\t\tconst entries: DetectedEntry[] = [];\n\n\t\t\t\t\t\tlet filterSet = layerFilterCache.get(detector.layerFilter);\n\t\t\t\t\t\tif (!filterSet) {\n\t\t\t\t\t\t\tfilterSet = new Set(detector.layerFilter);\n\t\t\t\t\t\t\tlayerFilterCache.set(detector.layerFilter, filterSet);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor (const candidateId of candidateSet) {\n\t\t\t\t\t\t\tif (candidateId === entity.id) continue;\n\t\t\t\t\t\t\tif (!ecs.getEntity(candidateId)) continue;\n\n\t\t\t\t\t\t\tconst layer = ecs.getComponent(candidateId, 'collisionLayer');\n\t\t\t\t\t\t\tif (!layer) continue;\n\t\t\t\t\t\t\tif (!filterSet.has(layer.layer)) continue;\n\n\t\t\t\t\t\t\tconst candidateTransform = ecs.getComponent(candidateId, 'worldTransform');\n\t\t\t\t\t\t\tif (!candidateTransform) continue;\n\n\t\t\t\t\t\t\tconst dx = candidateTransform.x - worldTransform.x;\n\t\t\t\t\t\t\tconst dy = candidateTransform.y - worldTransform.y;\n\t\t\t\t\t\t\tentries.push({ entityId: candidateId, distanceSq: dx * dx + dy * dy });\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tentries.sort(compareByDistance);\n\t\t\t\t\t\tconst capped = entries.length > detector.maxResults\n\t\t\t\t\t\t\t? entries.slice(0, detector.maxResults)\n\t\t\t\t\t\t\t: entries;\n\n\t\t\t\t\t\t// Update or add the detectedEntities component\n\t\t\t\t\t\tconst existing = ecs.getComponent(entity.id, 'detectedEntities');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\t(existing as { entities: readonly DetectedEntry[] }).entities = capped;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'detectedEntities');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(entity.id, 'detectedEntities', { entities: capped });\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Diff against previous frame for events\n\t\t\t\t\t\tconst prev = previousSets.get(entity.id);\n\t\t\t\t\t\tcurrentSet.clear();\n\t\t\t\t\t\tfor (const entry of capped) {\n\t\t\t\t\t\t\tcurrentSet.add(entry.entityId);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (prev) {\n\t\t\t\t\t\t\t// Detect gained\n\t\t\t\t\t\t\tfor (const id of currentSet) {\n\t\t\t\t\t\t\t\tif (!prev.has(id)) {\n\t\t\t\t\t\t\t\t\tecs.eventBus.publish('detectionGained', {\n\t\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\t\tdetectedId: id,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Detect lost\n\t\t\t\t\t\t\tfor (const id of prev) {\n\t\t\t\t\t\t\t\tif (!currentSet.has(id)) {\n\t\t\t\t\t\t\t\t\tecs.eventBus.publish('detectionLost', {\n\t\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\t\tlostId: id,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Update previous set in place\n\t\t\t\t\t\t\tprev.clear();\n\t\t\t\t\t\t\tfor (const id of currentSet) {\n\t\t\t\t\t\t\t\tprev.add(id);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// First frame — all are gained\n\t\t\t\t\t\t\tconst newSet = new Set<number>();\n\t\t\t\t\t\t\tfor (const entry of capped) {\n\t\t\t\t\t\t\t\tnewSet.add(entry.entityId);\n\t\t\t\t\t\t\t\tecs.eventBus.publish('detectionGained', {\n\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\tdetectedId: entry.entityId,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tpreviousSets.set(entity.id, newSet);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
|
|
5
|
+
"/**\n * Detection Plugin for ECSpresso\n *\n * Provides automatic proximity detection for entities. Entities with a\n * `detector` component get their `detectedEntities` populated each frame\n * with nearby entities that match the configured collision layer filter,\n * sorted by distance ascending (nearest first).\n *\n * Uses the spatial-index plugin for efficient range queries.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { TransformWorldConfig } from '../spatial/transform';\nimport type { SpatialIndexResourceTypes } from '../spatial/spatial-index';\nimport type { CollisionComponentTypes } from '../physics/collision';\n\n// ==================== Component Types ====================\n\n/**\n * Configures proximity detection for an entity.\n */\nexport interface Detector {\n\t/** Detection radius in world units */\n\trange: number;\n\t/** Only detect entities on these collision layers */\n\tlayerFilter: readonly string[];\n\t/** Maximum number of results to track (default: 32) */\n\tmaxResults: number;\n}\n\n/**\n * A detected entity with its squared distance from the detector.\n */\nexport interface DetectedEntry {\n\tentityId: number;\n\tdistanceSq: number;\n}\n\n/**\n * Auto-populated list of detected entities, sorted by distance ascending.\n */\nexport interface DetectedEntities {\n\tentities: readonly DetectedEntry[];\n}\n\n/**\n * Component types provided by the detection plugin.\n */\nexport interface DetectionComponentTypes {\n\tdetector: Detector;\n\tdetectedEntities: DetectedEntities;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when a new entity enters detection range.\n */\nexport interface DetectionGainedEvent {\n\t/** The entity doing the detecting */\n\tentityId: number;\n\t/** The entity that was detected */\n\tdetectedId: number;\n}\n\n/**\n * Event fired when an entity leaves detection range.\n */\nexport interface DetectionLostEvent {\n\t/** The entity doing the detecting */\n\tentityId: number;\n\t/** The entity that was lost */\n\tlostId: number;\n}\n\n/**\n * Event types provided by the detection plugin.\n */\nexport interface DetectionEventTypes {\n\tdetectionGained: DetectionGainedEvent;\n\tdetectionLost: DetectionLostEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the detection plugin's provided types.\n */\nexport type DetectionWorldConfig = WorldConfigFrom<DetectionComponentTypes, DetectionEventTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface DetectionPluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a detector component.\n *\n * @param range Detection radius in world units\n * @param layerFilter Only detect entities on these collision layers\n * @param maxResults Maximum results to track (default: 32)\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createDetector(300, ['enemy']),\n * ...createLocalTransform(400, 400),\n * });\n * ```\n */\nexport function createDetector(\n\trange: number,\n\tlayerFilter: readonly string[],\n\tmaxResults = 32,\n): Pick<DetectionComponentTypes, 'detector'> {\n\treturn { detector: { range, layerFilter, maxResults } };\n}\n\n/**\n * Check whether an entity has any detected targets.\n *\n * @param ecs ECS world instance\n * @param entityId Entity with a detector component\n * @returns true if detectedEntities contains at least one entry\n *\n * @example\n * ```typescript\n * if (hasDetectedTargets(ecs, guardId)) {\n * // transition to chase\n * }\n * ```\n */\nexport function hasDetectedTargets(\n\tecs: { getComponent(entityId: number, name: 'detectedEntities'): DetectedEntities | undefined },\n\tentityId: number,\n): boolean {\n\tconst detected = ecs.getComponent(entityId, 'detectedEntities');\n\treturn (detected?.entities.length ?? 0) > 0;\n}\n\n// ==================== Plugin Factory ====================\n\nfunction compareByDistance(a: DetectedEntry, b: DetectedEntry): number {\n\treturn a.distanceSq - b.distanceSq;\n}\n\n/**\n * Create a detection plugin for ECSpresso.\n *\n * Populates `detectedEntities` each frame with nearby entities matching\n * the detector's layer filter, sorted by distance (nearest first).\n * Publishes `detectionGained`/`detectionLost` events on transitions.\n *\n * Requires the spatial-index and transform plugins to be installed.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createCollisionPlugin({ layers }))\n * .withPlugin(createSpatialIndexPlugin())\n * .withPlugin(createDetectionPlugin())\n * .build();\n *\n * // Read nearest detected entity:\n * const detected = ecs.getComponent(turretId, 'detectedEntities');\n * const nearest = detected?.entities[0];\n * ```\n */\nexport function createDetectionPlugin<G extends string = 'ai'>(\n\toptions?: DetectionPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'ai',\n\t\tpriority = 500,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\t// Per-detector tracking of previous frame's detected set for event diffing\n\tconst previousSets = new Map<number, Set<number>>();\n\tconst currentSet = new Set<number>();\n\tconst candidateBuf: number[] = [];\n\t// Cache: layerFilter array → Set for O(1) lookups\n\tconst layerFilterCache = new WeakMap<readonly string[], Set<string>>();\n\n\treturn definePlugin('detection')\n\t\t.withComponentTypes<DetectionComponentTypes>()\n\t\t.withEventTypes<DetectionEventTypes>()\n\t\t.withLabels<'detection-scan'>()\n\t\t.withGroups<G>()\n\t\t.requires<\n\t\t\tTransformWorldConfig &\n\t\t\tWorldConfigFrom<Pick<CollisionComponentTypes<string>, 'collisionLayer'>> &\n\t\t\tWorldConfigFrom<{}, {}, SpatialIndexResourceTypes>\n\t\t>()\n\t\t.install((world) => {\n\t\t\tworld.registerDispose('detector', ({ entityId }) => {\n\t\t\t\tpreviousSets.delete(entityId);\n\t\t\t});\n\n\t\t\tworld\n\t\t\t\t.addSystem('detection-scan')\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('detectors', {\n\t\t\t\t\twith: ['detector', 'worldTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst spatialIndex = ecs.getResource('spatialIndex');\n\n\t\t\t\t\tfor (const entity of queries.detectors) {\n\t\t\t\t\t\tconst { detector, worldTransform } = entity.components;\n\n\t\t\t\t\t\tcandidateBuf.length = 0;\n\t\t\t\t\t\tspatialIndex.queryRadiusInto(worldTransform.x, worldTransform.y, detector.range, candidateBuf);\n\n\t\t\t\t\t\t// Build sorted results, filtering by layer and excluding self\n\t\t\t\t\t\tconst entries: DetectedEntry[] = [];\n\n\t\t\t\t\t\tlet filterSet = layerFilterCache.get(detector.layerFilter);\n\t\t\t\t\t\tif (!filterSet) {\n\t\t\t\t\t\t\tfilterSet = new Set(detector.layerFilter);\n\t\t\t\t\t\t\tlayerFilterCache.set(detector.layerFilter, filterSet);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor (const candidateId of candidateBuf) {\n\t\t\t\t\t\t\tif (candidateId === entity.id) continue;\n\t\t\t\t\t\t\tif (!ecs.getEntity(candidateId)) continue;\n\n\t\t\t\t\t\t\tconst layer = ecs.getComponent(candidateId, 'collisionLayer');\n\t\t\t\t\t\t\tif (!layer) continue;\n\t\t\t\t\t\t\tif (!filterSet.has(layer.layer)) continue;\n\n\t\t\t\t\t\t\tconst candidateTransform = ecs.getComponent(candidateId, 'worldTransform');\n\t\t\t\t\t\t\tif (!candidateTransform) continue;\n\n\t\t\t\t\t\t\tconst dx = candidateTransform.x - worldTransform.x;\n\t\t\t\t\t\t\tconst dy = candidateTransform.y - worldTransform.y;\n\t\t\t\t\t\t\tentries.push({ entityId: candidateId, distanceSq: dx * dx + dy * dy });\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tentries.sort(compareByDistance);\n\t\t\t\t\t\tconst capped = entries.length > detector.maxResults\n\t\t\t\t\t\t\t? entries.slice(0, detector.maxResults)\n\t\t\t\t\t\t\t: entries;\n\n\t\t\t\t\t\t// Update or add the detectedEntities component\n\t\t\t\t\t\tconst existing = ecs.getComponent(entity.id, 'detectedEntities');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\t(existing as { entities: readonly DetectedEntry[] }).entities = capped;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'detectedEntities');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(entity.id, 'detectedEntities', { entities: capped });\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Diff against previous frame for events\n\t\t\t\t\t\tconst prev = previousSets.get(entity.id);\n\t\t\t\t\t\tcurrentSet.clear();\n\t\t\t\t\t\tfor (const entry of capped) {\n\t\t\t\t\t\t\tcurrentSet.add(entry.entityId);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (prev) {\n\t\t\t\t\t\t\t// Detect gained\n\t\t\t\t\t\t\tfor (const id of currentSet) {\n\t\t\t\t\t\t\t\tif (!prev.has(id)) {\n\t\t\t\t\t\t\t\t\tecs.eventBus.publish('detectionGained', {\n\t\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\t\tdetectedId: id,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Detect lost\n\t\t\t\t\t\t\tfor (const id of prev) {\n\t\t\t\t\t\t\t\tif (!currentSet.has(id)) {\n\t\t\t\t\t\t\t\t\tecs.eventBus.publish('detectionLost', {\n\t\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\t\tlostId: id,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Update previous set in place\n\t\t\t\t\t\t\tprev.clear();\n\t\t\t\t\t\t\tfor (const id of currentSet) {\n\t\t\t\t\t\t\t\tprev.add(id);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// First frame — all are gained\n\t\t\t\t\t\t\tconst newSet = new Set<number>();\n\t\t\t\t\t\t\tfor (const entry of capped) {\n\t\t\t\t\t\t\t\tnewSet.add(entry.entityId);\n\t\t\t\t\t\t\t\tecs.eventBus.publish('detectionGained', {\n\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\tdetectedId: entry.entityId,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tpreviousSets.set(entity.id, newSet);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "2PAWA,uBAAS,kBAsGF,SAAS,CAAc,CAC7B,EACA,EACA,EAAa,GAC+B,CAC5C,MAAO,CAAE,SAAU,CAAE,QAAO,cAAa,YAAW,CAAE,EAiBhD,SAAS,CAAkB,CACjC,EACA,EACU,CAEV,OADiB,EAAI,aAAa,EAAU,kBAAkB,GAC5C,SAAS,QAAU,GAAK,EAK3C,SAAS,CAAiB,CAAC,EAAkB,EAA0B,CACtE,OAAO,EAAE,WAAa,EAAE,WA0BlB,SAAS,CAA8C,CAC7D,EACC,CACD,IACC,cAAc,KACd,WAAW,IACX,QAAQ,UACL,GAAW,CAAC,EAGV,EAAe,IAAI,IACnB,EAAa,IAAI,
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "9C433B1DED88EF8264756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var Dz=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(J,Q)=>(typeof require<"u"?require:J)[Q]}):z)(function(z){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+z+'" is not supported')});import{definePlugin as Vz}from"ecspresso";function n(z){let J=new Map,Q=new WeakMap,G=1;function j(K){let Z=J.get(K);if(Z!==void 0)return Z;if(G===0)throw Error(`[ecspresso] ${z} layer bitmask overflow: more than 32 distinct layers registered`);let N=G;return J.set(K,N),G<<=1,N}function O(K){let Z=Q.get(K);if(Z!==void 0)return Z;let N=0;for(let D=0;D<K.length;D++)N|=j(K[D]);return Q.set(K,N),N}return{getLayerBit:j,getCollidesWithMask:O}}var f={normalX:0,normalY:0,depth:0},I=0,m=1,i=n("Collision"),Zz=i.getLayerBit,$z=i.getCollidesWithMask;function e(z,J,Q,G,j,O,K,Z){if(z.entityId=J,z.layer=j,z.collidesWith=O,z.layerBit=Zz(j),z.collidesWithMask=$z(O),K)return z.x=Q+(K.offsetX??0),z.y=G+(K.offsetY??0),z.shape=I,z.halfWidth=K.width/2,z.halfHeight=K.height/2,z.radius=0,!0;if(Z)return z.x=Q+(Z.offsetX??0),z.y=G+(Z.offsetY??0),z.shape=m,z.halfWidth=0,z.halfHeight=0,z.radius=Z.radius,!0;return!1}function Kz(z,J,Q,G,j,O,K,Z,N){let D=j-z,$=O-J,U=Q+K-Math.abs(D),V=G+Z-Math.abs($);if(U<=0||V<=0)return!1;if(U<V)return N.normalX=D>=0?1:-1,N.normalY=0,N.depth=U,!0;return N.normalX=0,N.normalY=$>=0?1:-1,N.depth=V,!0}function jz(z,J,Q,G,j,O,K){let Z=G-z,N=j-J,D=Z*Z+N*N,$=Q+O;if(D>=$*$)return!1;let U=Math.sqrt(D);if(U===0)return K.normalX=1,K.normalY=0,K.depth=$,!0;return K.normalX=Z/U,K.normalY=N/U,K.depth=$-U,!0}function s(z,J,Q,G,j,O,K,Z){let N=Math.max(z-Q,Math.min(j,z+Q)),D=Math.max(J-G,Math.min(O,J+G)),$=j-N,U=O-D,V=$*$+U*U;if(V>=K*K)return!1;if(V===0){let k=j-(z-Q),H=z+Q-j,T=O-(J-G),q=J+G-O,P=Math.min(k,H,T,q);if(P===H)return Z.normalX=1,Z.normalY=0,Z.depth=H+K,!0;if(P===k)return Z.normalX=-1,Z.normalY=0,Z.depth=k+K,!0;if(P===q)return Z.normalX=0,Z.normalY=1,Z.depth=q+K,!0;return Z.normalX=0,Z.normalY=-1,Z.depth=T+K,!0}let L=Math.sqrt(V);return Z.normalX=$/L,Z.normalY=U/L,Z.depth=K-L,!0}function a(z,J,Q){if(z.shape===I&&J.shape===I)return Kz(z.x,z.y,z.halfWidth,z.halfHeight,J.x,J.y,J.halfWidth,J.halfHeight,Q);if(z.shape===m&&J.shape===m)return jz(z.x,z.y,z.radius,J.x,J.y,J.radius,Q);if(z.shape===I&&J.shape===m)return s(z.x,z.y,z.halfWidth,z.halfHeight,J.x,J.y,J.radius,Q);if(!s(J.x,J.y,J.halfWidth,J.halfHeight,z.x,z.y,z.radius,Q))return!1;return Q.normalX=-Q.normalX,Q.normalY=-Q.normalY,!0}var b=[];function o(){return{arr:[],gen:[],current:0}}var c=!1,Uz=50;function t(z,J,Q,G,j,O){if(G)Gz(z,J,Q,G,j,O);else Oz(z,J,j,O)}function Oz(z,J,Q,G){if(!c&&J>=Uz)c=!0,console.warn(`[ecspresso] Collision detection is using O(n²) brute force with ${J} colliders. For better performance, install createSpatialIndexPlugin() alongside your collision or physics2D plugin.`);for(let j=0;j<J;j++){let O=z[j];if(!O)continue;for(let K=j+1;K<J;K++){let Z=z[K];if(!Z)continue;if((O.collidesWithMask&Z.layerBit|Z.collidesWithMask&O.layerBit)===0)continue;if(!a(O,Z,f))continue;Q(O,Z,f,G)}}}function Gz(z,J,Q,G,j,O){let{arr:K,gen:Z}=Q,N=++Q.current;for(let D=0;D<J;D++){let $=z[D];if(!$)continue;let U=$.entityId;K[U]=$,Z[U]=N}for(let D=0;D<J;D++){let $=z[D];if(!$)continue;let U=$.shape===I?$.halfWidth:$.radius,V=$.shape===I?$.halfHeight:$.radius;b.length=0,G.queryRectInto($.x-U,$.y-V,$.x+U,$.y+V,b,$.entityId);for(let L of b){if(Z[L]!==N)continue;let k=K[L];if(!k)continue;if(($.collidesWithMask&k.layerBit|k.collidesWithMask&$.layerBit)===0)continue;if(!a($,k,f))continue;j($,k,f,O)}}}function Fz(z,J){return{rigidBody:{type:z,mass:z==="static"?1/0:J?.mass??1,drag:J?.drag??0,restitution:J?.restitution??0,friction:J?.friction??0,gravityScale:J?.gravityScale??1},force:{x:0,y:0}}}function Mz(z,J){return{force:{x:z,y:J}}}function zz(z,J,Q,G){let j=z.getComponent(J,"force");if(!j)return;j.x+=Q,j.y+=G}function Pz(z,J,Q,G){let j=z.getComponent(J,"velocity"),O=z.getComponent(J,"rigidBody");if(!j||!O)return;if(O.mass===1/0||O.mass===0)return;j.x+=Q/O.mass,j.y+=G/O.mass}function Ez(z,J,Q,G){let j=z.getComponent(J,"velocity");if(!j)return;j.x=Q,j.y=G}var B={entityA:0,entityB:0,normalX:0,normalY:0,depth:0};function Nz(z,J,Q,G){let j=z.rigidBody.type==="dynamic"&&z.rigidBody.mass>0&&z.rigidBody.mass!==1/0?1/z.rigidBody.mass:0,O=J.rigidBody.type==="dynamic"&&J.rigidBody.mass>0&&J.rigidBody.mass!==1/0?1/J.rigidBody.mass:0,K=j+O;if(K>0){let Z=Q.depth/K;if(j>0){let U=G.getComponent(z.entityId,"localTransform");if(!U)return;let V=Z*j;U.x-=V*Q.normalX,U.y-=V*Q.normalY,z.x=U.x,z.y=U.y,G.markChanged(z.entityId,"localTransform")}if(O>0){let U=G.getComponent(J.entityId,"localTransform");if(!U)return;let V=Z*O;U.x+=V*Q.normalX,U.y+=V*Q.normalY,J.x=U.x,J.y=U.y,G.markChanged(J.entityId,"localTransform")}let N=J.velocity.x-z.velocity.x,D=J.velocity.y-z.velocity.y,$=N*Q.normalX+D*Q.normalY;if($<0){let V=-(1+Math.min(z.rigidBody.restitution,J.rigidBody.restitution))*$/K;z.velocity.x-=V*j*Q.normalX,z.velocity.y-=V*j*Q.normalY,J.velocity.x+=V*O*Q.normalX,J.velocity.y+=V*O*Q.normalY;let L=N-$*Q.normalX,k=D-$*Q.normalY,H=Math.sqrt(L*L+k*k);if(H>0.000001){let T=L/H,q=k/H,X=Math.sqrt(z.rigidBody.friction*J.rigidBody.friction)*Math.abs(V),W=Math.min(H/K,X);z.velocity.x+=W*j*T,z.velocity.y+=W*j*q,J.velocity.x-=W*O*T,J.velocity.y-=W*O*q}}G.markChanged(z.entityId,"velocity"),G.markChanged(J.entityId,"velocity")}B.entityA=z.entityId,B.entityB=J.entityId,B.normalX=Q.normalX,B.normalY=Q.normalY,B.depth=Q.depth,G.eventBus.publish("physicsCollision",B)}function Xz(z){let{gravity:J={x:0,y:0},systemGroup:Q="physics2D",collisionSystemGroup:G,integrationPriority:j=1000,collisionPriority:O=900,phase:K="fixedUpdate"}=z??{};return Vz("physics2D").withComponentTypes().withEventTypes().withResourceTypes().withLabels().withGroups().requires().install((Z)=>{Z.registerRequired("rigidBody","velocity",()=>({x:0,y:0})),Z.registerRequired("rigidBody","force",()=>({x:0,y:0})),Z.addResource("physicsConfig",{gravity:{x:J.x,y:J.y}}),Z.addSystem("physics2D-integration").setPriority(j).inPhase(K).inGroup(Q).addQuery("bodies",{with:["localTransform","velocity","rigidBody","force"]}).setProcess(({queries:L,dt:k,ecs:H})=>{let{gravity:T}=H.getResource("physicsConfig"),q=T.x,P=T.y;for(let X of L.bodies){let{localTransform:W,velocity:R,rigidBody:M,force:F}=X.components;if(M.type==="static")continue;if(M.type==="dynamic"){let S=M.gravityScale*k;R.x+=q*S,R.y+=P*S;let C=M.mass;if(C>0&&C!==1/0){let Y=k/C;R.x+=F.x*Y,R.y+=F.y*Y}if(M.drag>0){let Y=Math.max(0,1-M.drag*k);R.x*=Y,R.y*=Y}}W.x+=R.x*k,W.y+=R.y*k,F.x=0,F.y=0,H.markChanged(X.id,"localTransform")}});let N=Z.addSystem("physics2D-collision").setPriority(O).inPhase(K).inGroup(Q);if(G)N.inGroup(G);let D=[],$=o(),U,V=!1;N.addQuery("collidables",{with:["localTransform","rigidBody","velocity","collisionLayer"]}).setProcess(({queries:L,ecs:k})=>{let H=0;for(let T of L.collidables){let{localTransform:q,rigidBody:P,velocity:X,collisionLayer:W}=T.components,R=k.getComponent(T.id,"aabbCollider"),M=R?void 0:k.getComponent(T.id,"circleCollider");if(!R&&!M)continue;let F=D[H];if(!F)F={entityId:T.id,x:q.x,y:q.y,layer:W.layer,collidesWith:W.collidesWith,layerBit:0,collidesWithMask:0,shape:I,halfWidth:0,halfHeight:0,radius:0,rigidBody:P,velocity:X},D[H]=F;else F.rigidBody=P,F.velocity=X;if(!e(F,T.id,q.x,q.y,W.layer,W.collidesWith,R,M))continue;H++}if(!V)U=k.tryGetResource("spatialIndex"),V=!0;t(D,H,$,U,Nz,k)})})}import{definePlugin as kz}from"ecspresso";function Iz(z){return{flockingAgent:{perceptionRadius:z?.perceptionRadius??100,separationWeight:z?.separationWeight??1.5,alignmentWeight:z?.alignmentWeight??1,cohesionWeight:z?.cohesionWeight??1,maxForce:z?.maxForce??400,maxSpeed:z?.maxSpeed??200,flockGroup:z?.flockGroup??0}}}var u=[],Jz=0.01;function Sz(z){let{systemGroup:J="ai",priority:Q=500,phase:G="update",headingPriority:j=200}=z??{};return kz("flocking").withComponentTypes().withLabels().withGroups().requires().install((O)=>{O.addSystem("flocking-forces").setPriority(Q).inPhase(G).inGroup(J).addQuery("boids",{with:["flockingAgent","worldTransform","velocity","force"]}).setProcess(({queries:K,ecs:Z})=>{let N=Z.getResource("spatialIndex");for(let D of K.boids){let{flockingAgent:$,worldTransform:U,velocity:V}=D.components,{perceptionRadius:L,separationWeight:k,alignmentWeight:H,cohesionWeight:T,maxForce:q,flockGroup:P}=$;u.length=0,N.queryRadiusInto(U.x,U.y,L,u);let X=0,W=0,R=0,M=0,F=0,S=0,C=0,Y=0,v=0,d=L*0.5,Qz=d*d;for(let E of u){if(E===D.id)continue;let A=Z.getComponent(E,"flockingAgent");if(!A)continue;if(A.flockGroup!==P)continue;let x=Z.getComponent(E,"worldTransform");if(!x)continue;let g=U.x-x.x,p=U.y-x.y,h=g*g+p*p;if(h>0&&h<Qz){let l=Math.sqrt(h);X+=g/l,W+=p/l,R++}let y=Z.getComponent(E,"velocity");if(y)M+=y.x,F+=y.y,S++;C+=x.x,Y+=x.y,v++}let _=0,w=0;if(R>0)_+=X/R*k,w+=W/R*k;if(S>0){let E=M/S,A=F/S;_+=(E-V.x)*H,w+=(A-V.y)*H}if(v>0){let E=C/v,A=Y/v;_+=(E-U.x-V.x)*T,w+=(A-U.y-V.y)*T}let r=_*_+w*w;if(r>q*q){let E=Math.sqrt(r);_=_/E*q,w=w/E*q}zz(Z,D.id,_,w)}}),O.addSystem("flocking-heading").setPriority(j).inPhase(G).inGroup(J).addQuery("boids",{with:["flockingAgent","velocity","localTransform"]}).setProcess(({queries:K,ecs:Z})=>{for(let N of K.boids){let{flockingAgent:D,velocity:$,localTransform:U}=N.components,{maxSpeed:V}=D,L=$.x*$.x+$.y*$.y;if(L>V*V){let k=Math.sqrt(L);$.x=$.x/k*V,$.y=$.y/k*V,Z.markChanged(N.id,"velocity")}if(L>Jz*Jz){let k=Math.atan2($.y,$.x);if(k!==U.rotation)U.rotation=k,Z.markChanged(N.id,"localTransform")}}})})}export{Sz as createFlockingPlugin,Iz as createFlockingAgent};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=F4B4008D6DBB975064756E2164756E21
|
|
4
4
|
//# sourceMappingURL=flocking.js.map
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/plugins/physics/physics2D.ts", "../src/utils/narrowphase.ts", "../src/plugins/ai/flocking.ts"],
|
|
3
|
+
"sources": ["../src/plugins/physics/physics2D.ts", "../src/utils/layer-bit-registry.ts", "../src/utils/narrowphase.ts", "../src/plugins/ai/flocking.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 '../spatial/transform';\nimport type { CollisionComponentTypes, LayerFactories } from './collision';\nimport type { Vector2D } from 'ecspresso';\nimport { fillBaseColliderInfo, detectCollisions, AABB_SHAPE, type Contact, type BaseColliderInfo } from '../../utils/narrowphase';\nimport type { SpatialIndex } from '../../utils/spatial-hash';\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\n/**\n * Module-level reusable physics collision event. Subscribers must consume\n * synchronously — same contract as the shared narrowphase Contact.\n */\nconst _physicsCollisionEvent: Physics2DCollisionEvent = {\n\tentityA: 0, entityB: 0, normalX: 0, normalY: 0, depth: 0,\n};\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\tconst corrA = correctionScale * invMassA;\n\t\t\tltA.x -= corrA * contact.normalX;\n\t\t\tltA.y -= corrA * contact.normalY;\n\t\t\t// Sync cached position so subsequent pairs in this frame use corrected values\n\t\t\ta.x = ltA.x;\n\t\t\ta.y = ltA.y;\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\tconst corrB = correctionScale * invMassB;\n\t\t\tltB.x += corrB * contact.normalX;\n\t\t\tltB.y += corrB * contact.normalY;\n\t\t\tb.x = ltB.x;\n\t\t\tb.y = ltB.y;\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\t_physicsCollisionEvent.entityA = a.entityId;\n\t_physicsCollisionEvent.entityB = b.entityId;\n\t_physicsCollisionEvent.normalX = contact.normalX;\n\t_physicsCollisionEvent.normalY = contact.normalY;\n\t_physicsCollisionEvent.depth = contact.depth;\n\tecs.eventBus.publish('physicsCollision', _physicsCollisionEvent);\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 { gravity: g } = ecs.getResource('physicsConfig');\n\t\t\t\t\tconst gx = g.x;\n\t\t\t\t\tconst gy = g.y;\n\n\t\t\t\t\t// TODO(perf): no early-out for \"sleeping\" dynamic bodies — a packed\n\t\t\t\t\t// pile of resting entities still runs gravity/drag/force-clear/\n\t\t\t\t\t// markChanged every step. A sleep flag on RigidBody that latches\n\t\t\t\t\t// after N frames of near-zero velocity (and clears on impulse or\n\t\t\t\t\t// applied force) would let most of a stabilized scene skip the\n\t\t\t\t\t// full per-entity body of this loop. Keep in sync with physics3D.\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\tconst gsdt = rigidBody.gravityScale * dt;\n\t\t\t\t\t\t\tvelocity.x += gx * gsdt;\n\t\t\t\t\t\t\tvelocity.y += gy * gsdt;\n\n\t\t\t\t\t\t\t// 2. Forces (F = ma → a = F/m)\n\t\t\t\t\t\t\tconst mass = rigidBody.mass;\n\t\t\t\t\t\t\tif (mass > 0 && mass !== Infinity) {\n\t\t\t\t\t\t\t\tconst invMassDt = dt / mass;\n\t\t\t\t\t\t\t\tvelocity.x += force.x * invMassDt;\n\t\t\t\t\t\t\t\tvelocity.y += force.y * invMassDt;\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\t\t\t// Cached spatial index reference (resolved once on first frame).\n\t\t\tlet cachedSI: SpatialIndex | undefined;\n\t\t\tlet siResolved = false;\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\t// TODO(perf): collider shape is discovered via two ecs.getComponent\n\t\t\t\t\t// calls per entity per frame because the query can't express\n\t\t\t\t\t// \"aabbCollider OR circleCollider\". Splitting into two queries\n\t\t\t\t\t// (aabb-bearing, circle-bearing) would eliminate these lookups at\n\t\t\t\t\t// the cost of two pool-fill passes. Keep in sync with physics3D.\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 = aabb ? undefined : 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\tif (!siResolved) {\n\t\t\t\t\t\tcachedSI = ecs.tryGetResource<SpatialIndex>('spatialIndex');\n\t\t\t\t\t\tsiResolved = true;\n\t\t\t\t\t}\n\t\t\t\t\tdetectCollisions(colliderPool, count, broadphaseMap, cachedSI, resolvePhysicsContact, ecs);\n\t\t\t\t});\n\t\t});\n}\n",
|
|
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\t// TODO(perf): mirrors narrowphase3D — dense grids add every candidate\n\t\t// (including `a` itself and lower-ID entities) to the set before the\n\t\t// filter below discards ~half of them. Emitting only pairs with larger\n\t\t// IDs at query time would remove the post-hoc filter. Keep in sync with\n\t\t// whatever fix lands in narrowphase3D.\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
|
-
"/**\n * Flocking Plugin for ECSpresso\n *\n * Classic boid simulation — separation, alignment, cohesion. Produces\n * emergent group movement from simple per-entity steering forces.\n *\n * Composes with the physics2D plugin: flocking computes steering forces\n * and feeds them through `applyForce()`. Physics integration handles\n * velocity and position updates.\n *\n * Requires the spatial-index plugin for efficient neighbor queries.\n * Entities must have a `circleCollider` (or `aabbCollider`) to appear\n * in spatial index queries.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { TransformWorldConfig } from '../spatial/transform';\nimport type { Physics2DOwnComponentTypes } from '../physics/physics2D';\nimport { applyForce } from '../physics/physics2D';\nimport type { SpatialIndexResourceTypes } from '../spatial/spatial-index';\n\n// ==================== Component Types ====================\n\n/**\n * Configures flocking behavior for a boid entity.\n *\n * Entities with this component must also have:\n * - `localTransform` + `worldTransform` (transform plugin)\n * - `velocity` + `force` + `rigidBody` (physics2D plugin)\n * - `circleCollider` with radius >= perceptionRadius (for spatial index queries)\n */\nexport interface FlockingAgent {\n\t/** Radius within which neighbors are detected */\n\tperceptionRadius: number;\n\t/** Separation weight — steer away from nearby neighbors (default: 1.5) */\n\tseparationWeight: number;\n\t/** Alignment weight — match average heading of neighbors (default: 1.0) */\n\talignmentWeight: number;\n\t/** Cohesion weight — steer toward average position of neighbors (default: 1.0) */\n\tcohesionWeight: number;\n\t/** Maximum steering force magnitude per frame */\n\tmaxForce: number;\n\t/** Maximum velocity magnitude (hard speed cap) */\n\tmaxSpeed: number;\n\t/** Flock group ID for independent flocks (default: 0) */\n\tflockGroup: number;\n}\n\n/**\n * Component types provided by the flocking plugin.\n */\nexport interface FlockingComponentTypes {\n\tflockingAgent: FlockingAgent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the flocking plugin's provided types.\n */\nexport type FlockingWorldConfig = WorldConfigFrom<FlockingComponentTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface FlockingPluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {\n\t/** Priority for the heading/speed-clamp system (default: 200) */\n\theadingPriority?: number;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a flockingAgent component with sensible defaults.\n *\n * Entities must also have a `circleCollider` with radius >= perceptionRadius\n * for the spatial index to find them as neighbors.\n *\n * @param options Partial overrides for flocking agent fields\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createFlockingAgent({ perceptionRadius: 80, maxSpeed: 150 }),\n * ...createRigidBody('dynamic', { mass: 1, drag: 1, gravityScale: 0 }),\n * ...createCircleCollider(80),\n * ...createGraphicsComponents(boidGraphics, { x: 100, y: 200 }),\n * });\n * ```\n */\nexport function createFlockingAgent(\n\toptions?: Partial<FlockingAgent>,\n): Pick<FlockingComponentTypes, 'flockingAgent'> {\n\treturn {\n\t\tflockingAgent: {\n\t\t\tperceptionRadius: options?.perceptionRadius ?? 100,\n\t\t\tseparationWeight: options?.separationWeight ?? 1.5,\n\t\t\talignmentWeight: options?.alignmentWeight ?? 1.0,\n\t\t\tcohesionWeight: options?.cohesionWeight ?? 1.0,\n\t\t\tmaxForce: options?.maxForce ?? 400,\n\t\t\tmaxSpeed: options?.maxSpeed ?? 200,\n\t\t\tflockGroup: options?.flockGroup ?? 0,\n\t\t},\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n// Module-scoped reusable set to reduce GC pressure in neighbor queries\nconst _neighborSet = new Set<number>();\n\nconst SPEED_EPSILON = 0.01;\n\n/**\n * Create a flocking plugin for ECSpresso.\n *\n * Installs two systems:\n * - `flocking-forces` — computes separation/alignment/cohesion and applies via applyForce()\n * - `flocking-heading` — clamps speed to maxSpeed and orients rotation to match velocity\n *\n * Requires the transform, physics2D, and spatial-index plugins to be installed.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ background: '#0a0a2e' }))\n * .withPlugin(createPhysics2DPlugin())\n * .withPlugin(createSpatialIndexPlugin())\n * .withPlugin(createFlockingPlugin())\n * .build();\n * ```\n */\nexport function createFlockingPlugin<G extends string = 'ai'>(\n\toptions?: FlockingPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'ai',\n\t\tpriority = 500,\n\t\tphase = 'update',\n\t\theadingPriority = 200,\n\t} = options ?? {};\n\n\treturn definePlugin('flocking')\n\t\t.withComponentTypes<FlockingComponentTypes>()\n\t\t.withLabels<'flocking-forces' | 'flocking-heading'>()\n\t\t.withGroups<G>()\n\t\t.requires<\n\t\t\tTransformWorldConfig &\n\t\t\tWorldConfigFrom<Pick<Physics2DOwnComponentTypes, 'velocity' | 'force'>> &\n\t\t\tWorldConfigFrom<{}, {}, SpatialIndexResourceTypes>\n\t\t>()\n\t\t.install((world) => {\n\t\t\t// --- System 1: Compute and apply flocking forces ---\n\t\t\tworld\n\t\t\t\t.addSystem('flocking-forces')\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('boids', {\n\t\t\t\t\twith: ['flockingAgent', 'worldTransform', 'velocity', 'force'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst spatialIndex = ecs.getResource('spatialIndex');\n\n\t\t\t\t\tfor (const entity of queries.boids) {\n\t\t\t\t\t\tconst { flockingAgent, worldTransform, velocity } = entity.components;\n\t\t\t\t\t\tconst { perceptionRadius, separationWeight, alignmentWeight, cohesionWeight, maxForce, flockGroup } = flockingAgent;\n\n\t\t\t\t\t\t// Query neighbors via spatial index\n\t\t\t\t\t\t_neighborSet.clear();\n\t\t\t\t\t\tspatialIndex.queryRadiusInto(worldTransform.x, worldTransform.y, perceptionRadius, _neighborSet);\n\n\t\t\t\t\t\t// Accumulate steering forces — all inline scalars, no allocations\n\t\t\t\t\t\tlet sepX = 0, sepY = 0, sepCount = 0;\n\t\t\t\t\t\tlet alignX = 0, alignY = 0, alignCount = 0;\n\t\t\t\t\t\tlet cohX = 0, cohY = 0, cohCount = 0;\n\n\t\t\t\t\t\tconst separationRadius = perceptionRadius * 0.5;\n\t\t\t\t\t\tconst separationRadiusSq = separationRadius * separationRadius;\n\n\t\t\t\t\t\tfor (const neighborId of _neighborSet) {\n\t\t\t\t\t\t\tif (neighborId === entity.id) continue;\n\n\t\t\t\t\t\t\tconst neighborAgent = ecs.getComponent(neighborId, 'flockingAgent');\n\t\t\t\t\t\t\tif (!neighborAgent) continue;\n\t\t\t\t\t\t\tif (neighborAgent.flockGroup !== flockGroup) continue;\n\n\t\t\t\t\t\t\tconst neighborTransform = ecs.getComponent(neighborId, 'worldTransform');\n\t\t\t\t\t\t\tif (!neighborTransform) continue;\n\n\t\t\t\t\t\t\tconst dx = worldTransform.x - neighborTransform.x;\n\t\t\t\t\t\t\tconst dy = worldTransform.y - neighborTransform.y;\n\t\t\t\t\t\t\tconst distSq = dx * dx + dy * dy;\n\n\t\t\t\t\t\t\t// Separation — closer neighbors push harder\n\t\t\t\t\t\t\tif (distSq > 0 && distSq < separationRadiusSq) {\n\t\t\t\t\t\t\t\tconst dist = Math.sqrt(distSq);\n\t\t\t\t\t\t\t\tsepX += dx / dist;\n\t\t\t\t\t\t\t\tsepY += dy / dist;\n\t\t\t\t\t\t\t\tsepCount++;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Alignment — average velocity of neighbors\n\t\t\t\t\t\t\tconst neighborVel = ecs.getComponent(neighborId, 'velocity');\n\t\t\t\t\t\t\tif (neighborVel) {\n\t\t\t\t\t\t\t\talignX += neighborVel.x;\n\t\t\t\t\t\t\t\talignY += neighborVel.y;\n\t\t\t\t\t\t\t\talignCount++;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Cohesion — average position of neighbors\n\t\t\t\t\t\t\tcohX += neighborTransform.x;\n\t\t\t\t\t\t\tcohY += neighborTransform.y;\n\t\t\t\t\t\t\tcohCount++;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tlet totalFx = 0, totalFy = 0;\n\n\t\t\t\t\t\t// Separation: steer away from crowded neighbors\n\t\t\t\t\t\tif (sepCount > 0) {\n\t\t\t\t\t\t\ttotalFx += (sepX / sepCount) * separationWeight;\n\t\t\t\t\t\t\ttotalFy += (sepY / sepCount) * separationWeight;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Alignment: steer toward average heading\n\t\t\t\t\t\tif (alignCount > 0) {\n\t\t\t\t\t\t\tconst avgVx = alignX / alignCount;\n\t\t\t\t\t\t\tconst avgVy = alignY / alignCount;\n\t\t\t\t\t\t\t// Desired = average velocity - current velocity\n\t\t\t\t\t\t\ttotalFx += (avgVx - velocity.x) * alignmentWeight;\n\t\t\t\t\t\t\ttotalFy += (avgVy - velocity.y) * alignmentWeight;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Cohesion: steer toward average position\n\t\t\t\t\t\tif (cohCount > 0) {\n\t\t\t\t\t\t\tconst avgPx = cohX / cohCount;\n\t\t\t\t\t\t\tconst avgPy = cohY / cohCount;\n\t\t\t\t\t\t\t// Desired = direction to center of mass - current velocity\n\t\t\t\t\t\t\ttotalFx += (avgPx - worldTransform.x - velocity.x) * cohesionWeight;\n\t\t\t\t\t\t\ttotalFy += (avgPy - worldTransform.y - velocity.y) * cohesionWeight;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Clamp total steering force to maxForce\n\t\t\t\t\t\tconst forceMagSq = totalFx * totalFx + totalFy * totalFy;\n\t\t\t\t\t\tif (forceMagSq > maxForce * maxForce) {\n\t\t\t\t\t\t\tconst forceMag = Math.sqrt(forceMagSq);\n\t\t\t\t\t\t\ttotalFx = (totalFx / forceMag) * maxForce;\n\t\t\t\t\t\t\ttotalFy = (totalFy / forceMag) * maxForce;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tapplyForce(ecs, entity.id, totalFx, totalFy);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// --- System 2: Clamp speed and orient heading from velocity ---\n\t\t\tworld\n\t\t\t\t.addSystem('flocking-heading')\n\t\t\t\t.setPriority(headingPriority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('boids', {\n\t\t\t\t\twith: ['flockingAgent', 'velocity', 'localTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.boids) {\n\t\t\t\t\t\tconst { flockingAgent, velocity, localTransform } = entity.components;\n\t\t\t\t\t\tconst { maxSpeed } = flockingAgent;\n\n\t\t\t\t\t\t// Clamp velocity to maxSpeed\n\t\t\t\t\t\tconst speedSq = velocity.x * velocity.x + velocity.y * velocity.y;\n\t\t\t\t\t\tif (speedSq > maxSpeed * maxSpeed) {\n\t\t\t\t\t\t\tconst speed = Math.sqrt(speedSq);\n\t\t\t\t\t\t\tvelocity.x = (velocity.x / speed) * maxSpeed;\n\t\t\t\t\t\t\tvelocity.y = (velocity.y / speed) * maxSpeed;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'velocity');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Orient rotation to match velocity heading\n\t\t\t\t\t\tif (speedSq > SPEED_EPSILON * SPEED_EPSILON) {\n\t\t\t\t\t\t\tconst heading = Math.atan2(velocity.y, velocity.x);\n\t\t\t\t\t\t\tif (heading !== localTransform.rotation) {\n\t\t\t\t\t\t\t\tlocalTransform.rotation = heading;\n\t\t\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\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"
|
|
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, createBroadphaseScratch, AABB_SHAPE, type Contact, type BaseColliderInfo } from '../../utils/narrowphase';\nimport type { SpatialIndex } from '../../utils/spatial-hash';\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\n/**\n * Module-level reusable physics collision event. Subscribers must consume\n * synchronously — same contract as the shared narrowphase Contact.\n */\nconst _physicsCollisionEvent: Physics2DCollisionEvent = {\n\tentityA: 0, entityB: 0, normalX: 0, normalY: 0, depth: 0,\n};\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\tconst corrA = correctionScale * invMassA;\n\t\t\tltA.x -= corrA * contact.normalX;\n\t\t\tltA.y -= corrA * contact.normalY;\n\t\t\t// Sync cached position so subsequent pairs in this frame use corrected values\n\t\t\ta.x = ltA.x;\n\t\t\ta.y = ltA.y;\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\tconst corrB = correctionScale * invMassB;\n\t\t\tltB.x += corrB * contact.normalX;\n\t\t\tltB.y += corrB * contact.normalY;\n\t\t\tb.x = ltB.x;\n\t\t\tb.y = ltB.y;\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\t_physicsCollisionEvent.entityA = a.entityId;\n\t_physicsCollisionEvent.entityB = b.entityId;\n\t_physicsCollisionEvent.normalX = contact.normalX;\n\t_physicsCollisionEvent.normalY = contact.normalY;\n\t_physicsCollisionEvent.depth = contact.depth;\n\tecs.eventBus.publish('physicsCollision', _physicsCollisionEvent);\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 { gravity: g } = ecs.getResource('physicsConfig');\n\t\t\t\t\tconst gx = g.x;\n\t\t\t\t\tconst gy = g.y;\n\n\t\t\t\t\t// TODO(perf): no early-out for \"sleeping\" dynamic bodies — a packed\n\t\t\t\t\t// pile of resting entities still runs gravity/drag/force-clear/\n\t\t\t\t\t// markChanged every step. A sleep flag on RigidBody that latches\n\t\t\t\t\t// after N frames of near-zero velocity (and clears on impulse or\n\t\t\t\t\t// applied force) would let most of a stabilized scene skip the\n\t\t\t\t\t// full per-entity body of this loop. Keep in sync with physics3D.\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\tconst gsdt = rigidBody.gravityScale * dt;\n\t\t\t\t\t\t\tvelocity.x += gx * gsdt;\n\t\t\t\t\t\t\tvelocity.y += gy * gsdt;\n\n\t\t\t\t\t\t\t// 2. Forces (F = ma → a = F/m)\n\t\t\t\t\t\t\tconst mass = rigidBody.mass;\n\t\t\t\t\t\t\tif (mass > 0 && mass !== Infinity) {\n\t\t\t\t\t\t\t\tconst invMassDt = dt / mass;\n\t\t\t\t\t\t\t\tvelocity.x += force.x * invMassDt;\n\t\t\t\t\t\t\t\tvelocity.y += force.y * invMassDt;\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\tconst broadphaseScratch = createBroadphaseScratch<Physics2DColliderInfo<L>>();\n\t\t\t// Cached spatial index reference (resolved once on first frame).\n\t\t\tlet cachedSI: SpatialIndex | undefined;\n\t\t\tlet siResolved = false;\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\t// TODO(perf): collider shape is discovered via two ecs.getComponent\n\t\t\t\t\t// calls per entity per frame because the query can't express\n\t\t\t\t\t// \"aabbCollider OR circleCollider\". Splitting into two queries\n\t\t\t\t\t// (aabb-bearing, circle-bearing) would eliminate these lookups at\n\t\t\t\t\t// the cost of two pool-fill passes. Keep in sync with physics3D.\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 = aabb ? undefined : 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\tlayerBit: 0,\n\t\t\t\t\t\t\t\tcollidesWithMask: 0,\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\tif (!siResolved) {\n\t\t\t\t\t\tcachedSI = ecs.tryGetResource<SpatialIndex>('spatialIndex');\n\t\t\t\t\t\tsiResolved = true;\n\t\t\t\t\t}\n\t\t\t\t\tdetectCollisions(colliderPool, count, broadphaseScratch, cachedSI, resolvePhysicsContact, ecs);\n\t\t\t\t});\n\t\t});\n}\n",
|
|
6
|
+
"/**\n * Lazy monotonic registry mapping layer name → unique bit. Lets pair\n * filtering use a single `(a.collidesWithMask & b.layerBit)` check\n * instead of `Array.includes` on every collision pair.\n *\n * One registry per dimension (2D and 3D) — user-defined layer namespaces\n * are independent, so bits should not be shared across systems.\n *\n * Maximum 32 layers per registry (one per bit in a 32-bit signed int).\n * Crossing the limit throws on the next `getLayerBit` call.\n */\nexport interface LayerBitRegistry {\n\tgetLayerBit(layer: string): number;\n\t/** OR of `getLayerBit` for every entry. Cached by array reference. */\n\tgetCollidesWithMask(collidesWith: readonly string[]): number;\n}\n\nexport function createLayerBitRegistry(label: string): LayerBitRegistry {\n\tconst layerBits = new Map<string, number>();\n\tconst maskCache = new WeakMap<readonly string[], number>();\n\tlet nextBit = 1;\n\n\tfunction getLayerBit(layer: string): number {\n\t\tconst existing = layerBits.get(layer);\n\t\tif (existing !== undefined) return existing;\n\t\tif (nextBit === 0) {\n\t\t\tthrow new Error(\n\t\t\t\t`[ecspresso] ${label} layer bitmask overflow: more than 32 distinct layers registered`,\n\t\t\t);\n\t\t}\n\t\tconst bit = nextBit;\n\t\tlayerBits.set(layer, bit);\n\t\t// `<<= 1` rolls 1<<31 to 0, which is detected on the next call.\n\t\tnextBit <<= 1;\n\t\treturn bit;\n\t}\n\n\tfunction getCollidesWithMask(collidesWith: readonly string[]): number {\n\t\tconst cached = maskCache.get(collidesWith);\n\t\tif (cached !== undefined) return cached;\n\t\tlet mask = 0;\n\t\tfor (let i = 0; i < collidesWith.length; i++) {\n\t\t\tmask |= getLayerBit(collidesWith[i]!);\n\t\t}\n\t\tmaskCache.set(collidesWith, mask);\n\t\treturn mask;\n\t}\n\n\treturn { getLayerBit, getCollidesWithMask };\n}\n",
|
|
7
|
+
"/**\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';\nimport { createLayerBitRegistry } from './layer-bit-registry';\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\t/**\n\t * Bit assigned to `layer` from the lazy layer registry. Populated by\n\t * `fillBaseColliderInfo`. Used together with `collidesWithMask` to\n\t * replace per-pair `Array.includes` layer checks with a single AND.\n\t */\n\tlayerBit: number;\n\t/** OR of `getLayerBit` for every entry in `collidesWith`. */\n\tcollidesWithMask: number;\n\tshape: ColliderShape;\n\thalfWidth: number;\n\thalfHeight: number;\n\tradius: number;\n}\n\n// ==================== Layer Bit Registry ====================\n\nconst _layerRegistry = createLayerBitRegistry('Collision');\nexport const getLayerBit = _layerRegistry.getLayerBit;\nexport const getCollidesWithMask = _layerRegistry.getCollidesWithMask;\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\tinfo.layerBit = getLayerBit(layer);\n\tinfo.collidesWithMask = getCollidesWithMask(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\nconst _broadphaseCandidates: number[] = [];\n\n/**\n * Per-caller scratch for the broadphase entityId → collider lookup.\n *\n * Dense `arr` indexed by entityId, paired with a `gen` stamp array that marks\n * which slots are live this call. Bumping `current` invalidates all prior\n * entries without clearing — replaces the per-frame `Map.clear()` + N\n * `Map.set()` allocation churn that a `Map<number, I>` would incur.\n *\n * Owned per plugin instance (alongside its `colliderPool`), so concurrent\n * worlds don't share state and `I` stays fully typed without erasure.\n */\nexport interface BroadphaseScratch<I extends BaseColliderInfo> {\n\tarr: (I | undefined)[];\n\tgen: number[];\n\tcurrent: number;\n}\n\nexport function createBroadphaseScratch<I extends BaseColliderInfo>(): BroadphaseScratch<I> {\n\treturn { arr: [], gen: [], current: 0 };\n}\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 * `scratch` is a caller-owned `BroadphaseScratch<I>` used by the broadphase\n * path as an entityId → collider lookup. Allocate it once per plugin instance\n * and pass the same reference every call.\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\tscratch: BroadphaseScratch<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, scratch, 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.collidesWithMask & b.layerBit) | (b.collidesWithMask & a.layerBit)) === 0) 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\tscratch: BroadphaseScratch<I>,\n\tspatialIndex: SpatialIndex,\n\tonContact: (a: I, b: I, contact: Contact, context: C) => void,\n\tcontext: C,\n): void {\n\tconst arr = scratch.arr;\n\tconst stamps = scratch.gen;\n\tconst gen = ++scratch.current;\n\tfor (let i = 0; i < count; i++) {\n\t\tconst c = colliders[i];\n\t\tif (!c) continue;\n\t\tconst id = c.entityId;\n\t\tarr[id] = c;\n\t\tstamps[id] = gen;\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.length = 0;\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\ta.entityId,\n\t\t);\n\n\t\tfor (const bId of _broadphaseCandidates) {\n\t\t\tif (stamps[bId] !== gen) continue;\n\t\t\tconst b = arr[bId];\n\t\t\tif (!b) continue;\n\n\t\t\tif (((a.collidesWithMask & b.layerBit) | (b.collidesWithMask & a.layerBit)) === 0) 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",
|
|
8
|
+
"/**\n * Flocking Plugin for ECSpresso\n *\n * Classic boid simulation — separation, alignment, cohesion. Produces\n * emergent group movement from simple per-entity steering forces.\n *\n * Composes with the physics2D plugin: flocking computes steering forces\n * and feeds them through `applyForce()`. Physics integration handles\n * velocity and position updates.\n *\n * Requires the spatial-index plugin for efficient neighbor queries.\n * Entities must have a `circleCollider` (or `aabbCollider`) to appear\n * in spatial index queries.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { TransformWorldConfig } from '../spatial/transform';\nimport type { Physics2DOwnComponentTypes } from '../physics/physics2D';\nimport { applyForce } from '../physics/physics2D';\nimport type { SpatialIndexResourceTypes } from '../spatial/spatial-index';\n\n// ==================== Component Types ====================\n\n/**\n * Configures flocking behavior for a boid entity.\n *\n * Entities with this component must also have:\n * - `localTransform` + `worldTransform` (transform plugin)\n * - `velocity` + `force` + `rigidBody` (physics2D plugin)\n * - `circleCollider` with radius >= perceptionRadius (for spatial index queries)\n */\nexport interface FlockingAgent {\n\t/** Radius within which neighbors are detected */\n\tperceptionRadius: number;\n\t/** Separation weight — steer away from nearby neighbors (default: 1.5) */\n\tseparationWeight: number;\n\t/** Alignment weight — match average heading of neighbors (default: 1.0) */\n\talignmentWeight: number;\n\t/** Cohesion weight — steer toward average position of neighbors (default: 1.0) */\n\tcohesionWeight: number;\n\t/** Maximum steering force magnitude per frame */\n\tmaxForce: number;\n\t/** Maximum velocity magnitude (hard speed cap) */\n\tmaxSpeed: number;\n\t/** Flock group ID for independent flocks (default: 0) */\n\tflockGroup: number;\n}\n\n/**\n * Component types provided by the flocking plugin.\n */\nexport interface FlockingComponentTypes {\n\tflockingAgent: FlockingAgent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the flocking plugin's provided types.\n */\nexport type FlockingWorldConfig = WorldConfigFrom<FlockingComponentTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface FlockingPluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {\n\t/** Priority for the heading/speed-clamp system (default: 200) */\n\theadingPriority?: number;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a flockingAgent component with sensible defaults.\n *\n * Entities must also have a `circleCollider` with radius >= perceptionRadius\n * for the spatial index to find them as neighbors.\n *\n * @param options Partial overrides for flocking agent fields\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createFlockingAgent({ perceptionRadius: 80, maxSpeed: 150 }),\n * ...createRigidBody('dynamic', { mass: 1, drag: 1, gravityScale: 0 }),\n * ...createCircleCollider(80),\n * ...createGraphicsComponents(boidGraphics, { x: 100, y: 200 }),\n * });\n * ```\n */\nexport function createFlockingAgent(\n\toptions?: Partial<FlockingAgent>,\n): Pick<FlockingComponentTypes, 'flockingAgent'> {\n\treturn {\n\t\tflockingAgent: {\n\t\t\tperceptionRadius: options?.perceptionRadius ?? 100,\n\t\t\tseparationWeight: options?.separationWeight ?? 1.5,\n\t\t\talignmentWeight: options?.alignmentWeight ?? 1.0,\n\t\t\tcohesionWeight: options?.cohesionWeight ?? 1.0,\n\t\t\tmaxForce: options?.maxForce ?? 400,\n\t\t\tmaxSpeed: options?.maxSpeed ?? 200,\n\t\t\tflockGroup: options?.flockGroup ?? 0,\n\t\t},\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\nconst _neighborBuf: number[] = [];\n\nconst SPEED_EPSILON = 0.01;\n\n/**\n * Create a flocking plugin for ECSpresso.\n *\n * Installs two systems:\n * - `flocking-forces` — computes separation/alignment/cohesion and applies via applyForce()\n * - `flocking-heading` — clamps speed to maxSpeed and orients rotation to match velocity\n *\n * Requires the transform, physics2D, and spatial-index plugins to be installed.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ background: '#0a0a2e' }))\n * .withPlugin(createPhysics2DPlugin())\n * .withPlugin(createSpatialIndexPlugin())\n * .withPlugin(createFlockingPlugin())\n * .build();\n * ```\n */\nexport function createFlockingPlugin<G extends string = 'ai'>(\n\toptions?: FlockingPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'ai',\n\t\tpriority = 500,\n\t\tphase = 'update',\n\t\theadingPriority = 200,\n\t} = options ?? {};\n\n\treturn definePlugin('flocking')\n\t\t.withComponentTypes<FlockingComponentTypes>()\n\t\t.withLabels<'flocking-forces' | 'flocking-heading'>()\n\t\t.withGroups<G>()\n\t\t.requires<\n\t\t\tTransformWorldConfig &\n\t\t\tWorldConfigFrom<Pick<Physics2DOwnComponentTypes, 'velocity' | 'force'>> &\n\t\t\tWorldConfigFrom<{}, {}, SpatialIndexResourceTypes>\n\t\t>()\n\t\t.install((world) => {\n\t\t\t// --- System 1: Compute and apply flocking forces ---\n\t\t\tworld\n\t\t\t\t.addSystem('flocking-forces')\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('boids', {\n\t\t\t\t\twith: ['flockingAgent', 'worldTransform', 'velocity', 'force'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst spatialIndex = ecs.getResource('spatialIndex');\n\n\t\t\t\t\tfor (const entity of queries.boids) {\n\t\t\t\t\t\tconst { flockingAgent, worldTransform, velocity } = entity.components;\n\t\t\t\t\t\tconst { perceptionRadius, separationWeight, alignmentWeight, cohesionWeight, maxForce, flockGroup } = flockingAgent;\n\n\t\t\t\t\t\t// Query neighbors via spatial index\n\t\t\t\t\t\t_neighborBuf.length = 0;\n\t\t\t\t\t\tspatialIndex.queryRadiusInto(worldTransform.x, worldTransform.y, perceptionRadius, _neighborBuf);\n\n\t\t\t\t\t\t// Accumulate steering forces — all inline scalars, no allocations\n\t\t\t\t\t\tlet sepX = 0, sepY = 0, sepCount = 0;\n\t\t\t\t\t\tlet alignX = 0, alignY = 0, alignCount = 0;\n\t\t\t\t\t\tlet cohX = 0, cohY = 0, cohCount = 0;\n\n\t\t\t\t\t\tconst separationRadius = perceptionRadius * 0.5;\n\t\t\t\t\t\tconst separationRadiusSq = separationRadius * separationRadius;\n\n\t\t\t\t\t\tfor (const neighborId of _neighborBuf) {\n\t\t\t\t\t\t\tif (neighborId === entity.id) continue;\n\n\t\t\t\t\t\t\tconst neighborAgent = ecs.getComponent(neighborId, 'flockingAgent');\n\t\t\t\t\t\t\tif (!neighborAgent) continue;\n\t\t\t\t\t\t\tif (neighborAgent.flockGroup !== flockGroup) continue;\n\n\t\t\t\t\t\t\tconst neighborTransform = ecs.getComponent(neighborId, 'worldTransform');\n\t\t\t\t\t\t\tif (!neighborTransform) continue;\n\n\t\t\t\t\t\t\tconst dx = worldTransform.x - neighborTransform.x;\n\t\t\t\t\t\t\tconst dy = worldTransform.y - neighborTransform.y;\n\t\t\t\t\t\t\tconst distSq = dx * dx + dy * dy;\n\n\t\t\t\t\t\t\t// Separation — closer neighbors push harder\n\t\t\t\t\t\t\tif (distSq > 0 && distSq < separationRadiusSq) {\n\t\t\t\t\t\t\t\tconst dist = Math.sqrt(distSq);\n\t\t\t\t\t\t\t\tsepX += dx / dist;\n\t\t\t\t\t\t\t\tsepY += dy / dist;\n\t\t\t\t\t\t\t\tsepCount++;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Alignment — average velocity of neighbors\n\t\t\t\t\t\t\tconst neighborVel = ecs.getComponent(neighborId, 'velocity');\n\t\t\t\t\t\t\tif (neighborVel) {\n\t\t\t\t\t\t\t\talignX += neighborVel.x;\n\t\t\t\t\t\t\t\talignY += neighborVel.y;\n\t\t\t\t\t\t\t\talignCount++;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Cohesion — average position of neighbors\n\t\t\t\t\t\t\tcohX += neighborTransform.x;\n\t\t\t\t\t\t\tcohY += neighborTransform.y;\n\t\t\t\t\t\t\tcohCount++;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tlet totalFx = 0, totalFy = 0;\n\n\t\t\t\t\t\t// Separation: steer away from crowded neighbors\n\t\t\t\t\t\tif (sepCount > 0) {\n\t\t\t\t\t\t\ttotalFx += (sepX / sepCount) * separationWeight;\n\t\t\t\t\t\t\ttotalFy += (sepY / sepCount) * separationWeight;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Alignment: steer toward average heading\n\t\t\t\t\t\tif (alignCount > 0) {\n\t\t\t\t\t\t\tconst avgVx = alignX / alignCount;\n\t\t\t\t\t\t\tconst avgVy = alignY / alignCount;\n\t\t\t\t\t\t\t// Desired = average velocity - current velocity\n\t\t\t\t\t\t\ttotalFx += (avgVx - velocity.x) * alignmentWeight;\n\t\t\t\t\t\t\ttotalFy += (avgVy - velocity.y) * alignmentWeight;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Cohesion: steer toward average position\n\t\t\t\t\t\tif (cohCount > 0) {\n\t\t\t\t\t\t\tconst avgPx = cohX / cohCount;\n\t\t\t\t\t\t\tconst avgPy = cohY / cohCount;\n\t\t\t\t\t\t\t// Desired = direction to center of mass - current velocity\n\t\t\t\t\t\t\ttotalFx += (avgPx - worldTransform.x - velocity.x) * cohesionWeight;\n\t\t\t\t\t\t\ttotalFy += (avgPy - worldTransform.y - velocity.y) * cohesionWeight;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Clamp total steering force to maxForce\n\t\t\t\t\t\tconst forceMagSq = totalFx * totalFx + totalFy * totalFy;\n\t\t\t\t\t\tif (forceMagSq > maxForce * maxForce) {\n\t\t\t\t\t\t\tconst forceMag = Math.sqrt(forceMagSq);\n\t\t\t\t\t\t\ttotalFx = (totalFx / forceMag) * maxForce;\n\t\t\t\t\t\t\ttotalFy = (totalFy / forceMag) * maxForce;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tapplyForce(ecs, entity.id, totalFx, totalFy);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// --- System 2: Clamp speed and orient heading from velocity ---\n\t\t\tworld\n\t\t\t\t.addSystem('flocking-heading')\n\t\t\t\t.setPriority(headingPriority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('boids', {\n\t\t\t\t\twith: ['flockingAgent', 'velocity', 'localTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.boids) {\n\t\t\t\t\t\tconst { flockingAgent, velocity, localTransform } = entity.components;\n\t\t\t\t\t\tconst { maxSpeed } = flockingAgent;\n\n\t\t\t\t\t\t// Clamp velocity to maxSpeed\n\t\t\t\t\t\tconst speedSq = velocity.x * velocity.x + velocity.y * velocity.y;\n\t\t\t\t\t\tif (speedSq > maxSpeed * maxSpeed) {\n\t\t\t\t\t\t\tconst speed = Math.sqrt(speedSq);\n\t\t\t\t\t\t\tvelocity.x = (velocity.x / speed) * maxSpeed;\n\t\t\t\t\t\t\tvelocity.y = (velocity.y / speed) * maxSpeed;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'velocity');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Orient rotation to match velocity heading\n\t\t\t\t\t\tif (speedSq > SPEED_EPSILON * SPEED_EPSILON) {\n\t\t\t\t\t\t\tconst heading = Math.atan2(velocity.y, velocity.x);\n\t\t\t\t\t\t\tif (heading !== localTransform.rotation) {\n\t\t\t\t\t\t\t\tlocalTransform.rotation = heading;\n\t\t\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\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"
|
|
8
9
|
],
|
|
9
|
-
"mappings": "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",
|
|
10
|
-
"debugId": "
|
|
10
|
+
"mappings": "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",
|
|
11
|
+
"debugId": "F4B4008D6DBB975064756E2164756E21",
|
|
11
12
|
"names": []
|
|
12
13
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var u=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(J,Q)=>(typeof require<"u"?require:J)[Q]}):z)(function(z){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+z+'" is not supported')});import{definePlugin as h}from"ecspresso";function E(z){let J=new Map,Q=new WeakMap,N=1;function V(Z){let $=J.get(Z);if($!==void 0)return $;if(N===0)throw Error(`[ecspresso] ${z} layer bitmask overflow: more than 32 distinct layers registered`);let j=N;return J.set(Z,j),N<<=1,j}function O(Z){let $=Q.get(Z);if($!==void 0)return $;let j=0;for(let G=0;G<Z.length;G++)j|=V(Z[G]);return Q.set(Z,j),j}return{getLayerBit:V,getCollidesWithMask:O}}var I={normalX:0,normalY:0,depth:0},K=0,P=1,Y=E("Collision"),C=Y.getLayerBit,v=Y.getCollidesWithMask;function k(z,J,Q,N,V,O,Z,$){if(z.entityId=J,z.layer=V,z.collidesWith=O,z.layerBit=C(V),z.collidesWithMask=v(O),Z)return z.x=Q+(Z.offsetX??0),z.y=N+(Z.offsetY??0),z.shape=K,z.halfWidth=Z.width/2,z.halfHeight=Z.height/2,z.radius=0,!0;if($)return z.x=Q+($.offsetX??0),z.y=N+($.offsetY??0),z.shape=P,z.halfWidth=0,z.halfHeight=0,z.radius=$.radius,!0;return!1}function B(z,J,Q,N,V,O,Z,$,j){let G=V-z,M=O-J,F=Q+Z-Math.abs(G),U=N+$-Math.abs(M);if(F<=0||U<=0)return!1;if(F<U)return j.normalX=G>=0?1:-1,j.normalY=0,j.depth=F,!0;return j.normalX=0,j.normalY=M>=0?1:-1,j.depth=U,!0}function x(z,J,Q,N,V,O,Z){let $=N-z,j=V-J,G=$*$+j*j,M=Q+O;if(G>=M*M)return!1;let F=Math.sqrt(G);if(F===0)return Z.normalX=1,Z.normalY=0,Z.depth=M,!0;return Z.normalX=$/F,Z.normalY=j/F,Z.depth=M-F,!0}function S(z,J,Q,N,V,O,Z,$){let j=Math.max(z-Q,Math.min(V,z+Q)),G=Math.max(J-N,Math.min(O,J+N)),M=V-j,F=O-G,U=M*M+F*F;if(U>=Z*Z)return!1;if(U===0){let q=V-(z-Q),R=z+Q-V,H=O-(J-N),D=J+N-O,W=Math.min(q,R,H,D);if(W===R)return $.normalX=1,$.normalY=0,$.depth=R+Z,!0;if(W===q)return $.normalX=-1,$.normalY=0,$.depth=q+Z,!0;if(W===D)return $.normalX=0,$.normalY=1,$.depth=D+Z,!0;return $.normalX=0,$.normalY=-1,$.depth=H+Z,!0}let T=Math.sqrt(U);return $.normalX=M/T,$.normalY=F/T,$.depth=Z-T,!0}function m(z,J,Q){if(z.shape===K&&J.shape===K)return B(z.x,z.y,z.halfWidth,z.halfHeight,J.x,J.y,J.halfWidth,J.halfHeight,Q);if(z.shape===P&&J.shape===P)return x(z.x,z.y,z.radius,J.x,J.y,J.radius,Q);if(z.shape===K&&J.shape===P)return S(z.x,z.y,z.halfWidth,z.halfHeight,J.x,J.y,J.radius,Q);if(!S(J.x,J.y,J.halfWidth,J.halfHeight,z.x,z.y,z.radius,Q))return!1;return Q.normalX=-Q.normalX,Q.normalY=-Q.normalY,!0}var g=[];function w(){return{arr:[],gen:[],current:0}}var X=!1,p=50;function _(z,J,Q,N,V,O){if(N)y(z,J,Q,N,V,O);else f(z,J,V,O)}function f(z,J,Q,N){if(!X&&J>=p)X=!0,console.warn(`[ecspresso] Collision detection is using O(n²) brute force with ${J} colliders. For better performance, install createSpatialIndexPlugin() alongside your collision or physics2D plugin.`);for(let V=0;V<J;V++){let O=z[V];if(!O)continue;for(let Z=V+1;Z<J;Z++){let $=z[Z];if(!$)continue;if((O.collidesWithMask&$.layerBit|$.collidesWithMask&O.layerBit)===0)continue;if(!m(O,$,I))continue;Q(O,$,I,N)}}}function y(z,J,Q,N,V,O){let{arr:Z,gen:$}=Q,j=++Q.current;for(let G=0;G<J;G++){let M=z[G];if(!M)continue;let F=M.entityId;Z[F]=M,$[F]=j}for(let G=0;G<J;G++){let M=z[G];if(!M)continue;let F=M.shape===K?M.halfWidth:M.radius,U=M.shape===K?M.halfHeight:M.radius;g.length=0,N.queryRectInto(M.x-F,M.y-U,M.x+F,M.y+U,g,M.entityId);for(let T of g){if($[T]!==j)continue;let q=Z[T];if(!q)continue;if((M.collidesWithMask&q.layerBit|q.collidesWithMask&M.layerBit)===0)continue;if(!m(M,q,I))continue;V(M,q,I,O)}}}function o(z,J,Q,N){let V={width:z,height:J};if(Q!==void 0)V.offsetX=Q;if(N!==void 0)V.offsetY=N;return{aabbCollider:V}}function a(z,J,Q){let N={radius:z};if(J!==void 0)N.offsetX=J;if(Q!==void 0)N.offsetY=Q;return{circleCollider:N}}function b(z,J){return{collisionLayer:{layer:z,collidesWith:J}}}function t(z){let J={};for(let Q of Object.keys(z)){let N=z[Q];J[Q]=()=>b(Q,N)}return J}function A(z){let J=z.indexOf(":");if(J===-1)throw Error(`Invalid collision pair key "${z}": must contain a colon separator (e.g. "player:enemy")`);let Q=z.slice(0,J),N=z.slice(J+1);if(Q===""||N==="")throw Error(`Invalid collision pair key "${z}": layer names must not be empty`);return[Q,N]}function e(z){let J=new Map,Q=new Set;for(let N of Object.keys(z))A(N),Q.add(N);for(let N of Object.keys(z)){let[V,O]=A(N),Z=z[N];if(!Z)continue;J.set(N,{callback:Z,swapped:!1});let $=`${O}:${V}`;if($!==N&&!Q.has($))J.set($,{callback:Z,swapped:!0})}return function({data:V,ecs:O}){let Z=J.get(V.layerA+":"+V.layerB);if(!Z)return;if(Z.swapped)Z.callback(V.entityB,V.entityA,O);else Z.callback(V.entityA,V.entityB,O)}}var L={entityA:0,entityB:0,layerA:"",layerB:"",normalX:0,normalY:0,depth:0};function d(z,J,Q,N){L.entityA=z.entityId,L.entityB=J.entityId,L.layerA=z.layer,L.layerB=J.layer,L.normalX=Q.normalX,L.normalY=Q.normalY,L.depth=Q.depth,N.publish("collision",L)}function zz(z){let{systemGroup:J="physics",priority:Q=0,phase:N="postUpdate"}=z;return h("collision").withComponentTypes().withEventTypes().withLabels().withGroups().requires().install((V)=>{let O=[],Z=w(),$,j=!1;V.addSystem("collision-detection").setPriority(Q).inPhase(N).inGroup(J).addQuery("collidables",{with:["worldTransform","collisionLayer"]}).setProcess(({queries:G,ecs:M})=>{let F=0;for(let U of G.collidables){let{worldTransform:T,collisionLayer:q}=U.components,R=M.getComponent(U.id,"aabbCollider"),H=R?void 0:M.getComponent(U.id,"circleCollider");if(!R&&!H)continue;let D=O[F];if(!D)D={entityId:U.id,x:T.x,y:T.y,layer:q.layer,collidesWith:q.collidesWith,layerBit:0,collidesWithMask:0,shape:K,halfWidth:0,halfHeight:0,radius:0},O[F]=D;if(!k(D,U.id,T.x,T.y,q.layer,q.collidesWith,R,H))continue;F++}if(!j)$=M.tryGetResource("spatialIndex"),j=!0;_(O,F,Z,$,d,M.eventBus)})})}export{t as defineCollisionLayers,zz as createCollisionPlugin,e as createCollisionPairHandler,b as createCollisionLayer,a as createCircleCollider,o as createAABBCollider};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=8CAA9F9ACA53BC0264756E2164756E21
|
|
4
4
|
//# sourceMappingURL=collision.js.map
|