ecspresso 0.16.3 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- var i=((J)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(J,{get:(K,Q)=>(typeof require<"u"?require:K)[Q]}):J)(function(J){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+J+'" is not supported')});import{definePlugin as n}from"ecspresso";function B(J){let K=new Map,Q=new WeakMap,G=1;function V(O){let $=K.get(O);if($!==void 0)return $;if(G===0)throw Error(`[ecspresso] ${J} layer bitmask overflow: more than 32 distinct layers registered`);let q=G;return K.set(O,q),G<<=1,q}function j(O){let $=Q.get(O);if($!==void 0)return $;let q=0;for(let L=0;L<O.length;L++)q|=V(O[L]);return Q.set(O,q),q}return{getLayerBit:V,getCollidesWithMask:j}}var A={normalX:0,normalY:0,normalZ:0,depth:0},_=0,x=1,D=B("3D collision"),h=D.getLayerBit,u=D.getCollidesWithMask;function f(J,K,Q,G,V,j,O,$,q){if(J.entityId=K,J.layer=j,J.collidesWith=O,J.layerBit=h(j),J.collidesWithMask=u(O),$)return J.x=Q+($.offsetX??0),J.y=G+($.offsetY??0),J.z=V+($.offsetZ??0),J.shape=_,J.halfWidth=$.width/2,J.halfHeight=$.height/2,J.halfDepth=$.depth/2,J.radius=0,!0;if(q)return J.x=Q+(q.offsetX??0),J.y=G+(q.offsetY??0),J.z=V+(q.offsetZ??0),J.shape=x,J.halfWidth=0,J.halfHeight=0,J.halfDepth=0,J.radius=q.radius,!0;return!1}function b(J,K,Q,G,V,j,O,$,q,L,F,H,U){let T=O-J,N=$-K,P=q-Q,k=G+L-Math.abs(T),W=V+F-Math.abs(N),X=j+H-Math.abs(P);if(k<=0||W<=0||X<=0)return!1;if(k<=W&&k<=X)return U.normalX=T>=0?1:-1,U.normalY=0,U.normalZ=0,U.depth=k,!0;if(W<=X)return U.normalX=0,U.normalY=N>=0?1:-1,U.normalZ=0,U.depth=W,!0;return U.normalX=0,U.normalY=0,U.normalZ=P>=0?1:-1,U.depth=X,!0}function d(J,K,Q,G,V,j,O,$,q){let L=V-J,F=j-K,H=O-Q,U=L*L+F*F+H*H,T=G+$;if(U>=T*T)return!1;let N=Math.sqrt(U);if(N===0)return q.normalX=1,q.normalY=0,q.normalZ=0,q.depth=T,!0;return q.normalX=L/N,q.normalY=F/N,q.normalZ=H/N,q.depth=T-N,!0}function g(J,K,Q,G,V,j,O,$,q,L,F){let H=Math.max(J-G,Math.min(O,J+G)),U=Math.max(K-V,Math.min($,K+V)),T=Math.max(Q-j,Math.min(q,Q+j)),N=O-H,P=$-U,k=q-T,W=N*N+P*P+k*k;if(W>=L*L)return!1;if(W===0){let Z=O-(J-G),z=J+G-O,E=$-(K-V),R=K+V-$,Y=q-(Q-j),M=Q+j-q,C=Math.min(Z,z,E,R,Y,M);if(C===z)return F.normalX=1,F.normalY=0,F.normalZ=0,F.depth=z+L,!0;if(C===Z)return F.normalX=-1,F.normalY=0,F.normalZ=0,F.depth=Z+L,!0;if(C===R)return F.normalX=0,F.normalY=1,F.normalZ=0,F.depth=R+L,!0;if(C===E)return F.normalX=0,F.normalY=-1,F.normalZ=0,F.depth=E+L,!0;if(C===M)return F.normalX=0,F.normalY=0,F.normalZ=1,F.depth=M+L,!0;return F.normalX=0,F.normalY=0,F.normalZ=-1,F.depth=Y+L,!0}let X=Math.sqrt(W);return F.normalX=N/X,F.normalY=P/X,F.normalZ=k/X,F.depth=L-X,!0}function y(J,K,Q){if(J.shape===_&&K.shape===_)return b(J.x,J.y,J.z,J.halfWidth,J.halfHeight,J.halfDepth,K.x,K.y,K.z,K.halfWidth,K.halfHeight,K.halfDepth,Q);if(J.shape===x&&K.shape===x)return d(J.x,J.y,J.z,J.radius,K.x,K.y,K.z,K.radius,Q);if(J.shape===_&&K.shape===x)return g(J.x,J.y,J.z,J.halfWidth,J.halfHeight,J.halfDepth,K.x,K.y,K.z,K.radius,Q);if(!g(K.x,K.y,K.z,K.halfWidth,K.halfHeight,K.halfDepth,J.x,J.y,J.z,J.radius,Q))return!1;return Q.normalX=-Q.normalX,Q.normalY=-Q.normalY,Q.normalZ=-Q.normalZ,!0}var m=[],v=!1,r=50;function p(J,K,Q,G,V,j){if(G)c(J,K,Q,G,V,j);else l(J,K,V,j)}function l(J,K,Q,G){if(!v&&K>=r)v=!0,console.warn(`[ecspresso] 3D collision detection is using O(n²) brute force with ${K} colliders. For better performance, install createSpatialIndex3DPlugin() alongside your collision or physics3D plugin.`);for(let V=0;V<K;V++){let j=J[V];if(!j)continue;for(let O=V+1;O<K;O++){let $=J[O];if(!$)continue;if((j.collidesWithMask&$.layerBit|$.collidesWithMask&j.layerBit)===0)continue;if(!y(j,$,A))continue;Q(j,$,A,G)}}}function c(J,K,Q,G,V,j){Q.clear();for(let O=0;O<K;O++){let $=J[O];if(!$)continue;Q.set($.entityId,$)}for(let O=0;O<K;O++){let $=J[O];if(!$)continue;let q=$.shape===_?$.halfWidth:$.radius,L=$.shape===_?$.halfHeight:$.radius,F=$.shape===_?$.halfDepth:$.radius;m.length=0,G.queryBoxInto($.x-q,$.y-L,$.z-F,$.x+q,$.y+L,$.z+F,m,$.entityId);for(let H of m){let U=Q.get(H);if(!U)continue;if(($.collidesWithMask&U.layerBit|U.collidesWithMask&$.layerBit)===0)continue;if(!y($,U,A))continue;V($,U,A,j)}}}function QJ(J,K){return{rigidBody3D:{type:J,mass:J==="static"?1/0:K?.mass??1,drag:K?.drag??0,restitution:K?.restitution??0,friction:K?.friction??0,gravityScale:K?.gravityScale??1},force3D:{x:0,y:0,z:0}}}function $J(J,K,Q){return{force3D:{x:J,y:K,z:Q}}}function jJ(J,K,Q,G,V){let j=J.getComponent(K,"force3D");if(!j)return;j.x+=Q,j.y+=G,j.z+=V}function OJ(J,K,Q,G,V){let j=J.getComponent(K,"velocity3D"),O=J.getComponent(K,"rigidBody3D");if(!j||!O)return;if(O.mass===1/0||O.mass===0)return;j.x+=Q/O.mass,j.y+=G/O.mass,j.z+=V/O.mass}function UJ(J,K,Q,G,V){let j=J.getComponent(K,"velocity3D");if(!j)return;j.x=Q,j.y=G,j.z=V}var S={entityA:0,entityB:0,normalX:0,normalY:0,normalZ:0,depth:0};function s(J,K,Q,G){let V=J.rigidBody.type==="dynamic"&&J.rigidBody.mass>0&&J.rigidBody.mass!==1/0?1/J.rigidBody.mass:0,j=K.rigidBody.type==="dynamic"&&K.rigidBody.mass>0&&K.rigidBody.mass!==1/0?1/K.rigidBody.mass:0,O=V+j;if(O>0){let $=Q.depth/O;if(V>0){let U=G.getComponent(J.entityId,"localTransform3D");if(!U)return;let T=$*V;U.x-=T*Q.normalX,U.y-=T*Q.normalY,U.z-=T*Q.normalZ,J.x=U.x,J.y=U.y,J.z=U.z,G.markChanged(J.entityId,"localTransform3D")}if(j>0){let U=G.getComponent(K.entityId,"localTransform3D");if(!U)return;let T=$*j;U.x+=T*Q.normalX,U.y+=T*Q.normalY,U.z+=T*Q.normalZ,K.x=U.x,K.y=U.y,K.z=U.z,G.markChanged(K.entityId,"localTransform3D")}let q=K.velocity.x-J.velocity.x,L=K.velocity.y-J.velocity.y,F=K.velocity.z-J.velocity.z,H=q*Q.normalX+L*Q.normalY+F*Q.normalZ;if(H<0){let T=-(1+Math.min(J.rigidBody.restitution,K.rigidBody.restitution))*H/O,N=T*V,P=T*j;J.velocity.x-=N*Q.normalX,J.velocity.y-=N*Q.normalY,J.velocity.z-=N*Q.normalZ,K.velocity.x+=P*Q.normalX,K.velocity.y+=P*Q.normalY,K.velocity.z+=P*Q.normalZ;let k=q-H*Q.normalX,W=L-H*Q.normalY,X=F-H*Q.normalZ,Z=Math.sqrt(k*k+W*W+X*X);if(Z>0.000001){let z=k/Z,E=W/Z,R=X/Z,M=Math.sqrt(J.rigidBody.friction*K.rigidBody.friction)*Math.abs(T),C=Math.min(Z/O,M),w=C*V,I=C*j;J.velocity.x+=w*z,J.velocity.y+=w*E,J.velocity.z+=w*R,K.velocity.x-=I*z,K.velocity.y-=I*E,K.velocity.z-=I*R}}G.markChanged(J.entityId,"velocity3D"),G.markChanged(K.entityId,"velocity3D")}S.entityA=J.entityId,S.entityB=K.entityId,S.normalX=Q.normalX,S.normalY=Q.normalY,S.normalZ=Q.normalZ,S.depth=Q.depth,G.eventBus.publish("physics3DCollision",S)}function FJ(J){let{gravity:K={x:0,y:0,z:0},systemGroup:Q="physics3D",collisionSystemGroup:G,integrationPriority:V=1000,collisionPriority:j=900,phase:O="fixedUpdate"}=J??{};return n("physics3D").withComponentTypes().withEventTypes().withResourceTypes().withLabels().withGroups().requires().install(($)=>{$.registerRequired("rigidBody3D","velocity3D",()=>({x:0,y:0,z:0})),$.registerRequired("rigidBody3D","force3D",()=>({x:0,y:0,z:0})),$.addResource("physics3DConfig",{gravity:{x:K.x,y:K.y,z:K.z}}),$.addSystem("physics3D-integration").setPriority(V).inPhase(O).inGroup(Q).addQuery("bodies",{with:["localTransform3D","velocity3D","rigidBody3D","force3D"]}).setProcess(({queries:T,dt:N,ecs:P})=>{let{gravity:k}=P.getResource("physics3DConfig"),W=k.x,X=k.y,Z=k.z;for(let z of T.bodies){let{localTransform3D:E,velocity3D:R,rigidBody3D:Y,force3D:M}=z.components;if(Y.type==="static")continue;if(Y.type==="dynamic"){let C=Y.gravityScale*N;R.x+=W*C,R.y+=X*C,R.z+=Z*C;let w=Y.mass;if(w>0&&w!==1/0){let I=N/w;R.x+=M.x*I,R.y+=M.y*I,R.z+=M.z*I}if(Y.drag>0){let I=Math.max(0,1-Y.drag*N);R.x*=I,R.y*=I,R.z*=I}}E.x+=R.x*N,E.y+=R.y*N,E.z+=R.z*N,M.x=0,M.y=0,M.z=0,P.markChanged(z.id,"localTransform3D")}});let q=$.addSystem("physics3D-collision").setPriority(j).inPhase(O).inGroup(Q);if(G)q.inGroup(G);let L=[],F=new Map,H,U=!1;q.addQuery("collidables",{with:["localTransform3D","rigidBody3D","velocity3D","collisionLayer"]}).setProcess(({queries:T,ecs:N})=>{let P=0;for(let k of T.collidables){let{localTransform3D:W,rigidBody3D:X,velocity3D:Z,collisionLayer:z}=k.components,E=N.getComponent(k.id,"aabb3DCollider"),R=E?void 0:N.getComponent(k.id,"sphereCollider");if(!E&&!R)continue;let Y=L[P];if(!Y)Y={entityId:k.id,x:W.x,y:W.y,z:W.z,layer:z.layer,collidesWith:z.collidesWith,layerBit:0,collidesWithMask:0,shape:_,halfWidth:0,halfHeight:0,halfDepth:0,radius:0,rigidBody:X,velocity:Z},L[P]=Y;else Y.rigidBody=X,Y.velocity=Z;if(!f(Y,k.id,W.x,W.y,W.z,z.layer,z.collidesWith,E,R))continue;P++}if(!U)H=N.tryGetResource("spatialIndex3D"),U=!0;p(L,P,F,H,s,N)})})}export{UJ as setVelocity3D,QJ as createRigidBody3D,FJ as createPhysics3DPlugin,$J as createForce3D,OJ as applyImpulse3D,jJ as applyForce3D};
1
+ var e=((J)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(J,{get:(K,Q)=>(typeof require<"u"?require:K)[Q]}):J)(function(J){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+J+'" is not supported')});import{definePlugin as s}from"ecspresso";function v(J){let K=new Map,Q=new WeakMap,G=1;function V(j){let $=K.get(j);if($!==void 0)return $;if(G===0)throw Error(`[ecspresso] ${J} layer bitmask overflow: more than 32 distinct layers registered`);let N=G;return K.set(j,N),G<<=1,N}function O(j){let $=Q.get(j);if($!==void 0)return $;let N=0;for(let k=0;k<j.length;k++)N|=V(j[k]);return Q.set(j,N),N}return{getLayerBit:V,getCollidesWithMask:O}}var m={normalX:0,normalY:0,normalZ:0,depth:0},w=0,A=1,y=v("3D collision"),b=y.getLayerBit,u=y.getCollidesWithMask;function B(J,K,Q,G,V,O,j,$,N){if(J.entityId=K,J.layer=O,J.collidesWith=j,J.layerBit=b(O),J.collidesWithMask=u(j),$)return J.x=Q+($.offsetX??0),J.y=G+($.offsetY??0),J.z=V+($.offsetZ??0),J.shape=w,J.halfWidth=$.width/2,J.halfHeight=$.height/2,J.halfDepth=$.depth/2,J.radius=0,!0;if(N)return J.x=Q+(N.offsetX??0),J.y=G+(N.offsetY??0),J.z=V+(N.offsetZ??0),J.shape=A,J.halfWidth=0,J.halfHeight=0,J.halfDepth=0,J.radius=N.radius,!0;return!1}function d(J,K,Q,G,V,O,j,$,N,k,F,z,U){let R=j-J,L=$-K,P=N-Q,W=G+k-Math.abs(R),q=V+F-Math.abs(L),T=O+z-Math.abs(P);if(W<=0||q<=0||T<=0)return!1;if(W<=q&&W<=T)return U.normalX=R>=0?1:-1,U.normalY=0,U.normalZ=0,U.depth=W,!0;if(q<=T)return U.normalX=0,U.normalY=L>=0?1:-1,U.normalZ=0,U.depth=q,!0;return U.normalX=0,U.normalY=0,U.normalZ=P>=0?1:-1,U.depth=T,!0}function r(J,K,Q,G,V,O,j,$,N){let k=V-J,F=O-K,z=j-Q,U=k*k+F*F+z*z,R=G+$;if(U>=R*R)return!1;let L=Math.sqrt(U);if(L===0)return N.normalX=1,N.normalY=0,N.normalZ=0,N.depth=R,!0;return N.normalX=k/L,N.normalY=F/L,N.normalZ=z/L,N.depth=R-L,!0}function D(J,K,Q,G,V,O,j,$,N,k,F){let z=Math.max(J-G,Math.min(j,J+G)),U=Math.max(K-V,Math.min($,K+V)),R=Math.max(Q-O,Math.min(N,Q+O)),L=j-z,P=$-U,W=N-R,q=L*L+P*P+W*W;if(q>=k*k)return!1;if(q===0){let Z=j-(J-G),Y=J+G-j,H=$-(K-V),E=K+V-$,X=N-(Q-O),_=Q+O-N,M=Math.min(Z,Y,H,E,X,_);if(M===Y)return F.normalX=1,F.normalY=0,F.normalZ=0,F.depth=Y+k,!0;if(M===Z)return F.normalX=-1,F.normalY=0,F.normalZ=0,F.depth=Z+k,!0;if(M===E)return F.normalX=0,F.normalY=1,F.normalZ=0,F.depth=E+k,!0;if(M===H)return F.normalX=0,F.normalY=-1,F.normalZ=0,F.depth=H+k,!0;if(M===_)return F.normalX=0,F.normalY=0,F.normalZ=1,F.depth=_+k,!0;return F.normalX=0,F.normalY=0,F.normalZ=-1,F.depth=X+k,!0}let T=Math.sqrt(q);return F.normalX=L/T,F.normalY=P/T,F.normalZ=W/T,F.depth=k-T,!0}function p(J,K,Q){if(J.shape===w&&K.shape===w)return d(J.x,J.y,J.z,J.halfWidth,J.halfHeight,J.halfDepth,K.x,K.y,K.z,K.halfWidth,K.halfHeight,K.halfDepth,Q);if(J.shape===A&&K.shape===A)return r(J.x,J.y,J.z,J.radius,K.x,K.y,K.z,K.radius,Q);if(J.shape===w&&K.shape===A)return D(J.x,J.y,J.z,J.halfWidth,J.halfHeight,J.halfDepth,K.x,K.y,K.z,K.radius,Q);if(!D(K.x,K.y,K.z,K.halfWidth,K.halfHeight,K.halfDepth,J.x,J.y,J.z,J.radius,Q))return!1;return Q.normalX=-Q.normalX,Q.normalY=-Q.normalY,Q.normalZ=-Q.normalZ,!0}var g=[],f=!1,c=50;function h(J,K,Q,G,V,O){if(G)n(J,K,Q,G,V,O);else l(J,K,V,O)}function l(J,K,Q,G){if(!f&&K>=c)f=!0,console.warn(`[ecspresso] 3D collision detection is using O(n²) brute force with ${K} colliders. For better performance, install createSpatialIndex3DPlugin() alongside your collision or physics3D plugin.`);for(let V=0;V<K;V++){let O=J[V];if(!O)continue;for(let j=V+1;j<K;j++){let $=J[j];if(!$)continue;if((O.collidesWithMask&$.layerBit|$.collidesWithMask&O.layerBit)===0)continue;if(!p(O,$,m))continue;Q(O,$,m,G)}}}function n(J,K,Q,G,V,O){Q.clear();for(let j=0;j<K;j++){let $=J[j];if(!$)continue;Q.set($.entityId,$)}for(let j=0;j<K;j++){let $=J[j];if(!$)continue;let N=$.shape===w?$.halfWidth:$.radius,k=$.shape===w?$.halfHeight:$.radius,F=$.shape===w?$.halfDepth:$.radius;g.length=0,G.queryBoxInto($.x-N,$.y-k,$.z-F,$.x+N,$.y+k,$.z+F,g,$.entityId);for(let z of g){let U=Q.get(z);if(!U)continue;if(($.collidesWithMask&U.layerBit|U.collidesWithMask&$.layerBit)===0)continue;if(!p($,U,m))continue;V($,U,m,O)}}}function $J(J,K){return{rigidBody3D:{type:J,mass:J==="static"?1/0:K?.mass??1,drag:K?.drag??0,restitution:K?.restitution??0,friction:K?.friction??0,gravityScale:K?.gravityScale??1},force3D:{x:0,y:0,z:0}}}function OJ(J,K,Q){return{force3D:{x:J,y:K,z:Q}}}function jJ(J,K,Q,G,V){let O=J.getComponent(K,"force3D");if(!O)return;O.x+=Q,O.y+=G,O.z+=V}function UJ(J,K,Q,G,V){let O=J.getComponent(K,"velocity3D"),j=J.getComponent(K,"rigidBody3D");if(!O||!j)return;if(j.mass===1/0||j.mass===0)return;O.x+=Q/j.mass,O.y+=G/j.mass,O.z+=V/j.mass}function FJ(J,K,Q,G,V){let O=J.getComponent(K,"velocity3D");if(!O)return;O.x=Q,O.y=G,O.z=V}var x={entityA:0,entityB:0,normalX:0,normalY:0,normalZ:0,depth:0};function i(J,K,Q,G){let V=J.rigidBody.type==="dynamic"&&J.rigidBody.mass>0&&J.rigidBody.mass!==1/0?1/J.rigidBody.mass:0,O=K.rigidBody.type==="dynamic"&&K.rigidBody.mass>0&&K.rigidBody.mass!==1/0?1/K.rigidBody.mass:0,j=V+O;if(j>0){let $=Q.depth/j;if(V>0){let U=G.getComponent(J.entityId,"localTransform3D");if(!U)return;let R=$*V;U.x-=R*Q.normalX,U.y-=R*Q.normalY,U.z-=R*Q.normalZ,J.x=U.x,J.y=U.y,J.z=U.z,G.markChanged(J.entityId,"localTransform3D")}if(O>0){let U=G.getComponent(K.entityId,"localTransform3D");if(!U)return;let R=$*O;U.x+=R*Q.normalX,U.y+=R*Q.normalY,U.z+=R*Q.normalZ,K.x=U.x,K.y=U.y,K.z=U.z,G.markChanged(K.entityId,"localTransform3D")}let N=K.velocity.x-J.velocity.x,k=K.velocity.y-J.velocity.y,F=K.velocity.z-J.velocity.z,z=N*Q.normalX+k*Q.normalY+F*Q.normalZ;if(z<0){let R=-(1+Math.min(J.rigidBody.restitution,K.rigidBody.restitution))*z/j,L=R*V,P=R*O;J.velocity.x-=L*Q.normalX,J.velocity.y-=L*Q.normalY,J.velocity.z-=L*Q.normalZ,K.velocity.x+=P*Q.normalX,K.velocity.y+=P*Q.normalY,K.velocity.z+=P*Q.normalZ;let W=N-z*Q.normalX,q=k-z*Q.normalY,T=F-z*Q.normalZ,Z=Math.sqrt(W*W+q*q+T*T);if(Z>0.000001){let Y=W/Z,H=q/Z,E=T/Z,_=Math.sqrt(J.rigidBody.friction*K.rigidBody.friction)*Math.abs(R),M=Math.min(Z/j,_),I=M*V,C=M*O;J.velocity.x+=I*Y,J.velocity.y+=I*H,J.velocity.z+=I*E,K.velocity.x-=C*Y,K.velocity.y-=C*H,K.velocity.z-=C*E}}G.markChanged(J.entityId,"velocity3D"),G.markChanged(K.entityId,"velocity3D")}x.entityA=J.entityId,x.entityB=K.entityId,x.normalX=Q.normalX,x.normalY=Q.normalY,x.normalZ=Q.normalZ,x.depth=Q.depth,G.eventBus.publish("physics3DCollision",x)}function GJ(J){let{gravity:K={x:0,y:0,z:0},systemGroup:Q="physics3D",collisionSystemGroup:G,integrationPriority:V=1000,collisionPriority:O=900,phase:j="fixedUpdate"}=J??{};return s("physics3D").withComponentTypes().withEventTypes().withResourceTypes().withLabels().withGroups().requires().install(($)=>{$.registerRequired("rigidBody3D","velocity3D",()=>({x:0,y:0,z:0})),$.registerRequired("rigidBody3D","force3D",()=>({x:0,y:0,z:0})),$.addResource("physics3DConfig",{gravity:{x:K.x,y:K.y,z:K.z}}),$.addSystem("physics3D-integration").setPriority(V).inPhase(j).inGroup(Q).addQuery("bodies",{with:["localTransform3D","velocity3D","rigidBody3D","force3D"]}).setProcess(({queries:L,dt:P,ecs:W})=>{let{gravity:q}=W.getResource("physics3DConfig"),T=q.x,Z=q.y,Y=q.z;for(let H of L.bodies){let{localTransform3D:E,velocity3D:X,rigidBody3D:_,force3D:M}=H.components;if(_.type==="static")continue;if(_.type==="dynamic"){let I=_.gravityScale*P;X.x+=T*I,X.y+=Z*I,X.z+=Y*I;let C=_.mass;if(C>0&&C!==1/0){let S=P/C;X.x+=M.x*S,X.y+=M.y*S,X.z+=M.z*S}if(_.drag>0){let S=Math.max(0,1-_.drag*P);X.x*=S,X.y*=S,X.z*=S}}E.x+=X.x*P,E.y+=X.y*P,E.z+=X.z*P,M.x=0,M.y=0,M.z=0,W.markChanged(H.id,"localTransform3D")}});let N=$.addSystem("physics3D-collision").setPriority(O).inPhase(j).inGroup(Q);if(G)N.inGroup(G);let k=[],F=new Map,z,U=!1,R=(L,P,W,q,T,Z)=>{let Y=k[L];if(!Y)Y={entityId:P,x:0,y:0,z:0,layer:W,collidesWith:q,layerBit:0,collidesWithMask:0,shape:w,halfWidth:0,halfHeight:0,halfDepth:0,radius:0,rigidBody:T,velocity:Z},k[L]=Y;else Y.rigidBody=T,Y.velocity=Z;return Y};N.addQuery("aabbOnly",{with:["localTransform3D","rigidBody3D","velocity3D","collisionLayer","aabb3DCollider"],without:["sphereCollider"]}).addQuery("sphereOnly",{with:["localTransform3D","rigidBody3D","velocity3D","collisionLayer","sphereCollider"],without:["aabb3DCollider"]}).addQuery("both",{with:["localTransform3D","rigidBody3D","velocity3D","collisionLayer","aabb3DCollider","sphereCollider"]}).setProcess(({queries:L,ecs:P})=>{let W=0;for(let q of L.aabbOnly){let{localTransform3D:T,rigidBody3D:Z,velocity3D:Y,collisionLayer:H,aabb3DCollider:E}=q.components,X=R(W,q.id,H.layer,H.collidesWith,Z,Y);if(!B(X,q.id,T.x,T.y,T.z,H.layer,H.collidesWith,E,void 0))continue;W++}for(let q of L.sphereOnly){let{localTransform3D:T,rigidBody3D:Z,velocity3D:Y,collisionLayer:H,sphereCollider:E}=q.components,X=R(W,q.id,H.layer,H.collidesWith,Z,Y);if(!B(X,q.id,T.x,T.y,T.z,H.layer,H.collidesWith,void 0,E))continue;W++}for(let q of L.both){let{localTransform3D:T,rigidBody3D:Z,velocity3D:Y,collisionLayer:H,aabb3DCollider:E,sphereCollider:X}=q.components,_=R(W,q.id,H.layer,H.collidesWith,Z,Y);if(!B(_,q.id,T.x,T.y,T.z,H.layer,H.collidesWith,E,X))continue;W++}if(!U)z=P.tryGetResource("spatialIndex3D"),U=!0;h(k,W,F,z,i,P)})})}export{FJ as setVelocity3D,$J as createRigidBody3D,GJ as createPhysics3DPlugin,OJ as createForce3D,UJ as applyImpulse3D,jJ as applyForce3D};
2
2
 
3
- //# debugId=6C1C37A7CF5B331564756E2164756E21
3
+ //# debugId=FABAD286A305821E64756E2164756E21
4
4
  //# sourceMappingURL=physics3D.js.map
@@ -2,11 +2,11 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/physics/physics3D.ts", "../src/utils/layer-bit-registry.ts", "../src/utils/narrowphase3D.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Physics 3D Plugin for ECSpresso\n *\n * Provides ECS-native 3D arcade physics: gravity, forces, drag, semi-implicit Euler\n * integration, and impulse-based collision response with friction.\n *\n * Reuses RigidBody and collider types from the 2D physics/collision plugins for\n * shape definitions. Has its own collision detection in fixedUpdate for physics\n * response; the existing collision3D plugin can still run in postUpdate for game\n * logic events.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { Vector3D } from 'ecspresso';\nimport type { Transform3DWorldConfig } from '../spatial/transform3D';\nimport type { Collision3DComponentTypes, LayerFactories } from './collision3D';\nimport type { RigidBody, BodyType, RigidBodyOptions } from './physics2D';\nimport { fillBaseColliderInfo3D, detectCollisions3D, AABB3D_SHAPE, type Contact3D, type BaseColliderInfo3D } from '../../utils/narrowphase3D';\nimport type { SpatialIndex3D } from '../../utils/spatial-hash3D';\n\n// Re-export so consumers can type rigid bodies without importing physics2D\nexport type { RigidBody, BodyType, RigidBodyOptions };\n\n// ==================== Component Types ====================\n\n/**\n * Component types directly provided by the physics3D plugin.\n */\nexport interface Physics3DOwnComponentTypes {\n\trigidBody3D: RigidBody;\n\tvelocity3D: Vector3D;\n\tforce3D: Vector3D;\n}\n\n/**\n * Full component types available when using the physics3D plugin\n * (own components + transform + collision dependencies).\n * Convenience alias for consumer code.\n */\nexport interface Physics3DComponentTypes<L extends string = never> extends Collision3DComponentTypes<L>, Physics3DOwnComponentTypes {}\n\n// ==================== Resource Types ====================\n\n/**\n * Physics 3D configuration resource.\n */\nexport interface Physics3DConfig {\n\tgravity: Vector3D;\n}\n\nexport interface Physics3DResourceTypes {\n\tphysics3DConfig: Physics3DConfig;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event emitted for each physics 3D collision pair.\n *\n * Normal components are flattened (`normalX`/`normalY`/`normalZ`) rather than\n * nested in a `Vector3D` to avoid a per-event allocation in the physics hot path.\n */\nexport interface Physics3DCollisionEvent {\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/** Unit normal Z, pointing from A toward B */\n\tnormalZ: number;\n\t/** Penetration depth (positive) */\n\tdepth: number;\n}\n\nexport interface Physics3DEventTypes {\n\tphysics3DCollision: Physics3DCollisionEvent;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface Physics3DPluginOptions<G extends string = 'physics3D', CG extends string = never> {\n\t/** World gravity vector (default: {x: 0, y: 0, z: 0}) */\n\tgravity?: Vector3D;\n\t/** System group name (default: 'physics3D') */\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\n/**\n * Create a rigidBody3D + force3D component pair.\n * Static bodies automatically get mass=Infinity.\n */\nexport function createRigidBody3D(\n\ttype: BodyType,\n\toptions?: RigidBodyOptions,\n): { rigidBody3D: RigidBody; force3D: Vector3D } {\n\treturn {\n\t\trigidBody3D: {\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\tforce3D: { x: 0, y: 0, z: 0 },\n\t};\n}\n\n/**\n * Create a force3D component with initial values.\n */\nexport function createForce3D(x: number, y: number, z: number): { force3D: Vector3D } {\n\treturn { force3D: { x, y, z } };\n}\n\n/**\n * Accumulate a force onto an entity's force3D component.\n */\nexport function applyForce3D(\n\tecs: { getComponent(id: number, name: 'force3D'): Vector3D | undefined },\n\tentityId: number,\n\tfx: number,\n\tfy: number,\n\tfz: number,\n): void {\n\tconst force = ecs.getComponent(entityId, 'force3D');\n\tif (!force) return;\n\tforce.x += fx;\n\tforce.y += fy;\n\tforce.z += fz;\n}\n\n/**\n * Apply an instantaneous impulse: velocity3D += impulse / mass.\n */\nexport function applyImpulse3D(\n\tecs: {\n\t\tgetComponent(id: number, name: 'velocity3D'): Vector3D | undefined;\n\t\tgetComponent(id: number, name: 'rigidBody3D'): RigidBody | undefined;\n\t},\n\tentityId: number,\n\tix: number,\n\tiy: number,\n\tiz: number,\n): void {\n\tconst velocity = ecs.getComponent(entityId, 'velocity3D');\n\tconst rigidBody = ecs.getComponent(entityId, 'rigidBody3D');\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\tvelocity.z += iz / rigidBody.mass;\n}\n\n/**\n * Directly set an entity's velocity3D.\n */\nexport function setVelocity3D(\n\tecs: { getComponent(id: number, name: 'velocity3D'): Vector3D | undefined },\n\tentityId: number,\n\tvx: number,\n\tvy: number,\n\tvz: number,\n): void {\n\tconst velocity = ecs.getComponent(entityId, 'velocity3D');\n\tif (!velocity) return;\n\tvelocity.x = vx;\n\tvelocity.y = vy;\n\tvelocity.z = vz;\n}\n\n// ==================== Internal: Collider Info ====================\n\ninterface Physics3DColliderInfo<L extends string = string> extends BaseColliderInfo3D<L> {\n\trigidBody: RigidBody;\n\tvelocity: Vector3D;\n}\n\n// ==================== Collision Response ====================\n\n/**\n * Module-level reusable physics3D collision event. Subscribers must consume\n * synchronously — same contract as the shared narrowphase Contact3D.\n */\nconst _physicsCollisionEvent: Physics3DCollisionEvent = {\n\tentityA: 0, entityB: 0, normalX: 0, normalY: 0, normalZ: 0, depth: 0,\n};\n\ninterface PhysicsEcs3DLike {\n\tgetComponent(id: number, name: 'localTransform3D'): { x: number; y: number; z: number } | undefined;\n\teventBus: { publish(event: 'physics3DCollision', data: Physics3DCollisionEvent): void };\n\tmarkChanged(entityId: number, componentName: 'localTransform3D' | 'velocity3D'): void;\n}\n\n/**\n * Resolve a 3D physics collision pair: position correction, impulse response, event.\n *\n * Friction uses a tangent plane projection: the tangential velocity is the\n * component of relative velocity perpendicular to the contact normal. This\n * generalizes the 2D tangent-line approach to 3D — mathematically the same\n * operation with an added Z component.\n */\nfunction resolvePhysicsContact3D(\n\ta: Physics3DColliderInfo,\n\tb: Physics3DColliderInfo,\n\tcontact: Contact3D,\n\tecs: PhysicsEcs3DLike,\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, 'localTransform3D');\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\tltA.z -= corrA * contact.normalZ;\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\ta.z = ltA.z;\n\t\t\tecs.markChanged(a.entityId, 'localTransform3D');\n\t\t}\n\n\t\tif (invMassB > 0) {\n\t\t\tconst ltB = ecs.getComponent(b.entityId, 'localTransform3D');\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\tltB.z += corrB * contact.normalZ;\n\t\t\tb.x = ltB.x;\n\t\t\tb.y = ltB.y;\n\t\t\tb.z = ltB.z;\n\t\t\tecs.markChanged(b.entityId, 'localTransform3D');\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 relVelZ = b.velocity.z - a.velocity.z;\n\t\tconst velAlongNormal = relVelX * contact.normalX + relVelY * contact.normalY + relVelZ * contact.normalZ;\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\t\t\tconst impA = normalImpulse * invMassA;\n\t\t\tconst impB = normalImpulse * invMassB;\n\n\t\t\ta.velocity.x -= impA * contact.normalX;\n\t\t\ta.velocity.y -= impA * contact.normalY;\n\t\t\ta.velocity.z -= impA * contact.normalZ;\n\t\t\tb.velocity.x += impB * contact.normalX;\n\t\t\tb.velocity.y += impB * contact.normalY;\n\t\t\tb.velocity.z += impB * contact.normalZ;\n\n\t\t\t// Friction (tangential impulse — project relative velocity onto tangent plane)\n\t\t\tconst tangentX = relVelX - velAlongNormal * contact.normalX;\n\t\t\tconst tangentY = relVelY - velAlongNormal * contact.normalY;\n\t\t\tconst tangentZ = relVelZ - velAlongNormal * contact.normalZ;\n\t\t\tconst tangentSpeed = Math.sqrt(tangentX * tangentX + tangentY * tangentY + tangentZ * tangentZ);\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 tangentNZ = tangentZ / 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\t\t\t\tconst tanA = tangentImpulse * invMassA;\n\t\t\t\tconst tanB = tangentImpulse * invMassB;\n\n\t\t\t\ta.velocity.x += tanA * tangentNX;\n\t\t\t\ta.velocity.y += tanA * tangentNY;\n\t\t\t\ta.velocity.z += tanA * tangentNZ;\n\t\t\t\tb.velocity.x -= tanB * tangentNX;\n\t\t\t\tb.velocity.y -= tanB * tangentNY;\n\t\t\t\tb.velocity.z -= tanB * tangentNZ;\n\t\t\t}\n\t\t}\n\n\t\tecs.markChanged(a.entityId, 'velocity3D');\n\t\tecs.markChanged(b.entityId, 'velocity3D');\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.normalZ = contact.normalZ;\n\t_physicsCollisionEvent.depth = contact.depth;\n\tecs.eventBus.publish('physics3DCollision', _physicsCollisionEvent);\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a 3D physics plugin for ECSpresso.\n *\n * Provides:\n * - Semi-implicit Euler integration (gravity, forces, drag → velocity3D → position)\n * - Impulse-based collision response with restitution and friction\n * - physics3DCollision events with contact normal and depth\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransform3DPlugin())\n * .withPlugin(createPhysics3DPlugin({ gravity: { x: 0, y: -9.81, z: 0 } }))\n * .withFixedTimestep(1/60)\n * .build();\n *\n * ecs.spawn({\n * ...createTransform3D(0, 10, 0),\n * ...createRigidBody3D('dynamic', { mass: 1, restitution: 0.5 }),\n * velocity3D: { x: 0, y: 0, z: 0 },\n * ...createAABB3DCollider(1, 1, 1),\n * ...createCollisionLayer('player', ['ground']),\n * });\n * ```\n */\n\ntype Physics3DProvides<L extends string = never> = Physics3DOwnComponentTypes & Collision3DComponentTypes<L>;\n\nexport function createPhysics3DPlugin<L extends string = never, G extends string = 'physics3D', CG extends string = never>(\n\toptions?: Physics3DPluginOptions<G, CG> & { layers?: LayerFactories<Record<L, readonly string[]>> },\n) {\n\tconst {\n\t\tgravity = { x: 0, y: 0, z: 0 },\n\t\tsystemGroup = 'physics3D',\n\t\tcollisionSystemGroup,\n\t\tintegrationPriority = 1000,\n\t\tcollisionPriority = 900,\n\t\tphase = 'fixedUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('physics3D')\n\t\t.withComponentTypes<Physics3DProvides<L>>()\n\t\t.withEventTypes<Physics3DEventTypes>()\n\t\t.withResourceTypes<Physics3DResourceTypes>()\n\t\t.withLabels<'physics3D-integration' | 'physics3D-collision'>()\n\t\t.withGroups<G | CG>()\n\t\t.requires<Transform3DWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// rigidBody3D requires velocity3D and force3D — auto-add with zero defaults\n\t\t\tworld.registerRequired('rigidBody3D', 'velocity3D', () => ({ x: 0, y: 0, z: 0 }));\n\t\t\tworld.registerRequired('rigidBody3D', 'force3D', () => ({ x: 0, y: 0, z: 0 }));\n\n\t\t\tworld.addResource('physics3DConfig', { gravity: { x: gravity.x, y: gravity.y, z: gravity.z } });\n\n\t\t\t// ==================== Integration System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('physics3D-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: ['localTransform3D', 'velocity3D', 'rigidBody3D', 'force3D'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tconst { gravity: g } = ecs.getResource('physics3DConfig');\n\t\t\t\t\tconst gx = g.x;\n\t\t\t\t\tconst gy = g.y;\n\t\t\t\t\tconst gz = g.z;\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. Needs collision response to\n\t\t\t\t\t// wake sleepers back up; keep in sync with physics2D when landed.\n\t\t\t\t\tfor (const entity of queries.bodies) {\n\t\t\t\t\t\tconst { localTransform3D, velocity3D, rigidBody3D, force3D } = entity.components;\n\n\t\t\t\t\t\t// Static bodies: skip entirely\n\t\t\t\t\t\tif (rigidBody3D.type === 'static') continue;\n\n\t\t\t\t\t\t// Dynamic bodies: apply gravity, forces, drag\n\t\t\t\t\t\tif (rigidBody3D.type === 'dynamic') {\n\t\t\t\t\t\t\t// 1. Gravity\n\t\t\t\t\t\t\tconst gsdt = rigidBody3D.gravityScale * dt;\n\t\t\t\t\t\t\tvelocity3D.x += gx * gsdt;\n\t\t\t\t\t\t\tvelocity3D.y += gy * gsdt;\n\t\t\t\t\t\t\tvelocity3D.z += gz * 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 = rigidBody3D.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\tvelocity3D.x += force3D.x * invMassDt;\n\t\t\t\t\t\t\t\tvelocity3D.y += force3D.y * invMassDt;\n\t\t\t\t\t\t\t\tvelocity3D.z += force3D.z * 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 (rigidBody3D.drag > 0) {\n\t\t\t\t\t\t\t\tconst damping = Math.max(0, 1 - rigidBody3D.drag * dt);\n\t\t\t\t\t\t\t\tvelocity3D.x *= damping;\n\t\t\t\t\t\t\t\tvelocity3D.y *= damping;\n\t\t\t\t\t\t\t\tvelocity3D.z *= 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\tlocalTransform3D.x += velocity3D.x * dt;\n\t\t\t\t\t\tlocalTransform3D.y += velocity3D.y * dt;\n\t\t\t\t\t\tlocalTransform3D.z += velocity3D.z * dt;\n\n\t\t\t\t\t\t// Clear accumulated forces\n\t\t\t\t\t\tforce3D.x = 0;\n\t\t\t\t\t\tforce3D.y = 0;\n\t\t\t\t\t\tforce3D.z = 0;\n\n\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform3D');\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('physics3D-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 Physics3DColliderInfo slots reused across frames.\n\t\t\t// Steady-state: zero allocations per frame once the pool is warm.\n\t\t\tconst colliderPool: Physics3DColliderInfo<L>[] = [];\n\t\t\t// Reusable entityId → collider lookup for the broadphase path.\n\t\t\tconst broadphaseMap = new Map<number, Physics3DColliderInfo<L>>();\n\t\t\t// Cached spatial index reference (resolved once on first frame).\n\t\t\tlet cachedSI: SpatialIndex3D | 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: ['localTransform3D', 'rigidBody3D', 'velocity3D', '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// \"aabb3DCollider OR sphereCollider\". Splitting into two queries\n\t\t\t\t\t// (aabb-bearing, sphere-bearing) would eliminate these lookups at\n\t\t\t\t\t// the cost of two pool-fill passes. Revisit once the query API\n\t\t\t\t\t// gains `anyOf`-style predicates.\n\t\t\t\t\tfor (const entity of queries.collidables) {\n\t\t\t\t\t\tconst { localTransform3D, rigidBody3D, velocity3D, collisionLayer } = entity.components;\n\t\t\t\t\t\tconst aabb = ecs.getComponent(entity.id, 'aabb3DCollider');\n\t\t\t\t\t\tconst sphere = aabb ? undefined : ecs.getComponent(entity.id, 'sphereCollider');\n\t\t\t\t\t\tif (!aabb && !sphere) 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: localTransform3D.x,\n\t\t\t\t\t\t\t\ty: localTransform3D.y,\n\t\t\t\t\t\t\t\tz: localTransform3D.z,\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: AABB3D_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\thalfDepth: 0,\n\t\t\t\t\t\t\t\tradius: 0,\n\t\t\t\t\t\t\t\trigidBody: rigidBody3D,\n\t\t\t\t\t\t\t\tvelocity: velocity3D,\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 = rigidBody3D;\n\t\t\t\t\t\t\tslot.velocity = velocity3D;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!fillBaseColliderInfo3D(\n\t\t\t\t\t\t\tslot,\n\t\t\t\t\t\t\tentity.id, localTransform3D.x, localTransform3D.y, localTransform3D.z,\n\t\t\t\t\t\t\tcollisionLayer.layer, collisionLayer.collidesWith,\n\t\t\t\t\t\t\taabb, sphere,\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<SpatialIndex3D>('spatialIndex3D');\n\t\t\t\t\t\tsiResolved = true;\n\t\t\t\t\t}\n\t\t\t\t\tdetectCollisions3D(colliderPool, count, broadphaseMap, cachedSI, resolvePhysicsContact3D, ecs);\n\t\t\t\t});\n\t\t});\n}\n",
5
+ "/**\n * Physics 3D Plugin for ECSpresso\n *\n * Provides ECS-native 3D arcade physics: gravity, forces, drag, semi-implicit Euler\n * integration, and impulse-based collision response with friction.\n *\n * Reuses RigidBody and collider types from the 2D physics/collision plugins for\n * shape definitions. Has its own collision detection in fixedUpdate for physics\n * response; the existing collision3D plugin can still run in postUpdate for game\n * logic events.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { Vector3D } from 'ecspresso';\nimport type { Transform3DWorldConfig } from '../spatial/transform3D';\nimport type { Collision3DComponentTypes, LayerFactories } from './collision3D';\nimport type { RigidBody, BodyType, RigidBodyOptions } from './physics2D';\nimport { fillBaseColliderInfo3D, detectCollisions3D, AABB3D_SHAPE, type Contact3D, type BaseColliderInfo3D } from '../../utils/narrowphase3D';\nimport type { SpatialIndex3D } from '../../utils/spatial-hash3D';\n\n// Re-export so consumers can type rigid bodies without importing physics2D\nexport type { RigidBody, BodyType, RigidBodyOptions };\n\n// ==================== Component Types ====================\n\n/**\n * Component types directly provided by the physics3D plugin.\n */\nexport interface Physics3DOwnComponentTypes {\n\trigidBody3D: RigidBody;\n\tvelocity3D: Vector3D;\n\tforce3D: Vector3D;\n}\n\n/**\n * Full component types available when using the physics3D plugin\n * (own components + transform + collision dependencies).\n * Convenience alias for consumer code.\n */\nexport interface Physics3DComponentTypes<L extends string = never> extends Collision3DComponentTypes<L>, Physics3DOwnComponentTypes {}\n\n// ==================== Resource Types ====================\n\n/**\n * Physics 3D configuration resource.\n */\nexport interface Physics3DConfig {\n\tgravity: Vector3D;\n}\n\nexport interface Physics3DResourceTypes {\n\tphysics3DConfig: Physics3DConfig;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event emitted for each physics 3D collision pair.\n *\n * Normal components are flattened (`normalX`/`normalY`/`normalZ`) rather than\n * nested in a `Vector3D` to avoid a per-event allocation in the physics hot path.\n */\nexport interface Physics3DCollisionEvent {\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/** Unit normal Z, pointing from A toward B */\n\tnormalZ: number;\n\t/** Penetration depth (positive) */\n\tdepth: number;\n}\n\nexport interface Physics3DEventTypes {\n\tphysics3DCollision: Physics3DCollisionEvent;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface Physics3DPluginOptions<G extends string = 'physics3D', CG extends string = never> {\n\t/** World gravity vector (default: {x: 0, y: 0, z: 0}) */\n\tgravity?: Vector3D;\n\t/** System group name (default: 'physics3D') */\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\n/**\n * Create a rigidBody3D + force3D component pair.\n * Static bodies automatically get mass=Infinity.\n */\nexport function createRigidBody3D(\n\ttype: BodyType,\n\toptions?: RigidBodyOptions,\n): { rigidBody3D: RigidBody; force3D: Vector3D } {\n\treturn {\n\t\trigidBody3D: {\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\tforce3D: { x: 0, y: 0, z: 0 },\n\t};\n}\n\n/**\n * Create a force3D component with initial values.\n */\nexport function createForce3D(x: number, y: number, z: number): { force3D: Vector3D } {\n\treturn { force3D: { x, y, z } };\n}\n\n/**\n * Accumulate a force onto an entity's force3D component.\n */\nexport function applyForce3D(\n\tecs: { getComponent(id: number, name: 'force3D'): Vector3D | undefined },\n\tentityId: number,\n\tfx: number,\n\tfy: number,\n\tfz: number,\n): void {\n\tconst force = ecs.getComponent(entityId, 'force3D');\n\tif (!force) return;\n\tforce.x += fx;\n\tforce.y += fy;\n\tforce.z += fz;\n}\n\n/**\n * Apply an instantaneous impulse: velocity3D += impulse / mass.\n */\nexport function applyImpulse3D(\n\tecs: {\n\t\tgetComponent(id: number, name: 'velocity3D'): Vector3D | undefined;\n\t\tgetComponent(id: number, name: 'rigidBody3D'): RigidBody | undefined;\n\t},\n\tentityId: number,\n\tix: number,\n\tiy: number,\n\tiz: number,\n): void {\n\tconst velocity = ecs.getComponent(entityId, 'velocity3D');\n\tconst rigidBody = ecs.getComponent(entityId, 'rigidBody3D');\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\tvelocity.z += iz / rigidBody.mass;\n}\n\n/**\n * Directly set an entity's velocity3D.\n */\nexport function setVelocity3D(\n\tecs: { getComponent(id: number, name: 'velocity3D'): Vector3D | undefined },\n\tentityId: number,\n\tvx: number,\n\tvy: number,\n\tvz: number,\n): void {\n\tconst velocity = ecs.getComponent(entityId, 'velocity3D');\n\tif (!velocity) return;\n\tvelocity.x = vx;\n\tvelocity.y = vy;\n\tvelocity.z = vz;\n}\n\n// ==================== Internal: Collider Info ====================\n\ninterface Physics3DColliderInfo<L extends string = string> extends BaseColliderInfo3D<L> {\n\trigidBody: RigidBody;\n\tvelocity: Vector3D;\n}\n\n// ==================== Collision Response ====================\n\n/**\n * Module-level reusable physics3D collision event. Subscribers must consume\n * synchronously — same contract as the shared narrowphase Contact3D.\n */\nconst _physicsCollisionEvent: Physics3DCollisionEvent = {\n\tentityA: 0, entityB: 0, normalX: 0, normalY: 0, normalZ: 0, depth: 0,\n};\n\ninterface PhysicsEcs3DLike {\n\tgetComponent(id: number, name: 'localTransform3D'): { x: number; y: number; z: number } | undefined;\n\teventBus: { publish(event: 'physics3DCollision', data: Physics3DCollisionEvent): void };\n\tmarkChanged(entityId: number, componentName: 'localTransform3D' | 'velocity3D'): void;\n}\n\n/**\n * Resolve a 3D physics collision pair: position correction, impulse response, event.\n *\n * Friction uses a tangent plane projection: the tangential velocity is the\n * component of relative velocity perpendicular to the contact normal. This\n * generalizes the 2D tangent-line approach to 3D — mathematically the same\n * operation with an added Z component.\n */\nfunction resolvePhysicsContact3D(\n\ta: Physics3DColliderInfo,\n\tb: Physics3DColliderInfo,\n\tcontact: Contact3D,\n\tecs: PhysicsEcs3DLike,\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, 'localTransform3D');\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\tltA.z -= corrA * contact.normalZ;\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\ta.z = ltA.z;\n\t\t\tecs.markChanged(a.entityId, 'localTransform3D');\n\t\t}\n\n\t\tif (invMassB > 0) {\n\t\t\tconst ltB = ecs.getComponent(b.entityId, 'localTransform3D');\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\tltB.z += corrB * contact.normalZ;\n\t\t\tb.x = ltB.x;\n\t\t\tb.y = ltB.y;\n\t\t\tb.z = ltB.z;\n\t\t\tecs.markChanged(b.entityId, 'localTransform3D');\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 relVelZ = b.velocity.z - a.velocity.z;\n\t\tconst velAlongNormal = relVelX * contact.normalX + relVelY * contact.normalY + relVelZ * contact.normalZ;\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\t\t\tconst impA = normalImpulse * invMassA;\n\t\t\tconst impB = normalImpulse * invMassB;\n\n\t\t\ta.velocity.x -= impA * contact.normalX;\n\t\t\ta.velocity.y -= impA * contact.normalY;\n\t\t\ta.velocity.z -= impA * contact.normalZ;\n\t\t\tb.velocity.x += impB * contact.normalX;\n\t\t\tb.velocity.y += impB * contact.normalY;\n\t\t\tb.velocity.z += impB * contact.normalZ;\n\n\t\t\t// Friction (tangential impulse — project relative velocity onto tangent plane)\n\t\t\tconst tangentX = relVelX - velAlongNormal * contact.normalX;\n\t\t\tconst tangentY = relVelY - velAlongNormal * contact.normalY;\n\t\t\tconst tangentZ = relVelZ - velAlongNormal * contact.normalZ;\n\t\t\tconst tangentSpeed = Math.sqrt(tangentX * tangentX + tangentY * tangentY + tangentZ * tangentZ);\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 tangentNZ = tangentZ / 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\t\t\t\tconst tanA = tangentImpulse * invMassA;\n\t\t\t\tconst tanB = tangentImpulse * invMassB;\n\n\t\t\t\ta.velocity.x += tanA * tangentNX;\n\t\t\t\ta.velocity.y += tanA * tangentNY;\n\t\t\t\ta.velocity.z += tanA * tangentNZ;\n\t\t\t\tb.velocity.x -= tanB * tangentNX;\n\t\t\t\tb.velocity.y -= tanB * tangentNY;\n\t\t\t\tb.velocity.z -= tanB * tangentNZ;\n\t\t\t}\n\t\t}\n\n\t\tecs.markChanged(a.entityId, 'velocity3D');\n\t\tecs.markChanged(b.entityId, 'velocity3D');\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.normalZ = contact.normalZ;\n\t_physicsCollisionEvent.depth = contact.depth;\n\tecs.eventBus.publish('physics3DCollision', _physicsCollisionEvent);\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a 3D physics plugin for ECSpresso.\n *\n * Provides:\n * - Semi-implicit Euler integration (gravity, forces, drag → velocity3D → position)\n * - Impulse-based collision response with restitution and friction\n * - physics3DCollision events with contact normal and depth\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransform3DPlugin())\n * .withPlugin(createPhysics3DPlugin({ gravity: { x: 0, y: -9.81, z: 0 } }))\n * .withFixedTimestep(1/60)\n * .build();\n *\n * ecs.spawn({\n * ...createTransform3D(0, 10, 0),\n * ...createRigidBody3D('dynamic', { mass: 1, restitution: 0.5 }),\n * velocity3D: { x: 0, y: 0, z: 0 },\n * ...createAABB3DCollider(1, 1, 1),\n * ...createCollisionLayer('player', ['ground']),\n * });\n * ```\n */\n\ntype Physics3DProvides<L extends string = never> = Physics3DOwnComponentTypes & Collision3DComponentTypes<L>;\n\nexport function createPhysics3DPlugin<L extends string = never, G extends string = 'physics3D', CG extends string = never>(\n\toptions?: Physics3DPluginOptions<G, CG> & { layers?: LayerFactories<Record<L, readonly string[]>> },\n) {\n\tconst {\n\t\tgravity = { x: 0, y: 0, z: 0 },\n\t\tsystemGroup = 'physics3D',\n\t\tcollisionSystemGroup,\n\t\tintegrationPriority = 1000,\n\t\tcollisionPriority = 900,\n\t\tphase = 'fixedUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('physics3D')\n\t\t.withComponentTypes<Physics3DProvides<L>>()\n\t\t.withEventTypes<Physics3DEventTypes>()\n\t\t.withResourceTypes<Physics3DResourceTypes>()\n\t\t.withLabels<'physics3D-integration' | 'physics3D-collision'>()\n\t\t.withGroups<G | CG>()\n\t\t.requires<Transform3DWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// rigidBody3D requires velocity3D and force3D — auto-add with zero defaults\n\t\t\tworld.registerRequired('rigidBody3D', 'velocity3D', () => ({ x: 0, y: 0, z: 0 }));\n\t\t\tworld.registerRequired('rigidBody3D', 'force3D', () => ({ x: 0, y: 0, z: 0 }));\n\n\t\t\tworld.addResource('physics3DConfig', { gravity: { x: gravity.x, y: gravity.y, z: gravity.z } });\n\n\t\t\t// ==================== Integration System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('physics3D-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: ['localTransform3D', 'velocity3D', 'rigidBody3D', 'force3D'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tconst { gravity: g } = ecs.getResource('physics3DConfig');\n\t\t\t\t\tconst gx = g.x;\n\t\t\t\t\tconst gy = g.y;\n\t\t\t\t\tconst gz = g.z;\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. Needs collision response to\n\t\t\t\t\t// wake sleepers back up; keep in sync with physics2D when landed.\n\t\t\t\t\tfor (const entity of queries.bodies) {\n\t\t\t\t\t\tconst { localTransform3D, velocity3D, rigidBody3D, force3D } = entity.components;\n\n\t\t\t\t\t\t// Static bodies: skip entirely\n\t\t\t\t\t\tif (rigidBody3D.type === 'static') continue;\n\n\t\t\t\t\t\t// Dynamic bodies: apply gravity, forces, drag\n\t\t\t\t\t\tif (rigidBody3D.type === 'dynamic') {\n\t\t\t\t\t\t\t// 1. Gravity\n\t\t\t\t\t\t\tconst gsdt = rigidBody3D.gravityScale * dt;\n\t\t\t\t\t\t\tvelocity3D.x += gx * gsdt;\n\t\t\t\t\t\t\tvelocity3D.y += gy * gsdt;\n\t\t\t\t\t\t\tvelocity3D.z += gz * 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 = rigidBody3D.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\tvelocity3D.x += force3D.x * invMassDt;\n\t\t\t\t\t\t\t\tvelocity3D.y += force3D.y * invMassDt;\n\t\t\t\t\t\t\t\tvelocity3D.z += force3D.z * 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 (rigidBody3D.drag > 0) {\n\t\t\t\t\t\t\t\tconst damping = Math.max(0, 1 - rigidBody3D.drag * dt);\n\t\t\t\t\t\t\t\tvelocity3D.x *= damping;\n\t\t\t\t\t\t\t\tvelocity3D.y *= damping;\n\t\t\t\t\t\t\t\tvelocity3D.z *= 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\tlocalTransform3D.x += velocity3D.x * dt;\n\t\t\t\t\t\tlocalTransform3D.y += velocity3D.y * dt;\n\t\t\t\t\t\tlocalTransform3D.z += velocity3D.z * dt;\n\n\t\t\t\t\t\t// Clear accumulated forces\n\t\t\t\t\t\tforce3D.x = 0;\n\t\t\t\t\t\tforce3D.y = 0;\n\t\t\t\t\t\tforce3D.z = 0;\n\n\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform3D');\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('physics3D-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 Physics3DColliderInfo slots reused across frames.\n\t\t\t// Steady-state: zero allocations per frame once the pool is warm.\n\t\t\tconst colliderPool: Physics3DColliderInfo<L>[] = [];\n\t\t\t// Reusable entityId → collider lookup for the broadphase path.\n\t\t\tconst broadphaseMap = new Map<number, Physics3DColliderInfo<L>>();\n\t\t\t// Cached spatial index reference (resolved once on first frame).\n\t\t\tlet cachedSI: SpatialIndex3D | undefined;\n\t\t\tlet siResolved = false;\n\n\t\t\tconst ensureSlot = (idx: number, entityId: number, layer: L, collidesWith: readonly L[], rigidBody: RigidBody, velocity: Vector3D): Physics3DColliderInfo<L> => {\n\t\t\t\tlet slot = colliderPool[idx];\n\t\t\t\tif (!slot) {\n\t\t\t\t\tslot = {\n\t\t\t\t\t\tentityId,\n\t\t\t\t\t\tx: 0,\n\t\t\t\t\t\ty: 0,\n\t\t\t\t\t\tz: 0,\n\t\t\t\t\t\tlayer,\n\t\t\t\t\t\tcollidesWith,\n\t\t\t\t\t\tlayerBit: 0,\n\t\t\t\t\t\tcollidesWithMask: 0,\n\t\t\t\t\t\tshape: AABB3D_SHAPE,\n\t\t\t\t\t\thalfWidth: 0,\n\t\t\t\t\t\thalfHeight: 0,\n\t\t\t\t\t\thalfDepth: 0,\n\t\t\t\t\t\tradius: 0,\n\t\t\t\t\t\trigidBody,\n\t\t\t\t\t\tvelocity,\n\t\t\t\t\t};\n\t\t\t\t\tcolliderPool[idx] = slot;\n\t\t\t\t} else {\n\t\t\t\t\tslot.rigidBody = rigidBody;\n\t\t\t\t\tslot.velocity = velocity;\n\t\t\t\t}\n\t\t\t\treturn slot;\n\t\t\t};\n\n\t\t\tcollisionSystem\n\t\t\t\t.addQuery('aabbOnly', {\n\t\t\t\t\twith: ['localTransform3D', 'rigidBody3D', 'velocity3D', 'collisionLayer', 'aabb3DCollider'],\n\t\t\t\t\twithout: ['sphereCollider'],\n\t\t\t\t})\n\t\t\t\t.addQuery('sphereOnly', {\n\t\t\t\t\twith: ['localTransform3D', 'rigidBody3D', 'velocity3D', 'collisionLayer', 'sphereCollider'],\n\t\t\t\t\twithout: ['aabb3DCollider'],\n\t\t\t\t})\n\t\t\t\t.addQuery('both', {\n\t\t\t\t\twith: ['localTransform3D', 'rigidBody3D', 'velocity3D', 'collisionLayer', 'aabb3DCollider', 'sphereCollider'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tlet count = 0;\n\n\t\t\t\t\tfor (const entity of queries.aabbOnly) {\n\t\t\t\t\t\tconst { localTransform3D, rigidBody3D, velocity3D, collisionLayer, aabb3DCollider } = entity.components;\n\t\t\t\t\t\tconst slot = ensureSlot(count, entity.id, collisionLayer.layer, collisionLayer.collidesWith, rigidBody3D, velocity3D);\n\t\t\t\t\t\tif (!fillBaseColliderInfo3D(\n\t\t\t\t\t\t\tslot,\n\t\t\t\t\t\t\tentity.id, localTransform3D.x, localTransform3D.y, localTransform3D.z,\n\t\t\t\t\t\t\tcollisionLayer.layer, collisionLayer.collidesWith,\n\t\t\t\t\t\t\taabb3DCollider, undefined,\n\t\t\t\t\t\t)) continue;\n\t\t\t\t\t\tcount++;\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.sphereOnly) {\n\t\t\t\t\t\tconst { localTransform3D, rigidBody3D, velocity3D, collisionLayer, sphereCollider } = entity.components;\n\t\t\t\t\t\tconst slot = ensureSlot(count, entity.id, collisionLayer.layer, collisionLayer.collidesWith, rigidBody3D, velocity3D);\n\t\t\t\t\t\tif (!fillBaseColliderInfo3D(\n\t\t\t\t\t\t\tslot,\n\t\t\t\t\t\t\tentity.id, localTransform3D.x, localTransform3D.y, localTransform3D.z,\n\t\t\t\t\t\t\tcollisionLayer.layer, collisionLayer.collidesWith,\n\t\t\t\t\t\t\tundefined, sphereCollider,\n\t\t\t\t\t\t)) continue;\n\t\t\t\t\t\tcount++;\n\t\t\t\t\t}\n\n\t\t\t\t\t// fillBaseColliderInfo3D picks aabb when both present — preserves prior precedence.\n\t\t\t\t\tfor (const entity of queries.both) {\n\t\t\t\t\t\tconst { localTransform3D, rigidBody3D, velocity3D, collisionLayer, aabb3DCollider, sphereCollider } = entity.components;\n\t\t\t\t\t\tconst slot = ensureSlot(count, entity.id, collisionLayer.layer, collisionLayer.collidesWith, rigidBody3D, velocity3D);\n\t\t\t\t\t\tif (!fillBaseColliderInfo3D(\n\t\t\t\t\t\t\tslot,\n\t\t\t\t\t\t\tentity.id, localTransform3D.x, localTransform3D.y, localTransform3D.z,\n\t\t\t\t\t\t\tcollisionLayer.layer, collisionLayer.collidesWith,\n\t\t\t\t\t\t\taabb3DCollider, sphereCollider,\n\t\t\t\t\t\t)) continue;\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<SpatialIndex3D>('spatialIndex3D');\n\t\t\t\t\t\tsiResolved = true;\n\t\t\t\t\t}\n\t\t\t\t\tdetectCollisions3D(colliderPool, count, broadphaseMap, cachedSI, resolvePhysicsContact3D, ecs);\n\t\t\t\t});\n\t\t});\n}\n",
6
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
7
  "/**\n * Shared Narrowphase Module — 3D\n *\n * Provides contact-computing narrowphase tests and a generic collision\n * iteration pipeline for 3D collider pairs (AABB3D + Sphere).\n *\n * Mirrors the 2D narrowphase (`narrowphase.ts`) with an added Z axis.\n */\n\nimport type { SpatialIndex3D } from './spatial-hash3D';\nimport { createLayerBitRegistry } from './layer-bit-registry';\n\n// ==================== Contact3D ====================\n\n/**\n * Contact result from a 3D 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 `detectCollisions3D` receives a shared module-\n * level instance — **subscribers must consume it synchronously and must not\n * retain the reference across frames**.\n */\nexport interface Contact3D {\n\tnormalX: number;\n\tnormalY: number;\n\tnormalZ: number;\n\t/** Penetration depth (positive = overlapping) */\n\tdepth: number;\n}\n\n/**\n * Module-level reusable Contact3D passed down from `detectCollisions3D` 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: Contact3D = { normalX: 0, normalY: 0, normalZ: 0, depth: 0 };\n\n// ==================== BaseColliderInfo3D ====================\n\n/** Collider shape discriminator for the flattened BaseColliderInfo3D layout. */\nexport const AABB3D_SHAPE = 0;\nexport const SPHERE_SHAPE = 1;\nexport type ColliderShape3D = typeof AABB3D_SHAPE | typeof SPHERE_SHAPE;\n\n/**\n * Minimum collider data shared by 3D collision and physics bundles.\n *\n * Flat layout (no nested sub-objects): the `shape` discriminator tells you\n * whether to read `halfWidth`/`halfHeight`/`halfDepth` (AABB3D) or `radius`\n * (Sphere). Unused fields are set to 0.\n *\n * Pool-friendly — all fields are assigned in place each frame.\n */\nexport interface BaseColliderInfo3D<L extends string = string> {\n\tentityId: number;\n\tx: number;\n\ty: number;\n\tz: 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 * `fillBaseColliderInfo3D`. 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 `getLayerBit3D` for every entry in `collidesWith`. */\n\tcollidesWithMask: number;\n\tshape: ColliderShape3D;\n\thalfWidth: number;\n\thalfHeight: number;\n\thalfDepth: number;\n\tradius: number;\n}\n\n// ==================== Layer Bit Registry ====================\n\n// Independent from the 2D registry: 2D and 3D layer namespaces are\n// defined separately by user code, so bits should not collide.\nconst _layerRegistry = createLayerBitRegistry('3D collision');\nexport const getLayerBit3D = _layerRegistry.getLayerBit;\nexport const getCollidesWithMask3D = _layerRegistry.getCollidesWithMask;\n\n// ==================== Collider Construction ====================\n\n/**\n * Populate a `BaseColliderInfo3D` 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 AABB3D and sphere colliders, AABB3D wins and only\n * the AABB3D offset is applied.\n */\nexport function fillBaseColliderInfo3D<L extends string>(\n\tinfo: BaseColliderInfo3D<L>,\n\tentityId: number,\n\tx: number,\n\ty: number,\n\tz: number,\n\tlayer: L,\n\tcollidesWith: readonly L[],\n\taabb3D: { width: number; height: number; depth: number; offsetX?: number; offsetY?: number; offsetZ?: number } | undefined,\n\tsphere: { radius: number; offsetX?: number; offsetY?: number; offsetZ?: number } | undefined,\n): boolean {\n\tinfo.entityId = entityId;\n\tinfo.layer = layer;\n\tinfo.collidesWith = collidesWith;\n\tinfo.layerBit = getLayerBit3D(layer);\n\tinfo.collidesWithMask = getCollidesWithMask3D(collidesWith);\n\n\tif (aabb3D) {\n\t\tinfo.x = x + (aabb3D.offsetX ?? 0);\n\t\tinfo.y = y + (aabb3D.offsetY ?? 0);\n\t\tinfo.z = z + (aabb3D.offsetZ ?? 0);\n\t\tinfo.shape = AABB3D_SHAPE;\n\t\tinfo.halfWidth = aabb3D.width / 2;\n\t\tinfo.halfHeight = aabb3D.height / 2;\n\t\tinfo.halfDepth = aabb3D.depth / 2;\n\t\tinfo.radius = 0;\n\t\treturn true;\n\t}\n\n\tif (sphere) {\n\t\tinfo.x = x + (sphere.offsetX ?? 0);\n\t\tinfo.y = y + (sphere.offsetY ?? 0);\n\t\tinfo.z = z + (sphere.offsetZ ?? 0);\n\t\tinfo.shape = SPHERE_SHAPE;\n\t\tinfo.halfWidth = 0;\n\t\tinfo.halfHeight = 0;\n\t\tinfo.halfDepth = 0;\n\t\tinfo.radius = sphere.radius;\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\n// ==================== Spatial Index Lookup ====================\n\n/**\n * Retrieve the optional spatialIndex3D resource, returning undefined when absent.\n * Centralizes the cross-plugin typed lookup so individual plugins don't each\n * need to import SpatialIndex3D or repeat the tryGetResource pattern.\n */\nexport function tryGetSpatialIndex3D(\n\ttryGetResource: <T>(key: string) => T | undefined,\n): SpatialIndex3D | undefined {\n\treturn tryGetResource<SpatialIndex3D>('spatialIndex3D');\n}\n\n// ==================== Narrowphase Tests ====================\n\n/**\n * Write an AABB3D-vs-AABB3D contact into `out`. Returns `true` if the\n * shapes overlap (out was filled), `false` otherwise.\n *\n * Resolves along the axis with minimum penetration depth.\n */\nexport function computeAABB3DvsAABB3D(\n\tax: number, ay: number, az: number, ahw: number, ahh: number, ahd: number,\n\tbx: number, by: number, bz: number, bhw: number, bhh: number, bhd: number,\n\tout: Contact3D,\n): boolean {\n\tconst dx = bx - ax;\n\tconst dy = by - ay;\n\tconst dz = bz - az;\n\tconst overlapX = (ahw + bhw) - Math.abs(dx);\n\tconst overlapY = (ahh + bhh) - Math.abs(dy);\n\tconst overlapZ = (ahd + bhd) - Math.abs(dz);\n\n\tif (overlapX <= 0 || overlapY <= 0 || overlapZ <= 0) return false;\n\n\tif (overlapX <= overlapY && overlapX <= overlapZ) {\n\t\tout.normalX = dx >= 0 ? 1 : -1;\n\t\tout.normalY = 0;\n\t\tout.normalZ = 0;\n\t\tout.depth = overlapX;\n\t\treturn true;\n\t}\n\n\tif (overlapY <= overlapZ) {\n\t\tout.normalX = 0;\n\t\tout.normalY = dy >= 0 ? 1 : -1;\n\t\tout.normalZ = 0;\n\t\tout.depth = overlapY;\n\t\treturn true;\n\t}\n\n\tout.normalX = 0;\n\tout.normalY = 0;\n\tout.normalZ = dz >= 0 ? 1 : -1;\n\tout.depth = overlapZ;\n\treturn true;\n}\n\n/**\n * Write a sphere-vs-sphere contact into `out`. Returns `true` if the\n * spheres overlap.\n */\nexport function computeSphereVsSphere(\n\tax: number, ay: number, az: number, ar: number,\n\tbx: number, by: number, bz: number, br: number,\n\tout: Contact3D,\n): boolean {\n\tconst dx = bx - ax;\n\tconst dy = by - ay;\n\tconst dz = bz - az;\n\tconst distSq = dx * dx + dy * dy + dz * dz;\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.normalZ = 0;\n\t\tout.depth = radiusSum;\n\t\treturn true;\n\t}\n\tout.normalX = dx / dist;\n\tout.normalY = dy / dist;\n\tout.normalZ = dz / dist;\n\tout.depth = radiusSum - dist;\n\treturn true;\n}\n\n/**\n * Write an AABB3D-vs-sphere contact into `out`. Returns `true` if the\n * shapes overlap.\n *\n * Uses closest-point-on-AABB to sphere center. When the sphere center\n * is inside the AABB, resolves along the axis with minimum push distance.\n */\nexport function computeAABB3DvsSphere(\n\taabbX: number, aabbY: number, aabbZ: number, ahw: number, ahh: number, ahd: number,\n\tsphereX: number, sphereY: number, sphereZ: number, radius: number,\n\tout: Contact3D,\n): boolean {\n\tconst closestX = Math.max(aabbX - ahw, Math.min(sphereX, aabbX + ahw));\n\tconst closestY = Math.max(aabbY - ahh, Math.min(sphereY, aabbY + ahh));\n\tconst closestZ = Math.max(aabbZ - ahd, Math.min(sphereZ, aabbZ + ahd));\n\n\tconst dx = sphereX - closestX;\n\tconst dy = sphereY - closestY;\n\tconst dz = sphereZ - closestZ;\n\tconst distSq = dx * dx + dy * dy + dz * dz;\n\n\tif (distSq >= radius * radius) return false;\n\n\t// Sphere center inside AABB — resolve along minimum push axis\n\tif (distSq === 0) {\n\t\tconst pushLeft = (sphereX - (aabbX - ahw));\n\t\tconst pushRight = ((aabbX + ahw) - sphereX);\n\t\tconst pushUp = (sphereY - (aabbY - ahh));\n\t\tconst pushDown = ((aabbY + ahh) - sphereY);\n\t\tconst pushFront = (sphereZ - (aabbZ - ahd));\n\t\tconst pushBack = ((aabbZ + ahd) - sphereZ);\n\t\tconst minPush = Math.min(pushLeft, pushRight, pushUp, pushDown, pushFront, pushBack);\n\n\t\tif (minPush === pushRight) {\n\t\t\tout.normalX = 1; out.normalY = 0; out.normalZ = 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.normalZ = 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.normalZ = 0; out.depth = pushDown + radius;\n\t\t\treturn true;\n\t\t}\n\t\tif (minPush === pushUp) {\n\t\t\tout.normalX = 0; out.normalY = -1; out.normalZ = 0; out.depth = pushUp + radius;\n\t\t\treturn true;\n\t\t}\n\t\tif (minPush === pushBack) {\n\t\t\tout.normalX = 0; out.normalY = 0; out.normalZ = 1; out.depth = pushBack + radius;\n\t\t\treturn true;\n\t\t}\n\t\tout.normalX = 0; out.normalY = 0; out.normalZ = -1; out.depth = pushFront + 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.normalZ = dz / 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 computeContact3D(a: BaseColliderInfo3D, b: BaseColliderInfo3D, out: Contact3D): boolean {\n\tif (a.shape === AABB3D_SHAPE && b.shape === AABB3D_SHAPE) {\n\t\treturn computeAABB3DvsAABB3D(\n\t\t\ta.x, a.y, a.z, a.halfWidth, a.halfHeight, a.halfDepth,\n\t\t\tb.x, b.y, b.z, b.halfWidth, b.halfHeight, b.halfDepth,\n\t\t\tout,\n\t\t);\n\t}\n\n\tif (a.shape === SPHERE_SHAPE && b.shape === SPHERE_SHAPE) {\n\t\treturn computeSphereVsSphere(\n\t\t\ta.x, a.y, a.z, a.radius,\n\t\t\tb.x, b.y, b.z, b.radius,\n\t\t\tout,\n\t\t);\n\t}\n\n\tif (a.shape === AABB3D_SHAPE && b.shape === SPHERE_SHAPE) {\n\t\treturn computeAABB3DvsSphere(\n\t\t\ta.x, a.y, a.z, a.halfWidth, a.halfHeight, a.halfDepth,\n\t\t\tb.x, b.y, b.z, b.radius,\n\t\t\tout,\n\t\t);\n\t}\n\n\t// a is Sphere, b is AABB3D — compute as AABB3D-vs-Sphere, then flip normal\n\tif (!computeAABB3DvsSphere(\n\t\tb.x, b.y, b.z, b.halfWidth, b.halfHeight, b.halfDepth,\n\t\ta.x, a.y, a.z, a.radius,\n\t\tout,\n\t)) return false;\n\tout.normalX = -out.normalX;\n\tout.normalY = -out.normalY;\n\tout.normalZ = -out.normalZ;\n\treturn true;\n}\n\n// ==================== Collision Iteration ====================\n\n/** Module-level reusable array for broadphase candidates. */\nconst _broadphaseCandidates: number[] = [];\n\nlet _bruteForceWarned = false;\nconst BRUTE_FORCE_WARN_THRESHOLD = 50;\n\n/**\n * Generic 3D 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.\n *\n * Uses a context parameter forwarded to the callback to avoid\n * per-frame closure allocation.\n */\nexport function detectCollisions3D<I extends BaseColliderInfo3D, C>(\n\tcolliders: I[],\n\tcount: number,\n\tworkingMap: Map<number, I>,\n\tspatialIndex: SpatialIndex3D | undefined,\n\tonContact: (a: I, b: I, contact: Contact3D, 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 BaseColliderInfo3D, C>(\n\tcolliders: I[],\n\tcount: number,\n\tonContact: (a: I, b: I, contact: Contact3D, 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] 3D collision detection is using O(n²) brute force with ${count} colliders. ` +\n\t\t\t`For better performance, install createSpatialIndex3DPlugin() alongside your collision or physics3D 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 (!computeContact3D(a, b, _sharedContact)) continue;\n\n\t\t\tonContact(a, b, _sharedContact, context);\n\t\t}\n\t}\n}\n\nfunction broadphaseDetect<I extends BaseColliderInfo3D, C>(\n\tcolliders: I[],\n\tcount: number,\n\tcolliderMap: Map<number, I>,\n\tspatialIndex: SpatialIndex3D,\n\tonContact: (a: I, b: I, contact: Contact3D, 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 === AABB3D_SHAPE ? a.halfWidth : a.radius;\n\t\tconst aHalfH = a.shape === AABB3D_SHAPE ? a.halfHeight : a.radius;\n\t\tconst aHalfD = a.shape === AABB3D_SHAPE ? a.halfDepth : a.radius;\n\n\t\t_broadphaseCandidates.length = 0;\n\t\tspatialIndex.queryBoxInto(\n\t\t\ta.x - aHalfW, a.y - aHalfH, a.z - aHalfD,\n\t\t\ta.x + aHalfW, a.y + aHalfH, a.z + aHalfD,\n\t\t\t_broadphaseCandidates,\n\t\t\ta.entityId,\n\t\t);\n\n\t\tfor (const bId of _broadphaseCandidates) {\n\t\t\tconst b = colliderMap.get(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 (!computeContact3D(a, b, _sharedContact)) continue;\n\n\t\t\tonContact(a, b, _sharedContact, context);\n\t\t}\n\t}\n}\n"
8
8
  ],
9
- "mappings": "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",
10
- "debugId": "6C1C37A7CF5B331564756E2164756E21",
9
+ "mappings": "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",
10
+ "debugId": "FABAD286A305821E64756E2164756E21",
11
11
  "names": []
12
12
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { type BasePluginOptions } from 'ecspresso';
12
12
  import type { BaseWorld } from 'ecspresso';
13
+ import type { Spritesheet, SpritesheetData, TextureSource } from 'pixi.js';
13
14
  /** BaseWorld narrowed to sprite-animation components for typed access in helpers. */
14
15
  type SpriteAnimationWorld = BaseWorld<SpriteAnimationComponentTypes>;
15
16
  export type AnimationLoopMode = 'once' | 'loop' | 'pingPong';
@@ -146,4 +147,113 @@ export declare function resumeAnimation(ecs: SpriteAnimationWorld, entityId: num
146
147
  * - Change detection via markChanged
147
148
  */
148
149
  export declare function createSpriteAnimationPlugin<G extends string = 'spriteAnimation'>(options?: SpriteAnimationPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, SpriteAnimationComponentTypes<string>>, import("ecspresso").EmptyConfig, "sprite-animation-update", G, never, never>;
150
+ /**
151
+ * Per-clip timing/loop overrides keyed by animation name. Each entry tweaks
152
+ * a single clip; omitted entries fall back to the top-level defaults.
153
+ */
154
+ export type SheetClipOverrides<A extends string> = {
155
+ readonly [K in A]?: Omit<SpriteAnimationClipInput, 'frames'>;
156
+ };
157
+ /**
158
+ * Extract the animation-name union from a typed SpritesheetData. Falls back
159
+ * to `string` for untyped sheets.
160
+ */
161
+ export type SheetAnimationKeys<S extends SpritesheetData> = S extends {
162
+ animations: infer A;
163
+ } ? A extends Record<infer K, unknown> ? K extends string ? K : never : string : string;
164
+ /**
165
+ * Build a clip from a named animation in a loaded PixiJS Spritesheet.
166
+ *
167
+ * @example
168
+ * const sheet = await Assets.load<Spritesheet>('/hero.json');
169
+ * const idle = clipFromSheet(sheet, 'idle', { frameDuration: 1 / 12 });
170
+ */
171
+ export declare function clipFromSheet(sheet: Spritesheet, animationName: string, options?: Omit<SpriteAnimationClipInput, 'frames'>): SpriteAnimationClip;
172
+ /**
173
+ * Build an animation set from every named animation in a PixiJS Spritesheet.
174
+ * When the sheet is typed as `Spritesheet<MyData>`, animation names and
175
+ * `defaultClip` / `perClip` keys are inferred at compile time.
176
+ *
177
+ * @example
178
+ * const sheet = ecs.assets.get('hero'); // Spritesheet<HeroData>
179
+ * const set = animationSetFromSheet('hero', sheet, {
180
+ * defaultClip: 'idle',
181
+ * frameDuration: 1 / 12,
182
+ * perClip: { attack: { loop: 'once' } },
183
+ * });
184
+ */
185
+ export declare function animationSetFromSheet<S extends SpritesheetData = SpritesheetData>(id: string, sheet: Spritesheet<S>, options?: {
186
+ defaultClip?: SheetAnimationKeys<S>;
187
+ frameDuration?: number;
188
+ loop?: AnimationLoopMode;
189
+ perClip?: SheetClipOverrides<SheetAnimationKeys<S>>;
190
+ }): SpriteAnimationSet<SheetAnimationKeys<S>>;
191
+ /**
192
+ * Slice a grid-arranged sprite sheet into a clip. Use when you don't have a
193
+ * TexturePacker JSON — just an image and uniform cell dimensions. Cells are
194
+ * walked row-major.
195
+ *
196
+ * Specify exactly one of `rows`, `count`, or `indices` to define the cell set
197
+ * (along with `columns`). Combining `count` and `indices` is rejected.
198
+ *
199
+ * Returns a `Promise` because pixi.js is imported lazily — keeps the static
200
+ * module graph free of a runtime pixi dependency for consumers who only use
201
+ * the sheet-based helpers.
202
+ *
203
+ * @example
204
+ * const tex = await Assets.load<Texture>('/coin.png');
205
+ * const clip = await clipFromGrid({
206
+ * source: tex.source,
207
+ * frameWidth: 16, frameHeight: 16,
208
+ * columns: 8, count: 8,
209
+ * frameDuration: 1 / 10,
210
+ * });
211
+ */
212
+ export declare function clipFromGrid(input: {
213
+ source: TextureSource;
214
+ frameWidth: number;
215
+ frameHeight: number;
216
+ columns: number;
217
+ rows?: number;
218
+ /** Explicit row-major, 0-based cell indices. Mutually exclusive with `count`. */
219
+ indices?: readonly number[];
220
+ /** Number of cells to use, walked row-major. Mutually exclusive with `indices`. */
221
+ count?: number;
222
+ /** Pixels between cells. */
223
+ spacing?: number;
224
+ /** Pixels around the sheet edge. */
225
+ margin?: number;
226
+ frameDuration?: number;
227
+ frameDurations?: readonly number[];
228
+ loop?: AnimationLoopMode;
229
+ }): Promise<SpriteAnimationClip>;
230
+ /**
231
+ * Build an asset-manager-compatible loader for a PixiJS spritesheet atlas
232
+ * (TexturePacker JSON, etc.). Returns the fully-parsed `Spritesheet` object
233
+ * with `.animations` and `.textures` populated.
234
+ *
235
+ * The loader performs a runtime shape check on the resolved value — `Assets.load<T>`
236
+ * is purely nominal in PixiJS, so pointing this at a non-atlas URL would
237
+ * otherwise surface as a misleading 'animation not found' error deep in
238
+ * `clipFromSheet`/`animationSetFromSheet`. The shape check turns that into a
239
+ * load-time error with a clear message.
240
+ *
241
+ * To get literal animation-name inference, declare `S` as an
242
+ * `interface ... extends SpritesheetData` (a `type` alias re-widens via
243
+ * `SpritesheetData.animations`'s `Dict<string[]>` string index signature).
244
+ *
245
+ * @example
246
+ * interface HeroData extends SpritesheetData {
247
+ * animations: { idle: string[]; walk: string[]; attack: string[] };
248
+ * }
249
+ *
250
+ * ecs.builder.withAssets(a => a
251
+ * .add('hero', spritesheetLoader<HeroData>('/hero.json'))
252
+ * );
253
+ *
254
+ * // Later:
255
+ * const sheet = ecs.assets.get('hero'); // Spritesheet<HeroData>
256
+ * const set = animationSetFromSheet('hero', sheet); // names inferred
257
+ */
258
+ export declare function spritesheetLoader<S extends SpritesheetData = SpritesheetData>(url: string): () => Promise<Spritesheet<S>>;
149
259
  export {};
@@ -1,4 +1,4 @@
1
- var G=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(z,E)=>(typeof require<"u"?require:z)[E]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as Z}from"ecspresso";function Y(j){return Object.freeze({frames:Object.freeze([...j.frames]),frameDuration:j.frameDuration??0.1,frameDurations:j.frameDurations?Object.freeze([...j.frameDurations]):null,loop:j.loop??"loop"})}function U(j,z){return Object.freeze({id:j,clips:Object.freeze({default:Y(z)}),defaultClip:"default"})}function q(j,z,E){let J={},H=Object.keys(z);for(let N of H)J[N]=Y(z[N]);let M=H[0];if(!M)throw Error("defineSpriteAnimations: clips object must have at least one key");return Object.freeze({id:j,clips:Object.freeze(J),defaultClip:E?.defaultClip??M})}function D(j,z){let E=z?.initial??j.defaultClip;return{spriteAnimation:{set:j,current:E,currentFrame:0,elapsed:0,playing:!0,speed:z?.speed??1,direction:1,totalLoops:z?.totalLoops??-1,completedLoops:0,justFinished:!1,onComplete:z?.onComplete}}}function K(j,z,E,J){let H=j.getComponent(z,"spriteAnimation");if(!H)return!1;if(!(E in H.set.clips))return!1;if(E!==H.current||J?.restart===!0)H.current=E,H.currentFrame=0,H.elapsed=0,H.direction=1,H.completedLoops=0,H.justFinished=!1;if(H.playing=!0,J?.speed!==void 0)H.speed=J.speed;return j.markChanged(z,"spriteAnimation"),!0}function S(j,z){let E=j.getComponent(z,"spriteAnimation");if(!E)return!1;return E.playing=!1,!0}function A(j,z){let E=j.getComponent(z,"spriteAnimation");if(!E)return!1;return E.playing=!0,!0}function W(j,z,E){j.playing=!1,j.justFinished=!0,j.onComplete?.({entityId:z,animation:j.current}),E.commands.removeComponent(z,"spriteAnimation")}function _(j,z,E,J){if(j.completedLoops++,z.loop==="once")return W(j,E,J),!1;if(j.totalLoops>0&&j.completedLoops>=j.totalLoops)return W(j,E,J),!1;if(z.loop==="pingPong")return j.direction=j.direction===1?-1:1,j.currentFrame+=j.direction,j.elapsed>0;return j.currentFrame=0,j.elapsed>0}function X(j,z,E,J){let H=j.currentFrame+j.direction;if(H>=z.frames.length||H<0)return _(j,z,E,J);return j.currentFrame=H,!0}function $(j,z,E,J){while(!0){let H=z.frameDurations!==null?z.frameDurations[j.currentFrame]??z.frameDuration:z.frameDuration;if(H<=0){if(!X(j,z,E,J))return;continue}let M=H-j.elapsed;if(M>0.000001)return;if(j.elapsed=M<0?-M:0,!X(j,z,E,J))return}}function b(j){let{systemGroup:z="spriteAnimation",priority:E=0,phase:J="update"}=j??{};return Z("spriteAnimation").withComponentTypes().withLabels().withGroups().install((H)=>{H.addSystem("sprite-animation-update").setPriority(E).inPhase(J).inGroup(z).addQuery("animations",{with:["spriteAnimation"]}).setProcess(({queries:M,dt:N,ecs:V})=>{for(let O of M.animations){let L=O.components.spriteAnimation,Q=L.set.clips[L.current];if(!Q)continue;if(L.justFinished){L.justFinished=!1;continue}if(!L.playing)continue;if(Q.frames.length<=1)continue;let R=L.currentFrame;if(L.elapsed+=N*L.speed,$(L,Q,O.id,V),L.currentFrame!==R||R===0)B(O.components,L,Q);if(L.currentFrame!==R)V.markChanged(O.id,"spriteAnimation")}})})}function B(j,z,E){let J=j.sprite;if(J&&typeof J==="object"&&"texture"in J)J.texture=E.frames[z.currentFrame]}export{S as stopAnimation,A as resumeAnimation,K as playAnimation,q as defineSpriteAnimations,U as defineSpriteAnimation,b as createSpriteAnimationPlugin,D as createSpriteAnimation};
1
+ var S=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(J,L)=>(typeof require<"u"?require:J)[L]}):z)(function(z){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+z+'" is not supported')});import{definePlugin as A}from"ecspresso";function G(z){return Object.freeze({frames:Object.freeze([...z.frames]),frameDuration:z.frameDuration??0.1,frameDurations:z.frameDurations?Object.freeze([...z.frameDurations]):null,loop:z.loop??"loop"})}function v(z,J){return Object.freeze({id:z,clips:Object.freeze({default:G(J)}),defaultClip:"default"})}function f(z,J,L){let O={},M=Object.keys(J);for(let Y of M)O[Y]=G(J[Y]);let Q=M[0];if(!Q)throw Error("defineSpriteAnimations: clips object must have at least one key");return Object.freeze({id:z,clips:Object.freeze(O),defaultClip:L?.defaultClip??Q})}function x(z,J){let L=J?.initial??z.defaultClip;return{spriteAnimation:{set:z,current:L,currentFrame:0,elapsed:0,playing:!0,speed:J?.speed??1,direction:1,totalLoops:J?.totalLoops??-1,completedLoops:0,justFinished:!1,onComplete:J?.onComplete}}}function g(z,J,L,O){let M=z.getComponent(J,"spriteAnimation");if(!M)return!1;if(!(L in M.set.clips))return!1;if(L!==M.current||O?.restart===!0)M.current=L,M.currentFrame=0,M.elapsed=0,M.direction=1,M.completedLoops=0,M.justFinished=!1;if(M.playing=!0,O?.speed!==void 0)M.speed=O.speed;return z.markChanged(J,"spriteAnimation"),!0}function F(z,J){let L=z.getComponent(J,"spriteAnimation");if(!L)return!1;return L.playing=!1,!0}function I(z,J){let L=z.getComponent(J,"spriteAnimation");if(!L)return!1;return L.playing=!0,!0}function j(z,J,L){z.playing=!1,z.justFinished=!0,z.onComplete?.({entityId:J,animation:z.current}),L.commands.removeComponent(J,"spriteAnimation")}function N(z,J,L,O){if(z.completedLoops++,J.loop==="once")return j(z,L,O),!1;if(z.totalLoops>0&&z.completedLoops>=z.totalLoops)return j(z,L,O),!1;if(J.loop==="pingPong")return z.direction=z.direction===1?-1:1,z.currentFrame+=z.direction,z.elapsed>0;return z.currentFrame=0,z.elapsed>0}function q(z,J,L,O){let M=z.currentFrame+z.direction;if(M>=J.frames.length||M<0)return N(z,J,L,O);return z.currentFrame=M,!0}function T(z,J,L,O){while(!0){let M=J.frameDurations!==null?J.frameDurations[z.currentFrame]??J.frameDuration:J.frameDuration;if(M<=0){if(!q(z,J,L,O))return;continue}let Q=M-z.elapsed;if(Q>0.000001)return;if(z.elapsed=Q<0?-Q:0,!q(z,J,L,O))return}}function h(z){let{systemGroup:J="spriteAnimation",priority:L=0,phase:O="update"}=z??{};return A("spriteAnimation").withComponentTypes().withLabels().withGroups().install((M)=>{M.addSystem("sprite-animation-update").setPriority(L).inPhase(O).inGroup(J).addQuery("animations",{with:["spriteAnimation"]}).setProcess(({queries:Q,dt:Y,ecs:V})=>{for(let X of Q.animations){let U=X.components.spriteAnimation,Z=U.set.clips[U.current];if(!Z)continue;if(U.justFinished){U.justFinished=!1;continue}if(!U.playing)continue;if(Z.frames.length<=1)continue;let $=U.currentFrame;if(U.elapsed+=Y*U.speed,T(U,Z,X.id,V),U.currentFrame!==$||$===0)b(X.components,U,Z);if(U.currentFrame!==$)V.markChanged(X.id,"spriteAnimation")}})})}function b(z,J,L){let O=z.sprite;if(O&&typeof O==="object"&&"texture"in O)O.texture=L.frames[J.currentFrame]}function y(z,J,L){let O=z.animations[J];if(!O||O.length===0){let M=Object.keys(z.animations).join(", ")||"(none)";throw Error(`clipFromSheet: animation "${J}" not found on sheet (or has no frames). Available: ${M}`)}return G({...L,frames:O})}function u(z,J,L){let O=Object.entries(J.animations),M=O[0];if(!M)throw Error(`animationSetFromSheet: sheet "${z}" has no animations defined`);let Q=O.reduce((Y,[V,X])=>{if(!Array.isArray(X)||X.length===0)throw Error(`animationSetFromSheet: animation "${String(V)}" on sheet "${z}" has no frames (got ${X===null?"null":Array.isArray(X)?"empty array":typeof X})`);let U=L?.perClip?.[V];return Y[V]=G({frames:X,frameDuration:U?.frameDuration??L?.frameDuration,frameDurations:U?.frameDurations,loop:U?.loop??L?.loop}),Y},{});return Object.freeze({id:z,clips:Object.freeze(Q),defaultClip:L?.defaultClip??M[0]})}async function d(z){let{source:J,frameWidth:L,frameHeight:O,columns:M,rows:Q,indices:Y,count:V,spacing:X=0,margin:U=0}=z;if(!J)throw Error("clipFromGrid: source is required");if(!Number.isInteger(M)||M<=0)throw Error(`clipFromGrid: columns must be a positive integer, got ${M}`);if(Q!==void 0&&(!Number.isInteger(Q)||Q<=0))throw Error(`clipFromGrid: rows must be a positive integer, got ${Q}`);if(V!==void 0&&(!Number.isInteger(V)||V<0))throw Error(`clipFromGrid: count must be a non-negative integer, got ${V}`);if(Y!==void 0&&V!==void 0)throw Error("clipFromGrid: pass either 'indices' or 'count', not both");if(Y===void 0&&V===void 0&&Q===void 0)throw Error("clipFromGrid: specify 'rows', 'count', or 'indices' to define the cell set (only 'columns' is ambiguous)");let Z=Q!==void 0?M*Q:void 0,$=Y??Array.from({length:Z!==void 0&&V!==void 0?Math.min(V,Z):V??Z??0},(_,P)=>P);if($.length===0)throw Error("clipFromGrid: resolved to zero cells (empty indices array or count: 0)");let B=Z??1/0,R=$.find((_)=>!Number.isInteger(_)||_<0||_>=B);if(R!==void 0){let _=Z!==void 0?`[0, ${Z})`:"[0, ∞) — pass 'rows' to enable upper-bound checking";throw Error(`clipFromGrid: invalid cell index ${R}; expected integer in ${_}`)}let{Texture:D,Rectangle:E}=await import("pixi.js"),H=$.map((_)=>{let P=_%M,K=Math.floor(_/M),W=U+P*(L+X),k=U+K*(O+X);return new D({source:J,frame:new E(W,k,L,O)})});return G({frames:H,frameDuration:z.frameDuration,frameDurations:z.frameDurations,loop:z.loop})}function l(z){return async()=>{let{Assets:J}=await import("pixi.js"),L=await J.load(z);if(!L||typeof L!=="object"||typeof L.animations!=="object"||L.animations===null||typeof L.textures!=="object"||L.textures===null)throw Error(`spritesheetLoader: resource at "${z}" did not resolve to a Spritesheet (expected non-null 'animations' and 'textures' objects). Check that the URL points to a TexturePacker-style JSON atlas, not a raw image.`);return L}}export{F as stopAnimation,l as spritesheetLoader,I as resumeAnimation,g as playAnimation,f as defineSpriteAnimations,v as defineSpriteAnimation,h as createSpriteAnimationPlugin,x as createSpriteAnimation,y as clipFromSheet,d as clipFromGrid,u as animationSetFromSheet};
2
2
 
3
- //# debugId=9EF329142BBA6E6864756E2164756E21
3
+ //# debugId=288D71B236D04EC264756E2164756E21
4
4
  //# sourceMappingURL=sprite-animation.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/rendering/sprite-animation.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Sprite Animation Plugin for ECSpresso\n *\n * ECS-native frame-based sprite animation. Advances through spritesheet frames,\n * handles loop modes (once, loop, pingPong), publishes completion events, and\n * syncs the current frame's texture to the PixiJS Sprite via structural access.\n *\n * Renderer2D is a required dependency — the `sprite` component comes from that plugin.\n * This plugin declares only `spriteAnimation` as its component type.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\n\n/** BaseWorld narrowed to sprite-animation components for typed access in helpers. */\ntype SpriteAnimationWorld = BaseWorld<SpriteAnimationComponentTypes>;\n\n// ==================== Loop Mode ====================\n\nexport type AnimationLoopMode = 'once' | 'loop' | 'pingPong';\n\n// ==================== Clip Types ====================\n\n/**\n * A single animation clip: an ordered sequence of texture frames with timing.\n * Immutable and shared across entities.\n */\nexport interface SpriteAnimationClip {\n\treadonly frames: readonly unknown[];\n\treadonly frameDuration: number;\n\treadonly frameDurations: readonly number[] | null;\n\treadonly loop: AnimationLoopMode;\n}\n\n/**\n * Input format for defining a clip. Accepts either uniform or per-frame timing.\n */\nexport interface SpriteAnimationClipInput {\n\t/** Array of PixiJS Texture objects */\n\tframes: readonly unknown[];\n\t/** Uniform seconds-per-frame (used when frameDurations is not provided) */\n\tframeDuration?: number;\n\t/** Per-frame durations in seconds (overrides frameDuration) */\n\tframeDurations?: readonly number[];\n\t/** Loop mode (default: 'loop') */\n\tloop?: AnimationLoopMode;\n}\n\n// ==================== Animation Set ====================\n\n/**\n * A named collection of animation clips. Immutable and shared across entities.\n * Parameterized by A (animation name union) for compile-time validation.\n */\nexport interface SpriteAnimationSet<A extends string = string> {\n\treadonly id: string;\n\treadonly clips: { readonly [K in A]: SpriteAnimationClip };\n\treadonly defaultClip: A;\n}\n\n// ==================== Component ====================\n\n/**\n * Per-entity runtime animation state.\n */\nexport interface SpriteAnimation<A extends string = string> {\n\treadonly set: SpriteAnimationSet<A>;\n\tcurrent: A;\n\tcurrentFrame: number;\n\telapsed: number;\n\tplaying: boolean;\n\tspeed: number;\n\tdirection: 1 | -1;\n\ttotalLoops: number;\n\tcompletedLoops: number;\n\tjustFinished: boolean;\n\tonComplete?: (data: SpriteAnimationEventData) => void;\n}\n\n/**\n * Component types provided by the sprite animation plugin.\n */\nexport interface SpriteAnimationComponentTypes<A extends string = string> {\n\tspriteAnimation: SpriteAnimation<A>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Data published when an animation completes.\n */\nexport interface SpriteAnimationEventData {\n\tentityId: number;\n\tanimation: string;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface SpriteAnimationPluginOptions<G extends string = 'spriteAnimation'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\nfunction buildClip(input: SpriteAnimationClipInput): SpriteAnimationClip {\n\treturn Object.freeze({\n\t\tframes: Object.freeze([...input.frames]),\n\t\tframeDuration: input.frameDuration ?? (1 / 10),\n\t\tframeDurations: input.frameDurations\n\t\t\t? Object.freeze([...input.frameDurations])\n\t\t\t: null,\n\t\tloop: input.loop ?? 'loop',\n\t});\n}\n\n/**\n * Define a single-clip animation set named 'default'.\n * For simple use cases like spinning coins, pulsing effects, etc.\n *\n * @param id Unique identifier for this animation set\n * @param clip Clip definition\n * @returns A frozen SpriteAnimationSet with one clip named 'default'\n */\nexport function defineSpriteAnimation(\n\tid: string,\n\tclip: SpriteAnimationClipInput,\n): SpriteAnimationSet<'default'> {\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze({ default: buildClip(clip) }),\n\t\tdefaultClip: 'default' as const,\n\t});\n}\n\n/**\n * Define a multi-clip animation set with named animations.\n * Animation names are inferred from the keys of the clips object.\n *\n * @param id Unique identifier for this animation set\n * @param clips Object mapping animation names to clip definitions\n * @param options Optional configuration (defaultClip)\n * @returns A frozen SpriteAnimationSet with inferred animation name union\n */\nexport function defineSpriteAnimations<A extends string>(\n\tid: string,\n\tclips: Record<A, SpriteAnimationClipInput>,\n\toptions?: { defaultClip?: NoInfer<A> },\n): SpriteAnimationSet<A> {\n\tconst builtClips = {} as Record<A, SpriteAnimationClip>;\n\tconst keys = Object.keys(clips) as A[];\n\n\tfor (const key of keys) {\n\t\tbuiltClips[key] = buildClip(clips[key]);\n\t}\n\n\tconst firstKey = keys[0];\n\tif (!firstKey) {\n\t\tthrow new Error(`defineSpriteAnimations: clips object must have at least one key`);\n\t}\n\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze(builtClips),\n\t\tdefaultClip: options?.defaultClip ?? firstKey,\n\t});\n}\n\n/**\n * Create a spriteAnimation component from an animation set.\n *\n * @param set The animation set to use\n * @param options Optional configuration (initial clip, speed, onComplete event)\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createSpriteAnimation<A extends string>(\n\tset: SpriteAnimationSet<A>,\n\toptions?: {\n\t\tinitial?: A;\n\t\tspeed?: number;\n\t\ttotalLoops?: number;\n\t\tonComplete?: (data: SpriteAnimationEventData) => void;\n\t},\n): Pick<SpriteAnimationComponentTypes<A>, 'spriteAnimation'> {\n\tconst initial = options?.initial ?? set.defaultClip;\n\treturn {\n\t\tspriteAnimation: {\n\t\t\tset,\n\t\t\tcurrent: initial,\n\t\t\tcurrentFrame: 0,\n\t\t\telapsed: 0,\n\t\t\tplaying: true,\n\t\t\tspeed: options?.speed ?? 1,\n\t\t\tdirection: 1,\n\t\t\ttotalLoops: options?.totalLoops ?? -1,\n\t\t\tcompletedLoops: 0,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Switch an entity's current animation at runtime.\n * Resets state if switching to a different animation (or restart=true).\n *\n * @returns false if entity has no spriteAnimation or animation name doesn't exist\n */\nexport function playAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n\tanimation: string,\n\toptions?: { restart?: boolean; speed?: number },\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\tif (!(animation in anim.set.clips)) return false;\n\n\tconst shouldReset = animation !== anim.current || options?.restart === true;\n\n\tif (shouldReset) {\n\t\tanim.current = animation;\n\t\tanim.currentFrame = 0;\n\t\tanim.elapsed = 0;\n\t\tanim.direction = 1;\n\t\tanim.completedLoops = 0;\n\t\tanim.justFinished = false;\n\t}\n\n\tanim.playing = true;\n\n\tif (options?.speed !== undefined) {\n\t\tanim.speed = options.speed;\n\t}\n\n\tecs.markChanged(entityId, 'spriteAnimation');\n\treturn true;\n}\n\n/**\n * Pause an entity's animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function stopAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = false;\n\treturn true;\n}\n\n/**\n * Resume a paused animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function resumeAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = true;\n\treturn true;\n}\n\n// ==================== Animation Processing Helpers ====================\n\nfunction completeAnimation(\n\tanim: SpriteAnimation,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\tanim.playing = false;\n\tanim.justFinished = true;\n\n\tanim.onComplete?.({ entityId, animation: anim.current });\n\n\tecs.commands.removeComponent(entityId, 'spriteAnimation');\n}\n\nfunction handleBoundary(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tanim.completedLoops++;\n\n\tif (clip.loop === 'once') {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\t// Check finite loop count\n\tif (anim.totalLoops > 0 && anim.completedLoops >= anim.totalLoops) {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\tif (clip.loop === 'pingPong') {\n\t\tanim.direction = anim.direction === 1 ? -1 : 1;\n\t\t// Step one frame in the new direction from the boundary\n\t\tanim.currentFrame += anim.direction;\n\t\treturn anim.elapsed > 0;\n\t}\n\n\t// loop mode: wrap to frame 0\n\tanim.currentFrame = 0;\n\treturn anim.elapsed > 0;\n}\n\n/**\n * Advance to next frame. Returns true if processing should continue (more overflow),\n * false if animation completed or reached a boundary.\n */\nfunction advanceFrame(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tconst nextFrame = anim.currentFrame + anim.direction;\n\n\t// Check boundary\n\tif (nextFrame >= clip.frames.length || nextFrame < 0) {\n\t\treturn handleBoundary(anim, clip, entityId, ecs);\n\t}\n\n\tanim.currentFrame = nextFrame;\n\treturn true;\n}\n\nfunction processFrameAdvancement(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\t// Process frame overflow\n\t// eslint-disable-next-line no-constant-condition\n\twhile (true) {\n\t\tconst frameDuration = clip.frameDurations !== null\n\t\t\t? (clip.frameDurations[anim.currentFrame] ?? clip.frameDuration)\n\t\t\t: clip.frameDuration;\n\n\t\tif (frameDuration <= 0) {\n\t\t\t// Zero-duration frame: advance immediately\n\t\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Floating-point-safe comparison: treat elapsed within 1μs of\n\t\t// frameDuration as having reached the boundary.\n\t\tconst remaining = frameDuration - anim.elapsed;\n\t\tif (remaining > 1e-6) return;\n\n\t\t// Frame complete — carry overflow (clamp negative remainders to 0)\n\t\tanim.elapsed = remaining < 0 ? -remaining : 0;\n\n\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t}\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a sprite animation plugin for ECSpresso.\n *\n * Provides:\n * - Frame-based animation system processing spriteAnimation components\n * - Loop modes: once, loop, pingPong\n * - justFinished one-frame flag for completion detection\n * - onComplete event publishing\n * - Sprite texture sync via structural cross-plugin access\n * - Change detection via markChanged\n */\nexport function createSpriteAnimationPlugin<\n\tG extends string = 'spriteAnimation',\n>(\n\toptions?: SpriteAnimationPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'spriteAnimation',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin('spriteAnimation')\n\t\t.withComponentTypes<SpriteAnimationComponentTypes>()\n\t\t.withLabels<'sprite-animation-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('sprite-animation-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('animations', {\n\t\t\t\t\twith: ['spriteAnimation'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.animations) {\n\t\t\t\t\t\tconst anim = entity.components.spriteAnimation as SpriteAnimation;\n\t\t\t\t\t\tconst clip = anim.set.clips[anim.current];\n\t\t\t\t\t\tif (!clip) continue;\n\n\t\t\t\t\t\t// Clear justFinished from previous frame\n\t\t\t\t\t\tif (anim.justFinished) {\n\t\t\t\t\t\t\tanim.justFinished = false;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Skip paused animations\n\t\t\t\t\t\tif (!anim.playing) continue;\n\n\t\t\t\t\t\t// Skip single-frame clips\n\t\t\t\t\t\tif (clip.frames.length <= 1) continue;\n\n\t\t\t\t\t\tconst previousFrame = anim.currentFrame;\n\t\t\t\t\t\tanim.elapsed += dt * anim.speed;\n\n\t\t\t\t\t\tprocessFrameAdvancement(anim, clip, entity.id, ecs);\n\n\t\t\t\t\t\t// Sync sprite texture if frame changed\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame || previousFrame === 0) {\n\t\t\t\t\t\t\tsyncSpriteTexture(entity.components as Record<string, unknown>, anim, clip);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame) {\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'spriteAnimation');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n\n// ==================== Internal: Sprite Texture Sync ====================\n\n/**\n * Sync the sprite's texture to the current frame. Uses structural access\n * following the tween plugin's cross-component pattern.\n */\nfunction syncSpriteTexture(\n\tentityComponents: Record<string, unknown>,\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n): void {\n\tconst sprite = entityComponents['sprite'];\n\tif (sprite && typeof sprite === 'object' && 'texture' in sprite) {\n\t\t(sprite as { texture: unknown }).texture = clip.frames[anim.currentFrame];\n\t}\n}\n"
5
+ "/**\n * Sprite Animation Plugin for ECSpresso\n *\n * ECS-native frame-based sprite animation. Advances through spritesheet frames,\n * handles loop modes (once, loop, pingPong), publishes completion events, and\n * syncs the current frame's texture to the PixiJS Sprite via structural access.\n *\n * Renderer2D is a required dependency — the `sprite` component comes from that plugin.\n * This plugin declares only `spriteAnimation` as its component type.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\nimport type { Spritesheet, SpritesheetData, Texture, TextureSource } from 'pixi.js';\n\n/** BaseWorld narrowed to sprite-animation components for typed access in helpers. */\ntype SpriteAnimationWorld = BaseWorld<SpriteAnimationComponentTypes>;\n\n// ==================== Loop Mode ====================\n\nexport type AnimationLoopMode = 'once' | 'loop' | 'pingPong';\n\n// ==================== Clip Types ====================\n\n/**\n * A single animation clip: an ordered sequence of texture frames with timing.\n * Immutable and shared across entities.\n */\nexport interface SpriteAnimationClip {\n\treadonly frames: readonly unknown[];\n\treadonly frameDuration: number;\n\treadonly frameDurations: readonly number[] | null;\n\treadonly loop: AnimationLoopMode;\n}\n\n/**\n * Input format for defining a clip. Accepts either uniform or per-frame timing.\n */\nexport interface SpriteAnimationClipInput {\n\t/** Array of PixiJS Texture objects */\n\tframes: readonly unknown[];\n\t/** Uniform seconds-per-frame (used when frameDurations is not provided) */\n\tframeDuration?: number;\n\t/** Per-frame durations in seconds (overrides frameDuration) */\n\tframeDurations?: readonly number[];\n\t/** Loop mode (default: 'loop') */\n\tloop?: AnimationLoopMode;\n}\n\n// ==================== Animation Set ====================\n\n/**\n * A named collection of animation clips. Immutable and shared across entities.\n * Parameterized by A (animation name union) for compile-time validation.\n */\nexport interface SpriteAnimationSet<A extends string = string> {\n\treadonly id: string;\n\treadonly clips: { readonly [K in A]: SpriteAnimationClip };\n\treadonly defaultClip: A;\n}\n\n// ==================== Component ====================\n\n/**\n * Per-entity runtime animation state.\n */\nexport interface SpriteAnimation<A extends string = string> {\n\treadonly set: SpriteAnimationSet<A>;\n\tcurrent: A;\n\tcurrentFrame: number;\n\telapsed: number;\n\tplaying: boolean;\n\tspeed: number;\n\tdirection: 1 | -1;\n\ttotalLoops: number;\n\tcompletedLoops: number;\n\tjustFinished: boolean;\n\tonComplete?: (data: SpriteAnimationEventData) => void;\n}\n\n/**\n * Component types provided by the sprite animation plugin.\n */\nexport interface SpriteAnimationComponentTypes<A extends string = string> {\n\tspriteAnimation: SpriteAnimation<A>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Data published when an animation completes.\n */\nexport interface SpriteAnimationEventData {\n\tentityId: number;\n\tanimation: string;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface SpriteAnimationPluginOptions<G extends string = 'spriteAnimation'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\nfunction buildClip(input: SpriteAnimationClipInput): SpriteAnimationClip {\n\treturn Object.freeze({\n\t\tframes: Object.freeze([...input.frames]),\n\t\tframeDuration: input.frameDuration ?? (1 / 10),\n\t\tframeDurations: input.frameDurations\n\t\t\t? Object.freeze([...input.frameDurations])\n\t\t\t: null,\n\t\tloop: input.loop ?? 'loop',\n\t});\n}\n\n/**\n * Define a single-clip animation set named 'default'.\n * For simple use cases like spinning coins, pulsing effects, etc.\n *\n * @param id Unique identifier for this animation set\n * @param clip Clip definition\n * @returns A frozen SpriteAnimationSet with one clip named 'default'\n */\nexport function defineSpriteAnimation(\n\tid: string,\n\tclip: SpriteAnimationClipInput,\n): SpriteAnimationSet<'default'> {\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze({ default: buildClip(clip) }),\n\t\tdefaultClip: 'default' as const,\n\t});\n}\n\n/**\n * Define a multi-clip animation set with named animations.\n * Animation names are inferred from the keys of the clips object.\n *\n * @param id Unique identifier for this animation set\n * @param clips Object mapping animation names to clip definitions\n * @param options Optional configuration (defaultClip)\n * @returns A frozen SpriteAnimationSet with inferred animation name union\n */\nexport function defineSpriteAnimations<A extends string>(\n\tid: string,\n\tclips: Record<A, SpriteAnimationClipInput>,\n\toptions?: { defaultClip?: NoInfer<A> },\n): SpriteAnimationSet<A> {\n\tconst builtClips = {} as Record<A, SpriteAnimationClip>;\n\tconst keys = Object.keys(clips) as A[];\n\n\tfor (const key of keys) {\n\t\tbuiltClips[key] = buildClip(clips[key]);\n\t}\n\n\tconst firstKey = keys[0];\n\tif (!firstKey) {\n\t\tthrow new Error(`defineSpriteAnimations: clips object must have at least one key`);\n\t}\n\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze(builtClips),\n\t\tdefaultClip: options?.defaultClip ?? firstKey,\n\t});\n}\n\n/**\n * Create a spriteAnimation component from an animation set.\n *\n * @param set The animation set to use\n * @param options Optional configuration (initial clip, speed, onComplete event)\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createSpriteAnimation<A extends string>(\n\tset: SpriteAnimationSet<A>,\n\toptions?: {\n\t\tinitial?: A;\n\t\tspeed?: number;\n\t\ttotalLoops?: number;\n\t\tonComplete?: (data: SpriteAnimationEventData) => void;\n\t},\n): Pick<SpriteAnimationComponentTypes<A>, 'spriteAnimation'> {\n\tconst initial = options?.initial ?? set.defaultClip;\n\treturn {\n\t\tspriteAnimation: {\n\t\t\tset,\n\t\t\tcurrent: initial,\n\t\t\tcurrentFrame: 0,\n\t\t\telapsed: 0,\n\t\t\tplaying: true,\n\t\t\tspeed: options?.speed ?? 1,\n\t\t\tdirection: 1,\n\t\t\ttotalLoops: options?.totalLoops ?? -1,\n\t\t\tcompletedLoops: 0,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Switch an entity's current animation at runtime.\n * Resets state if switching to a different animation (or restart=true).\n *\n * @returns false if entity has no spriteAnimation or animation name doesn't exist\n */\nexport function playAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n\tanimation: string,\n\toptions?: { restart?: boolean; speed?: number },\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\tif (!(animation in anim.set.clips)) return false;\n\n\tconst shouldReset = animation !== anim.current || options?.restart === true;\n\n\tif (shouldReset) {\n\t\tanim.current = animation;\n\t\tanim.currentFrame = 0;\n\t\tanim.elapsed = 0;\n\t\tanim.direction = 1;\n\t\tanim.completedLoops = 0;\n\t\tanim.justFinished = false;\n\t}\n\n\tanim.playing = true;\n\n\tif (options?.speed !== undefined) {\n\t\tanim.speed = options.speed;\n\t}\n\n\tecs.markChanged(entityId, 'spriteAnimation');\n\treturn true;\n}\n\n/**\n * Pause an entity's animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function stopAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = false;\n\treturn true;\n}\n\n/**\n * Resume a paused animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function resumeAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = true;\n\treturn true;\n}\n\n// ==================== Animation Processing Helpers ====================\n\nfunction completeAnimation(\n\tanim: SpriteAnimation,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\tanim.playing = false;\n\tanim.justFinished = true;\n\n\tanim.onComplete?.({ entityId, animation: anim.current });\n\n\tecs.commands.removeComponent(entityId, 'spriteAnimation');\n}\n\nfunction handleBoundary(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tanim.completedLoops++;\n\n\tif (clip.loop === 'once') {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\t// Check finite loop count\n\tif (anim.totalLoops > 0 && anim.completedLoops >= anim.totalLoops) {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\tif (clip.loop === 'pingPong') {\n\t\tanim.direction = anim.direction === 1 ? -1 : 1;\n\t\t// Step one frame in the new direction from the boundary\n\t\tanim.currentFrame += anim.direction;\n\t\treturn anim.elapsed > 0;\n\t}\n\n\t// loop mode: wrap to frame 0\n\tanim.currentFrame = 0;\n\treturn anim.elapsed > 0;\n}\n\n/**\n * Advance to next frame. Returns true if processing should continue (more overflow),\n * false if animation completed or reached a boundary.\n */\nfunction advanceFrame(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tconst nextFrame = anim.currentFrame + anim.direction;\n\n\t// Check boundary\n\tif (nextFrame >= clip.frames.length || nextFrame < 0) {\n\t\treturn handleBoundary(anim, clip, entityId, ecs);\n\t}\n\n\tanim.currentFrame = nextFrame;\n\treturn true;\n}\n\nfunction processFrameAdvancement(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\t// Process frame overflow\n\t// eslint-disable-next-line no-constant-condition\n\twhile (true) {\n\t\tconst frameDuration = clip.frameDurations !== null\n\t\t\t? (clip.frameDurations[anim.currentFrame] ?? clip.frameDuration)\n\t\t\t: clip.frameDuration;\n\n\t\tif (frameDuration <= 0) {\n\t\t\t// Zero-duration frame: advance immediately\n\t\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Floating-point-safe comparison: treat elapsed within 1μs of\n\t\t// frameDuration as having reached the boundary.\n\t\tconst remaining = frameDuration - anim.elapsed;\n\t\tif (remaining > 1e-6) return;\n\n\t\t// Frame complete — carry overflow (clamp negative remainders to 0)\n\t\tanim.elapsed = remaining < 0 ? -remaining : 0;\n\n\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t}\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a sprite animation plugin for ECSpresso.\n *\n * Provides:\n * - Frame-based animation system processing spriteAnimation components\n * - Loop modes: once, loop, pingPong\n * - justFinished one-frame flag for completion detection\n * - onComplete event publishing\n * - Sprite texture sync via structural cross-plugin access\n * - Change detection via markChanged\n */\nexport function createSpriteAnimationPlugin<\n\tG extends string = 'spriteAnimation',\n>(\n\toptions?: SpriteAnimationPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'spriteAnimation',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin('spriteAnimation')\n\t\t.withComponentTypes<SpriteAnimationComponentTypes>()\n\t\t.withLabels<'sprite-animation-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('sprite-animation-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('animations', {\n\t\t\t\t\twith: ['spriteAnimation'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.animations) {\n\t\t\t\t\t\tconst anim = entity.components.spriteAnimation as SpriteAnimation;\n\t\t\t\t\t\tconst clip = anim.set.clips[anim.current];\n\t\t\t\t\t\tif (!clip) continue;\n\n\t\t\t\t\t\t// Clear justFinished from previous frame\n\t\t\t\t\t\tif (anim.justFinished) {\n\t\t\t\t\t\t\tanim.justFinished = false;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Skip paused animations\n\t\t\t\t\t\tif (!anim.playing) continue;\n\n\t\t\t\t\t\t// Skip single-frame clips\n\t\t\t\t\t\tif (clip.frames.length <= 1) continue;\n\n\t\t\t\t\t\tconst previousFrame = anim.currentFrame;\n\t\t\t\t\t\tanim.elapsed += dt * anim.speed;\n\n\t\t\t\t\t\tprocessFrameAdvancement(anim, clip, entity.id, ecs);\n\n\t\t\t\t\t\t// Sync sprite texture if frame changed\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame || previousFrame === 0) {\n\t\t\t\t\t\t\tsyncSpriteTexture(entity.components as Record<string, unknown>, anim, clip);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame) {\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'spriteAnimation');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n\n// ==================== Internal: Sprite Texture Sync ====================\n\n/**\n * Sync the sprite's texture to the current frame. Uses structural access\n * following the tween plugin's cross-component pattern.\n */\nfunction syncSpriteTexture(\n\tentityComponents: Record<string, unknown>,\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n): void {\n\tconst sprite = entityComponents['sprite'];\n\tif (sprite && typeof sprite === 'object' && 'texture' in sprite) {\n\t\t(sprite as { texture: unknown }).texture = clip.frames[anim.currentFrame];\n\t}\n}\n\n// ==================== Spritesheet Helpers ====================\n\n/**\n * Per-clip timing/loop overrides keyed by animation name. Each entry tweaks\n * a single clip; omitted entries fall back to the top-level defaults.\n */\nexport type SheetClipOverrides<A extends string> = {\n\treadonly [K in A]?: Omit<SpriteAnimationClipInput, 'frames'>;\n};\n\n/**\n * Extract the animation-name union from a typed SpritesheetData. Falls back\n * to `string` for untyped sheets.\n */\nexport type SheetAnimationKeys<S extends SpritesheetData> =\n\tS extends { animations: infer A }\n\t\t? A extends Record<infer K, unknown>\n\t\t\t? K extends string ? K : never\n\t\t\t: string\n\t\t: string;\n\n/**\n * Build a clip from a named animation in a loaded PixiJS Spritesheet.\n *\n * @example\n * const sheet = await Assets.load<Spritesheet>('/hero.json');\n * const idle = clipFromSheet(sheet, 'idle', { frameDuration: 1 / 12 });\n */\nexport function clipFromSheet(\n\tsheet: Spritesheet,\n\tanimationName: string,\n\toptions?: Omit<SpriteAnimationClipInput, 'frames'>,\n): SpriteAnimationClip {\n\tconst frames = sheet.animations[animationName];\n\tif (!frames || frames.length === 0) {\n\t\tconst available = Object.keys(sheet.animations).join(', ') || '(none)';\n\t\tthrow new Error(\n\t\t\t`clipFromSheet: animation \"${animationName}\" not found on sheet (or has no frames). Available: ${available}`,\n\t\t);\n\t}\n\treturn buildClip({ ...options, frames });\n}\n\n/**\n * Build an animation set from every named animation in a PixiJS Spritesheet.\n * When the sheet is typed as `Spritesheet<MyData>`, animation names and\n * `defaultClip` / `perClip` keys are inferred at compile time.\n *\n * @example\n * const sheet = ecs.assets.get('hero'); // Spritesheet<HeroData>\n * const set = animationSetFromSheet('hero', sheet, {\n * defaultClip: 'idle',\n * frameDuration: 1 / 12,\n * perClip: { attack: { loop: 'once' } },\n * });\n */\nexport function animationSetFromSheet<S extends SpritesheetData = SpritesheetData>(\n\tid: string,\n\tsheet: Spritesheet<S>,\n\toptions?: {\n\t\tdefaultClip?: SheetAnimationKeys<S>;\n\t\tframeDuration?: number;\n\t\tloop?: AnimationLoopMode;\n\t\tperClip?: SheetClipOverrides<SheetAnimationKeys<S>>;\n\t},\n): SpriteAnimationSet<SheetAnimationKeys<S>> {\n\ttype A = SheetAnimationKeys<S>;\n\tconst entries = Object.entries(sheet.animations) as unknown as [A, readonly unknown[]][];\n\tconst firstEntry = entries[0];\n\tif (!firstEntry) {\n\t\tthrow new Error(`animationSetFromSheet: sheet \"${id}\" has no animations defined`);\n\t}\n\n\tconst clips = entries.reduce((acc, [name, sheetFrames]) => {\n\t\tif (!Array.isArray(sheetFrames) || sheetFrames.length === 0) {\n\t\t\tthrow new Error(`animationSetFromSheet: animation \"${String(name)}\" on sheet \"${id}\" has no frames (got ${sheetFrames === null ? 'null' : Array.isArray(sheetFrames) ? 'empty array' : typeof sheetFrames})`);\n\t\t}\n\t\tconst override = options?.perClip?.[name];\n\t\tacc[name] = buildClip({\n\t\t\tframes: sheetFrames,\n\t\t\tframeDuration: override?.frameDuration ?? options?.frameDuration,\n\t\t\tframeDurations: override?.frameDurations,\n\t\t\tloop: override?.loop ?? options?.loop,\n\t\t});\n\t\treturn acc;\n\t}, {} as Record<A, SpriteAnimationClip>);\n\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze(clips),\n\t\tdefaultClip: options?.defaultClip ?? firstEntry[0],\n\t});\n}\n\n/**\n * Slice a grid-arranged sprite sheet into a clip. Use when you don't have a\n * TexturePacker JSON — just an image and uniform cell dimensions. Cells are\n * walked row-major.\n *\n * Specify exactly one of `rows`, `count`, or `indices` to define the cell set\n * (along with `columns`). Combining `count` and `indices` is rejected.\n *\n * Returns a `Promise` because pixi.js is imported lazily — keeps the static\n * module graph free of a runtime pixi dependency for consumers who only use\n * the sheet-based helpers.\n *\n * @example\n * const tex = await Assets.load<Texture>('/coin.png');\n * const clip = await clipFromGrid({\n * source: tex.source,\n * frameWidth: 16, frameHeight: 16,\n * columns: 8, count: 8,\n * frameDuration: 1 / 10,\n * });\n */\nexport async function clipFromGrid(input: {\n\tsource: TextureSource;\n\tframeWidth: number;\n\tframeHeight: number;\n\tcolumns: number;\n\trows?: number;\n\t/** Explicit row-major, 0-based cell indices. Mutually exclusive with `count`. */\n\tindices?: readonly number[];\n\t/** Number of cells to use, walked row-major. Mutually exclusive with `indices`. */\n\tcount?: number;\n\t/** Pixels between cells. */\n\tspacing?: number;\n\t/** Pixels around the sheet edge. */\n\tmargin?: number;\n\tframeDuration?: number;\n\tframeDurations?: readonly number[];\n\tloop?: AnimationLoopMode;\n}): Promise<SpriteAnimationClip> {\n\tconst { source, frameWidth, frameHeight, columns, rows, indices, count, spacing = 0, margin = 0 } = input;\n\n\tif (!source) {\n\t\tthrow new Error(`clipFromGrid: source is required`);\n\t}\n\tif (!Number.isInteger(columns) || columns <= 0) {\n\t\tthrow new Error(`clipFromGrid: columns must be a positive integer, got ${columns}`);\n\t}\n\tif (rows !== undefined && (!Number.isInteger(rows) || rows <= 0)) {\n\t\tthrow new Error(`clipFromGrid: rows must be a positive integer, got ${rows}`);\n\t}\n\tif (count !== undefined && (!Number.isInteger(count) || count < 0)) {\n\t\tthrow new Error(`clipFromGrid: count must be a non-negative integer, got ${count}`);\n\t}\n\tif (indices !== undefined && count !== undefined) {\n\t\tthrow new Error(`clipFromGrid: pass either 'indices' or 'count', not both`);\n\t}\n\tif (indices === undefined && count === undefined && rows === undefined) {\n\t\tthrow new Error(`clipFromGrid: specify 'rows', 'count', or 'indices' to define the cell set (only 'columns' is ambiguous)`);\n\t}\n\n\tconst gridTotal = rows !== undefined ? columns * rows : undefined;\n\tconst chosen: readonly number[] = indices ?? Array.from(\n\t\t{ length: gridTotal !== undefined && count !== undefined ? Math.min(count, gridTotal) : (count ?? gridTotal ?? 0) },\n\t\t(_, i) => i,\n\t);\n\n\tif (chosen.length === 0) {\n\t\tthrow new Error(`clipFromGrid: resolved to zero cells (empty indices array or count: 0)`);\n\t}\n\n\tconst upperBound = gridTotal ?? Infinity;\n\tconst invalid = chosen.find(idx => !Number.isInteger(idx) || idx < 0 || idx >= upperBound);\n\tif (invalid !== undefined) {\n\t\tconst bounds = gridTotal !== undefined ? `[0, ${gridTotal})` : `[0, ∞) — pass 'rows' to enable upper-bound checking`;\n\t\tthrow new Error(`clipFromGrid: invalid cell index ${invalid}; expected integer in ${bounds}`);\n\t}\n\n\tconst { Texture, Rectangle } = await import('pixi.js');\n\n\tconst frames: Texture[] = chosen.map(idx => {\n\t\tconst col = idx % columns;\n\t\tconst row = Math.floor(idx / columns);\n\t\tconst x = margin + col * (frameWidth + spacing);\n\t\tconst y = margin + row * (frameHeight + spacing);\n\t\treturn new Texture({\n\t\t\tsource,\n\t\t\tframe: new Rectangle(x, y, frameWidth, frameHeight),\n\t\t});\n\t});\n\n\treturn buildClip({\n\t\tframes,\n\t\tframeDuration: input.frameDuration,\n\t\tframeDurations: input.frameDurations,\n\t\tloop: input.loop,\n\t});\n}\n\n/**\n * Build an asset-manager-compatible loader for a PixiJS spritesheet atlas\n * (TexturePacker JSON, etc.). Returns the fully-parsed `Spritesheet` object\n * with `.animations` and `.textures` populated.\n *\n * The loader performs a runtime shape check on the resolved value — `Assets.load<T>`\n * is purely nominal in PixiJS, so pointing this at a non-atlas URL would\n * otherwise surface as a misleading 'animation not found' error deep in\n * `clipFromSheet`/`animationSetFromSheet`. The shape check turns that into a\n * load-time error with a clear message.\n *\n * To get literal animation-name inference, declare `S` as an\n * `interface ... extends SpritesheetData` (a `type` alias re-widens via\n * `SpritesheetData.animations`'s `Dict<string[]>` string index signature).\n *\n * @example\n * interface HeroData extends SpritesheetData {\n * animations: { idle: string[]; walk: string[]; attack: string[] };\n * }\n *\n * ecs.builder.withAssets(a => a\n * .add('hero', spritesheetLoader<HeroData>('/hero.json'))\n * );\n *\n * // Later:\n * const sheet = ecs.assets.get('hero'); // Spritesheet<HeroData>\n * const set = animationSetFromSheet('hero', sheet); // names inferred\n */\nexport function spritesheetLoader<S extends SpritesheetData = SpritesheetData>(\n\turl: string,\n): () => Promise<Spritesheet<S>> {\n\treturn async () => {\n\t\tconst { Assets } = await import('pixi.js');\n\t\tconst result = await Assets.load<Spritesheet<S>>(url);\n\t\t// Verify properties exist AND are non-null objects — `'animations' in result`\n\t\t// alone would pass for { animations: undefined }, defeating the load-time guard.\n\t\tif (\n\t\t\t!result || typeof result !== 'object'\n\t\t\t|| typeof (result as { animations?: unknown }).animations !== 'object'\n\t\t\t|| (result as { animations: unknown }).animations === null\n\t\t\t|| typeof (result as { textures?: unknown }).textures !== 'object'\n\t\t\t|| (result as { textures: unknown }).textures === null\n\t\t) {\n\t\t\tthrow new Error(\n\t\t\t\t`spritesheetLoader: resource at \"${url}\" did not resolve to a Spritesheet ` +\n\t\t\t\t`(expected non-null 'animations' and 'textures' objects). Check that the URL points to a TexturePacker-style JSON atlas, not a raw image.`,\n\t\t\t);\n\t\t}\n\t\treturn result;\n\t};\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "9EF329142BBA6E6864756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "288D71B236D04EC264756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -24,7 +24,25 @@ export interface SpatialIndexPluginOptions<G extends string = 'spatialIndex'> {
24
24
  systemGroup?: G;
25
25
  /** Priority for rebuild systems (default: 2000, before collision) */
26
26
  priority?: number;
27
- /** Phases to register rebuild systems in (default: ['fixedUpdate', 'postUpdate']) */
27
+ /**
28
+ * Phases to register rebuild systems in (default: ['fixedUpdate', 'postUpdate']).
29
+ *
30
+ * When both phases are registered, the `postUpdate` rebuild is
31
+ * automatically skipped on cycles where:
32
+ * 1. The `fixedUpdate` rebuild already ran this cycle, AND
33
+ * 2. No entity hierarchy exists.
34
+ *
35
+ * In flat-hierarchy scenes `transform-propagation` copies
36
+ * `localTransform → worldTransform` unchanged, so the postUpdate
37
+ * grid would duplicate the fixedUpdate one. The skip is automatic
38
+ * and re-engages once any parent relationship is set, or whenever
39
+ * `fixedUpdate` doesn't run that cycle (sub-fixed-DT frames).
40
+ *
41
+ * Edge case: if you write to `worldTransform` directly between
42
+ * phases without using `transform-propagation` or entity hierarchy,
43
+ * the auto-skip will leave your writes unindexed. Set
44
+ * `phases: ['postUpdate']` explicitly to bypass the auto-skip.
45
+ */
28
46
  phases?: ReadonlyArray<SpatialIndexPhase>;
29
47
  }
30
48
  /**