kippy 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -3
- package/dist/bundle.min.js +1 -0
- package/dist/types/index.d.ts +8 -0
- package/package.json +7 -3
- package/dist/camera.js +0 -48
- package/dist/entity.js +0 -33
- package/dist/game.js +0 -61
- package/dist/input.js +0 -90
- package/dist/physics.js +0 -530
- package/dist/scene.js +0 -29
- package/dist/sprite.js +0 -10
- package/dist/vector.js +0 -107
- package/index.d.ts +0 -8
- package/index.js +0 -8
- /package/dist/{camera.d.ts → types/camera.d.ts} +0 -0
- /package/dist/{entity.d.ts → types/entity.d.ts} +0 -0
- /package/dist/{game.d.ts → types/game.d.ts} +0 -0
- /package/dist/{input.d.ts → types/input.d.ts} +0 -0
- /package/dist/{physics.d.ts → types/physics.d.ts} +0 -0
- /package/dist/{scene.d.ts → types/scene.d.ts} +0 -0
- /package/dist/{sprite.d.ts → types/sprite.d.ts} +0 -0
- /package/dist/{vector.d.ts → types/vector.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -2,13 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
Kippy is a 2D JS game engine written purely for fun and simplicity. It currently utilizes the Canvas 2D context for rendering and aims to have a small set of APIs viable for game dev, but do expect a lot of components to change in the future.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Usage
|
|
6
6
|
|
|
7
7
|
Install through npm:
|
|
8
|
-
```
|
|
8
|
+
```sh
|
|
9
9
|
npm install kippy
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
then import, for example:
|
|
13
|
+
```js
|
|
14
|
+
import { Game, Scene, Entity } from "kippy";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
or if you are on raw HTML5, you can pull from a cdn:
|
|
18
|
+
```js
|
|
19
|
+
import { Game, Scene, Entity } from "https://unpkg.com/kippy";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Example
|
|
23
|
+
|
|
24
|
+
There is a Flappy Bird game in `./example` for now. You can run it by cloning this repo, then install deps:
|
|
25
|
+
```sh
|
|
26
|
+
npm install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
and build:
|
|
30
|
+
```sh
|
|
31
|
+
npm run build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
and start Vite:
|
|
35
|
+
```sh
|
|
36
|
+
npx vite
|
|
37
|
+
```
|
|
38
|
+
|
|
12
39
|
## Tutorial
|
|
13
40
|
|
|
14
41
|
Here is a vaguely-written tutorial for now:
|
|
@@ -297,7 +324,7 @@ To be added, for now use web's built-in `Audio` class.
|
|
|
297
324
|
|
|
298
325
|
### Sleep system
|
|
299
326
|
|
|
300
|
-
When a body's velocity is too low for too long, the body will enter sleep state, which means its position will not be affected by the physics engine until a force is applied
|
|
327
|
+
When a body's velocity is too low for too long, the body will enter sleep state, which means its position will not be affected by the physics engine until a force is applied, a collision happens, or the velocity is above threshold again, this is to prevent jittering and optimize performance.
|
|
301
328
|
|
|
302
329
|
You can configure it inside `RigidBody`:
|
|
303
330
|
```js
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class t{x;y;static ZERO=new t(0,0);static ONE=new t(1,1);static UP=new t(0,-1);static DOWN=new t(0,1);static LEFT=new t(-1,0);static RIGHT=new t(1,0);constructor(t,e){this.x=t,this.y=e}toString(){return`Vector2(${this.x}, ${this.y})`}add(e){return new t(this.x+e.x,this.y+e.y)}sub(e){return new t(this.x-e.x,this.y-e.y)}mul(e){return new t(this.x*e.x,this.y*e.y)}div(e){return new t(this.x/e.x,this.y/e.y)}neg(){return new t(-this.x,-this.y)}scale(e){return new t(this.x*e,this.y*e)}magnitude(){return Math.sqrt(this.x*this.x+this.y*this.y)}magnitudeSquared(){return this.x*this.x+this.y*this.y}normalize(){const e=this.magnitude();return e>0?new t(this.x/e,this.y/e):new t(0,0)}dot(t){return this.x*t.x+this.y*t.y}cross(t){return this.x*t.y-this.y*t.x}project(t){const e=this.dot(t)/t.magnitudeSquared();return t.scale(e)}min(e){return new t(Math.min(this.x,e.x),Math.min(this.y,e.y))}max(e){return new t(Math.max(this.x,e.x),Math.max(this.y,e.y))}floor(){return new t(Math.floor(this.x),Math.floor(this.y))}ceil(){return new t(Math.ceil(this.x),Math.ceil(this.y))}round(){return new t(Math.round(this.x),Math.round(this.y))}distance(t){return Math.sqrt((this.x-t.x)**2+(this.y-t.y)**2)}distanceSquared(t){return(this.x-t.x)**2+(this.y-t.y)**2}copy(){return new t(this.x,this.y)}lerp(t,e){return this.add(t.sub(this).scale(e))}clamp(t){const e=this.magnitude();return e>t?this.scale(t/e):this.copy()}rotate(e){const i=Math.cos(e),s=Math.sin(e);return new t(this.x*i-this.y*s,this.x*s+this.y*i)}orthogonal(){return new t(-this.y,this.x)}angle(){return Math.atan2(this.y,this.x)}angleTo(t){return Math.atan2(t.y-this.y,t.x-this.x)}reflect(t){const e=this.dot(t);return this.sub(t.scale(2*e))}equals(t){return this.x===t.x&&this.y===t.y}}class e{canvas;keys=new Set;keysPressed=new Set;keysReleased=new Set;pointer=new t(0,0);pointers=new Set;pointersPressed=new Set;pointersReleased=new Set;constructor(t){this.canvas=t.canvas,window.addEventListener("keydown",t=>{this.keys.has(t.key)||this.keysPressed.add(t.key),this.keys.add(t.key)}),window.addEventListener("keyup",t=>{this.keys.delete(t.key),this.keysReleased.add(t.key)}),this.canvas.addEventListener("mousemove",t=>{const e=this.canvas.getBoundingClientRect();this.pointer.x=t.clientX-e.left,this.pointer.y=t.clientY-e.top}),this.canvas.addEventListener("mousedown",t=>{this.pointers.has(t.button)||this.pointersPressed.add(t.button),this.pointers.add(t.button)}),this.canvas.addEventListener("mouseup",t=>{this.pointers.delete(t.button),this.pointersReleased.add(t.button)}),this.canvas.addEventListener("touchmove",t=>{t.preventDefault();const e=this.canvas.getBoundingClientRect(),i=t.touches[0];this.pointer.x=i.clientX-e.left,this.pointer.y=i.clientY-e.top}),this.canvas.addEventListener("touchstart",t=>{t.preventDefault(),this.pointers.has(2)||this.pointersPressed.add(2),this.pointers.add(2)}),this.canvas.addEventListener("touchend",t=>{t.preventDefault(),this.pointers.delete(2),this.pointersReleased.add(2)}),this.canvas.addEventListener("contextmenu",t=>{t.preventDefault()})}update(){this.keysPressed.clear(),this.keysReleased.clear(),this.pointersPressed.clear(),this.pointersReleased.clear()}isKeyDown(t){return this.keys.has(t)}isKeyPressed(t){return this.keysPressed.has(t)}isKeyReleased(t){return this.keysReleased.has(t)}isPointerDown(t=0){return this.pointers.has(t)}isPointerPressed(t=0){return this.pointersPressed.has(t)}isPointerReleased(t=0){return this.pointersReleased.has(t)}}class i{velocity;rotationVelocity;mass;inertia;force;torque;restitution;sleepThreshold;sleepTimeThreshold;isSleeping;sleepTimer;constructor(e={}){this.velocity=e.velocity??new t(0,0),this.rotationVelocity=e.rotationVelocity??0,this.mass=e.mass??1,this.inertia=e.inertia??1,this.force=e.force??new t(0,0),this.torque=e.torque??0,this.restitution=e.restitution??0,this.sleepThreshold=e.sleepThreshold??.1,this.sleepTimeThreshold=e.sleepTimeThreshold??.5,this.isSleeping=e.isSleeping??!1,this.sleepTimer=e.sleepTimer??0}wake(){this.isSleeping=!1,this.sleepTimer=0}}class s{radius;offset;isTrigger;layer;mask;constructor(e){this.radius=e.radius,this.offset=e.offset??new t(0,0),this.isTrigger=e.isTrigger??!1,this.layer=e.layer??1,this.mask=e.mask??4294967295}}class o{width;height;offset;isTrigger;layer;mask;constructor(e){this.width=e.width,this.height=e.height,this.offset=e.offset??new t(0,0),this.isTrigger=e.isTrigger??!1,this.layer=e.layer??1,this.mask=e.mask??4294967295}}class n{cellSize;grid;constructor(t={}){this.cellSize=t.cellSize||100,this.grid=t.grid||new Map}clear(){this.grid.clear()}adaptCellSize(t){const e=[];for(const i of t){const t=this.getEntityBounds(i),s=t.maxX-t.minX,o=t.maxY-t.minY,n=Math.max(s,o);e.push(n)}e.sort((t,e)=>t-e);const i=e[Math.floor(.75*e.length)];this.cellSize=2.5*i}insert(t){if(t.collider){const e=this.getEntityBounds(t),i=Math.floor(e.minX/this.cellSize),s=Math.floor(e.maxX/this.cellSize),o=Math.floor(e.minY/this.cellSize),n=Math.floor(e.maxY/this.cellSize);for(let e=i;e<=s;e++)for(let i=o;i<=n;i++){const s=`${e},${i}`;this.grid.has(s)||this.grid.set(s,new Set),this.grid.get(s).add(t)}}}getNearby(t){const e=Math.floor(t.position.x/this.cellSize),i=Math.floor(t.position.y/this.cellSize),s=new Set;for(let o=-1;o<=1;o++)for(let n=-1;n<=1;n++){const r=`${e+o},${i+n}`,a=this.grid.get(r);if(a)for(const e of a)e!==t&&s.add(e)}return Array.from(s)}getEntityBounds(t){if(t.collider instanceof s){const e=t.collider.radius;return{minX:t.position.x-e,maxX:t.position.x+e,minY:t.position.y-e,maxY:t.position.y+e}}if(t.collider instanceof o){const e=t.collider.width,i=t.collider.height;return{minX:t.position.x-e,maxX:t.position.x+e,minY:t.position.y-i,maxY:t.position.y+i}}throw new Error("Collider type not supported")}}class r{collisionPairs=new Map;spatialGrid=new n;entityCount=0;update(e,s){for(const t of e)if(t.body instanceof i){if((t.body.force.magnitudeSquared()>0||0!==t.body.torque||t.body.velocity.magnitudeSquared()>t.body.sleepThreshold**2||Math.abs(t.body.rotationVelocity)>t.body.sleepThreshold)&&t.body.wake(),t.body.isSleeping)continue;t.body.velocity.x+=t.body.force.x/t.body.mass*s,t.body.velocity.y+=t.body.force.y/t.body.mass*s,t.body.rotationVelocity+=t.body.torque/t.body.inertia*s,t.body.force.x=0,t.body.force.y=0,t.body.torque=0}this.spatialGrid.clear(),this.entityCount!==e.length&&(this.entityCount=e.length,this.spatialGrid.adaptCellSize(e));for(const t of e)this.spatialGrid.insert(t);const o=new Map,n=[];for(const i of e)if(i.collider){const e=this.spatialGrid.getNearby(i);for(const s of e)if(s.collider){if(o.has(i)&&o.get(i)?.has(s)||o.has(s)&&o.get(s)?.has(i))continue;const e=this.checkCollision(i,s);if(e.info){i.body?.isSleeping&&s.body?.isSleeping||(i.body?.wake(),s.body?.wake()),o.has(i)||o.set(i,new Map),o.get(i)?.set(s,e.info);this.collisionPairs.get(i)?.has(s)||this.collisionPairs.get(s)?.has(i)?e.isTrigger?(i.onTriggerStay?.(s),s.onTriggerStay?.(i)):(i.onCollisionStay?.(s,e.info),s.onCollisionStay?.(i,{...e.info,normal:new t(-e.info.normal.x,-e.info.normal.y)})):e.isTrigger?(i.onTriggerEnter?.(s),s.onTriggerEnter?.(i)):(i.onCollisionEnter?.(s,e.info),s.onCollisionEnter?.(i,{...e.info,normal:new t(-e.info.normal.x,-e.info.normal.y)})),e.isTrigger||n.push({entityA:i,entityB:s,info:e.info})}}}for(let t=0;t<6;t++)for(const t of n)this.resolveCollision(t.entityA,t.entityB,t.info,s);for(const[e,i]of this.collisionPairs)for(const[s,n]of i){if(!(o.get(e)?.has(s)||o.get(s)?.has(e))){e.collider?.isTrigger||s.collider?.isTrigger?(e.onTriggerExit?.(s),s.onTriggerExit?.(e)):(e.onCollisionExit?.(s,n),s.onCollisionExit?.(e,{...n,normal:new t(-n.normal.x,-n.normal.y)}))}}this.collisionPairs=o;for(const t of e)if(t.body instanceof i){if(t.body.isSleeping)continue;t.position.x+=t.body.velocity.x*s,t.position.y+=t.body.velocity.y*s,t.rotation+=t.body.rotationVelocity*s;const e=t.body.velocity.magnitude(),i=Math.abs(t.body.rotationVelocity);e<t.body.sleepThreshold&&i<t.body.sleepThreshold?(t.body.sleepTimer+=s,t.body.sleepTimer>=t.body.sleepTimeThreshold&&(t.body.isSleeping=!0,t.body.velocity.x=0,t.body.velocity.y=0,t.body.rotationVelocity=0)):t.body.sleepTimer=0}}checkCollision(e,i){if(e.collider&&i.collider){if(0===(e.collider.mask&i.collider.layer)||0===(i.collider.mask&e.collider.layer))return{isTrigger:!1};const n=e.collider.isTrigger||i.collider.isTrigger,r=e.position.add(e.collider.offset),a=i.position.add(i.collider.offset);if(e.collider instanceof s&&i.collider instanceof s){const s=r.distance(a),o=e.collider.radius+i.collider.radius;if(s>=o)return{isTrigger:!1};const h=o-s,l=a.sub(r);let c=s>0?l.scale(1/s):new t(1,0),d=0;const y=this.collisionPairs.get(e)?.get(i)||this.collisionPairs.get(i)?.get(e);return y&&(d=y.accumulatedNormalImpulse),{isTrigger:n,info:{normal:c,penetration:h,contact:r.add(c.scale(e.collider.radius)),accumulatedNormalImpulse:d}}}if(e.collider instanceof o&&i.collider instanceof o){const s=e.collider.width/2,o=e.collider.height/2,h=i.collider.width/2,l=i.collider.height/2,c=a.sub(r),d=s+h-Math.abs(c.x),y=o+l-Math.abs(c.y);if(d<=0||y<=0)return{isTrigger:!1};let u,p;d<y?(p=d,u=new t(c.x<0?-1:1,0)):(p=y,u=new t(0,c.y<0?-1:1));const g=r.x+Math.sign(c.x)*(s-d/2),m=r.y+Math.sign(c.y)*(o-y/2);let f=0;const x=this.collisionPairs.get(e)?.get(i)||this.collisionPairs.get(i)?.get(e);return x&&(f=x.accumulatedNormalImpulse),{isTrigger:n,info:{normal:u,penetration:p,contact:new t(g,m),accumulatedNormalImpulse:f}}}if(e.collider instanceof s&&i.collider instanceof o||e.collider instanceof o&&i.collider instanceof s){const o=e.collider instanceof s,h=o?r:a,l=o?a:r,c=(o?e:i).collider,d=(o?i:e).collider,y=d.width/2,u=d.height/2,p=h.sub(l);let g,m,f;if(Math.abs(p.x)<y&&Math.abs(p.y)<u){const e=y-Math.abs(p.x),i=u-Math.abs(p.y);if(e<i){const i=p.x<0?-1:1,s=new t(i,0);g=o?s.neg():s,m=c.radius+e,f=new t(l.x+i*y,h.y)}else{const e=p.y<0?-1:1,s=new t(0,e);g=o?s.neg():s,m=c.radius+i,f=new t(h.x,l.y+e*u)}}else{const e=Math.max(-y,Math.min(y,p.x)),i=Math.max(-u,Math.min(u,p.y)),s=l.add(new t(e,i)),n=h.sub(s),r=n.magnitudeSquared();if(r>=c.radius*c.radius)return{isTrigger:!1};const a=Math.sqrt(r),d=a>0?n.scale(1/a):new t(1,0);g=o?d.neg():d,m=c.radius-a,f=s}let x=0;const w=this.collisionPairs.get(e)?.get(i)||this.collisionPairs.get(i)?.get(e);return w&&(x=w.accumulatedNormalImpulse),{isTrigger:n,info:{normal:g,penetration:m,contact:f,accumulatedNormalImpulse:x}}}return{isTrigger:!1}}return{isTrigger:!1}}resolveCollision(t,e,i,s){if(!t.body||!e.body)return;const o=t.body,n=e.body,r=isFinite(o.mass)?1/o.mass:0,a=isFinite(n.mass)?1/n.mass:0,h=r+a;if(0===h)return;const l=n.velocity.sub(o.velocity).dot(i.normal);if(l>0)return;let c=Math.max(o.restitution,n.restitution);Math.abs(l)<.5&&(c=0);const d=(-(1+c)*l+(0===c?.2*Math.max(0,i.penetration-.01)/s:0))/h,y=i.accumulatedNormalImpulse||0;i.accumulatedNormalImpulse=Math.max(0,y+d);const u=i.accumulatedNormalImpulse-y;o.velocity=o.velocity.sub(i.normal.scale(u*r)),n.velocity=n.velocity.add(i.normal.scale(u*a))}}class a{canvas;ctx;scene;lastTime=0;input;physics;paused=!1;constructor(t){this.canvas=t.canvas,this.ctx=this.canvas.getContext("2d"),this.input=t.input??new e({canvas:this.canvas}),this.physics=t.physics??new r}setCanvas(t){this.canvas=t,this.ctx=this.canvas.getContext("2d")}setScene(t){this.scene?.exit(),t.ctx=this.ctx,this.scene=t,this.scene.init()}start(){window.addEventListener("blur",()=>{this.paused=!0}),window.addEventListener("focus",()=>{this.paused=!1,this.lastTime=performance.now()}),requestAnimationFrame(this.loop.bind(this))}loop(t){if(this.paused)return void requestAnimationFrame(this.loop.bind(this));const e=(t-this.lastTime)/1e3;if(this.lastTime=t,!this.scene)throw new Error("Can not run game loop without a scene");this.scene.update(e),this.physics.update(this.scene.entities,e),this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height),this.scene.render(),this.input.update(),requestAnimationFrame(this.loop.bind(this))}}class h{position;rotation;zoom;scene;constructor(e){this.position=e.position??new t(0,0),this.rotation=e.rotation??0,this.zoom=e.zoom??1,this.scene=e.scene}apply(){const t=this.scene.ctx;if(t){const e=t.canvas.width/2,i=t.canvas.height/2;t.translate(e,i),t.scale(this.zoom,this.zoom),t.rotate(this.rotation),t.translate(-this.position.x-e/this.zoom,-this.position.y-i/this.zoom)}}screenToWorld(e){const i=this.scene.ctx;if(i){const s=i.canvas.width/2,o=i.canvas.height/2;let n=e.x-s,r=e.y-o;const a=Math.cos(-this.rotation),h=Math.sin(-this.rotation),l=n*h+r*a,c=(n*a-r*h)/this.zoom,d=l/this.zoom;return new t(c+this.position.x+s/this.zoom,d+this.position.y+o/this.zoom)}return new t(0,0)}}class l{ctx;camera=new h({scene:this});entities=[];init(){}update(t){}exit(){}addEntity(t){this.entities.push(t)}removeEntity(t){this.entities=this.entities.filter(e=>e!==t)}render(){const t=this.ctx;if(t){t.save(),this.camera.apply();for(const e of this.entities)e.render(t);t.restore()}}}class c{sprite;position;rotation;body;collider;constructor(e={}){this.sprite=e.sprite,this.position=e.position??new t(0,0),this.rotation=e.rotation??0,this.body=e.body,this.collider=e.collider}onCollisionEnter;onCollisionStay;onCollisionExit;onTriggerEnter;onTriggerStay;onTriggerExit;render(t){this.sprite&&(t.save(),t.translate(this.position.x,this.position.y),t.rotate(this.rotation),t.drawImage(this.sprite.texture,-this.sprite.width/2,-this.sprite.height/2,this.sprite.width,this.sprite.height),t.restore())}}class d{texture;width;height;constructor(t){this.texture=t.texture,this.width=t.width??this.texture.width,this.height=t.height??this.texture.height}}export{o as BoxCollider,h as Camera,s as CircleCollider,c as Entity,a as Game,e as Input,r as Physics,i as RigidBody,l as Scene,n as SpatialGrid,d as Sprite,t as Vector2};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kippy",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Kippy 2D web game engine for JS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kippy",
|
|
@@ -25,11 +25,15 @@
|
|
|
25
25
|
"license": "Apache-2.0",
|
|
26
26
|
"author": "nguyenphuminh",
|
|
27
27
|
"type": "module",
|
|
28
|
-
"main": "
|
|
28
|
+
"main": "./dist/bundle.min.js",
|
|
29
29
|
"scripts": {
|
|
30
|
-
"
|
|
30
|
+
"build": "npx rollup -c"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
34
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
35
|
+
"rollup": "^4.57.1",
|
|
36
|
+
"tslib": "^2.8.1",
|
|
33
37
|
"typescript": "^5.9.3",
|
|
34
38
|
"vite": "^7.3.1"
|
|
35
39
|
},
|
package/dist/camera.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Vector2 } from "./vector";
|
|
2
|
-
export class Camera {
|
|
3
|
-
position;
|
|
4
|
-
rotation;
|
|
5
|
-
zoom;
|
|
6
|
-
scene;
|
|
7
|
-
constructor(options) {
|
|
8
|
-
this.position = options.position ?? new Vector2(0, 0);
|
|
9
|
-
this.rotation = options.rotation ?? 0;
|
|
10
|
-
this.zoom = options.zoom ?? 1;
|
|
11
|
-
this.scene = options.scene;
|
|
12
|
-
}
|
|
13
|
-
apply() {
|
|
14
|
-
const ctx = this.scene.ctx;
|
|
15
|
-
if (ctx) {
|
|
16
|
-
const cx = ctx.canvas.width / 2;
|
|
17
|
-
const cy = ctx.canvas.height / 2;
|
|
18
|
-
// Move to center for zoom/rotation
|
|
19
|
-
ctx.translate(cx, cy);
|
|
20
|
-
// Zoom and rotate around center
|
|
21
|
-
ctx.scale(this.zoom, this.zoom);
|
|
22
|
-
ctx.rotate(this.rotation);
|
|
23
|
-
// Offset so position appears at top-left
|
|
24
|
-
ctx.translate(-this.position.x - cx / this.zoom, -this.position.y - cy / this.zoom);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
screenToWorld(screenPos) {
|
|
28
|
-
const ctx = this.scene.ctx;
|
|
29
|
-
if (ctx) {
|
|
30
|
-
const cx = ctx.canvas.width / 2;
|
|
31
|
-
const cy = ctx.canvas.height / 2;
|
|
32
|
-
// Offset from center
|
|
33
|
-
let x = screenPos.x - cx;
|
|
34
|
-
let y = screenPos.y - cy;
|
|
35
|
-
// Undo rotation
|
|
36
|
-
const cos = Math.cos(-this.rotation);
|
|
37
|
-
const sin = Math.sin(-this.rotation);
|
|
38
|
-
const rotatedX = x * cos - y * sin;
|
|
39
|
-
const rotatedY = x * sin + y * cos;
|
|
40
|
-
// Undo zoom
|
|
41
|
-
const worldX = rotatedX / this.zoom;
|
|
42
|
-
const worldY = rotatedY / this.zoom;
|
|
43
|
-
// Add camera offset
|
|
44
|
-
return new Vector2(worldX + this.position.x + cx / this.zoom, worldY + this.position.y + cy / this.zoom);
|
|
45
|
-
}
|
|
46
|
-
return new Vector2(0, 0);
|
|
47
|
-
}
|
|
48
|
-
}
|
package/dist/entity.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Vector2 } from "./vector.js";
|
|
2
|
-
export class Entity {
|
|
3
|
-
// Basic entity structure
|
|
4
|
-
sprite;
|
|
5
|
-
position;
|
|
6
|
-
rotation;
|
|
7
|
-
body;
|
|
8
|
-
collider;
|
|
9
|
-
constructor(options = {}) {
|
|
10
|
-
this.sprite = options.sprite;
|
|
11
|
-
this.position = options.position ?? new Vector2(0, 0);
|
|
12
|
-
this.rotation = options.rotation ?? 0;
|
|
13
|
-
this.body = options.body;
|
|
14
|
-
this.collider = options.collider;
|
|
15
|
-
}
|
|
16
|
-
// Event handlers
|
|
17
|
-
onCollisionEnter;
|
|
18
|
-
onCollisionStay;
|
|
19
|
-
onCollisionExit;
|
|
20
|
-
onTriggerEnter;
|
|
21
|
-
onTriggerStay;
|
|
22
|
-
onTriggerExit;
|
|
23
|
-
// Render with sprite
|
|
24
|
-
render(ctx) {
|
|
25
|
-
if (this.sprite) {
|
|
26
|
-
ctx.save();
|
|
27
|
-
ctx.translate(this.position.x, this.position.y);
|
|
28
|
-
ctx.rotate(this.rotation);
|
|
29
|
-
ctx.drawImage(this.sprite.texture, -this.sprite.width / 2, -this.sprite.height / 2, this.sprite.width, this.sprite.height);
|
|
30
|
-
ctx.restore();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
package/dist/game.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { Input } from "./input.js";
|
|
2
|
-
import { Physics } from "./physics.js";
|
|
3
|
-
export class Game {
|
|
4
|
-
canvas;
|
|
5
|
-
ctx;
|
|
6
|
-
scene;
|
|
7
|
-
lastTime = 0;
|
|
8
|
-
input;
|
|
9
|
-
physics;
|
|
10
|
-
paused = false;
|
|
11
|
-
constructor(options) {
|
|
12
|
-
this.canvas = options.canvas;
|
|
13
|
-
this.ctx = this.canvas.getContext("2d");
|
|
14
|
-
this.input = options.input ?? new Input({ canvas: this.canvas });
|
|
15
|
-
this.physics = options.physics ?? new Physics();
|
|
16
|
-
}
|
|
17
|
-
setCanvas(canvas) {
|
|
18
|
-
this.canvas = canvas;
|
|
19
|
-
this.ctx = this.canvas.getContext("2d");
|
|
20
|
-
}
|
|
21
|
-
setScene(scene) {
|
|
22
|
-
this.scene?.exit();
|
|
23
|
-
scene.ctx = this.ctx;
|
|
24
|
-
this.scene = scene;
|
|
25
|
-
this.scene.init();
|
|
26
|
-
}
|
|
27
|
-
start() {
|
|
28
|
-
window.addEventListener("blur", () => {
|
|
29
|
-
this.paused = true;
|
|
30
|
-
});
|
|
31
|
-
window.addEventListener("focus", () => {
|
|
32
|
-
this.paused = false;
|
|
33
|
-
this.lastTime = performance.now(); // Reset!
|
|
34
|
-
});
|
|
35
|
-
requestAnimationFrame(this.loop.bind(this));
|
|
36
|
-
}
|
|
37
|
-
// Game loop
|
|
38
|
-
loop(timestamp) {
|
|
39
|
-
if (this.paused) {
|
|
40
|
-
requestAnimationFrame(this.loop.bind(this));
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const dt = (timestamp - this.lastTime) / 1000;
|
|
44
|
-
this.lastTime = timestamp;
|
|
45
|
-
if (this.scene) {
|
|
46
|
-
// Update input info
|
|
47
|
-
this.input.update();
|
|
48
|
-
// Update game logic
|
|
49
|
-
this.scene.update(dt);
|
|
50
|
-
// Update physics info
|
|
51
|
-
this.physics.update(this.scene.entities, dt);
|
|
52
|
-
// Render
|
|
53
|
-
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
|
54
|
-
this.scene.render();
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
throw new Error("Can not run game loop without a scene");
|
|
58
|
-
}
|
|
59
|
-
requestAnimationFrame(this.loop.bind(this));
|
|
60
|
-
}
|
|
61
|
-
}
|
package/dist/input.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { Vector2 } from "./vector";
|
|
2
|
-
export class Input {
|
|
3
|
-
canvas;
|
|
4
|
-
keys = new Set(); // Key on hold
|
|
5
|
-
keysPressed = new Set(); // Key pressed
|
|
6
|
-
keysReleased = new Set(); // Key released
|
|
7
|
-
pointer = new Vector2(0, 0);
|
|
8
|
-
pointers = new Set(); // Mouse/touch on hold
|
|
9
|
-
pointersPressed = new Set(); // Mouse/touch pressed
|
|
10
|
-
pointersReleased = new Set(); // Mouse/touch released
|
|
11
|
-
constructor(options) {
|
|
12
|
-
this.canvas = options.canvas;
|
|
13
|
-
// Keyboard
|
|
14
|
-
window.addEventListener("keydown", (e) => {
|
|
15
|
-
if (!this.keys.has(e.key)) {
|
|
16
|
-
this.keysPressed.add(e.key);
|
|
17
|
-
}
|
|
18
|
-
this.keys.add(e.key);
|
|
19
|
-
});
|
|
20
|
-
window.addEventListener("keyup", (e) => {
|
|
21
|
-
this.keys.delete(e.key);
|
|
22
|
-
this.keysReleased.add(e.key);
|
|
23
|
-
});
|
|
24
|
-
// Mouse and touch
|
|
25
|
-
this.canvas.addEventListener("mousemove", (e) => {
|
|
26
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
27
|
-
this.pointer.x = e.clientX - rect.left;
|
|
28
|
-
this.pointer.y = e.clientY - rect.top;
|
|
29
|
-
});
|
|
30
|
-
this.canvas.addEventListener("mousedown", (e) => {
|
|
31
|
-
if (!this.pointers.has(e.button)) {
|
|
32
|
-
this.pointersPressed.add(e.button);
|
|
33
|
-
}
|
|
34
|
-
this.pointers.add(e.button);
|
|
35
|
-
});
|
|
36
|
-
this.canvas.addEventListener("mouseup", (e) => {
|
|
37
|
-
this.pointers.delete(e.button);
|
|
38
|
-
this.pointersReleased.add(e.button);
|
|
39
|
-
});
|
|
40
|
-
this.canvas.addEventListener("touchmove", (e) => {
|
|
41
|
-
e.preventDefault();
|
|
42
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
43
|
-
const touch = e.touches[0];
|
|
44
|
-
this.pointer.x = touch.clientX - rect.left;
|
|
45
|
-
this.pointer.y = touch.clientY - rect.top;
|
|
46
|
-
});
|
|
47
|
-
this.canvas.addEventListener("touchstart", (e) => {
|
|
48
|
-
e.preventDefault();
|
|
49
|
-
if (!this.pointers.has(2)) {
|
|
50
|
-
this.pointersPressed.add(2);
|
|
51
|
-
}
|
|
52
|
-
this.pointers.add(2);
|
|
53
|
-
});
|
|
54
|
-
this.canvas.addEventListener("touchend", (e) => {
|
|
55
|
-
e.preventDefault();
|
|
56
|
-
this.pointers.delete(2);
|
|
57
|
-
this.pointersReleased.add(2);
|
|
58
|
-
});
|
|
59
|
-
// Prevent right-click menu
|
|
60
|
-
this.canvas.addEventListener("contextmenu", (e) => {
|
|
61
|
-
e.preventDefault();
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
// Called every frames
|
|
65
|
-
update() {
|
|
66
|
-
this.keysPressed.clear();
|
|
67
|
-
this.keysReleased.clear();
|
|
68
|
-
this.pointersPressed.clear();
|
|
69
|
-
this.pointersReleased.clear();
|
|
70
|
-
}
|
|
71
|
-
// Helper methods
|
|
72
|
-
isKeyDown(key) {
|
|
73
|
-
return this.keys.has(key);
|
|
74
|
-
}
|
|
75
|
-
isKeyPressed(key) {
|
|
76
|
-
return this.keysPressed.has(key);
|
|
77
|
-
}
|
|
78
|
-
isKeyReleased(key) {
|
|
79
|
-
return this.keysReleased.has(key);
|
|
80
|
-
}
|
|
81
|
-
isPointerDown(button = 0) {
|
|
82
|
-
return this.pointers.has(button);
|
|
83
|
-
}
|
|
84
|
-
isPointerPressed(button = 0) {
|
|
85
|
-
return this.pointersPressed.has(button);
|
|
86
|
-
}
|
|
87
|
-
isPointerReleased(button = 0) {
|
|
88
|
-
return this.pointersReleased.has(button);
|
|
89
|
-
}
|
|
90
|
-
}
|
package/dist/physics.js
DELETED
|
@@ -1,530 +0,0 @@
|
|
|
1
|
-
import { Vector2 } from "./vector";
|
|
2
|
-
export class RigidBody {
|
|
3
|
-
velocity;
|
|
4
|
-
rotationVelocity;
|
|
5
|
-
mass;
|
|
6
|
-
inertia;
|
|
7
|
-
force;
|
|
8
|
-
torque;
|
|
9
|
-
restitution;
|
|
10
|
-
sleepThreshold;
|
|
11
|
-
sleepTimeThreshold;
|
|
12
|
-
isSleeping;
|
|
13
|
-
sleepTimer;
|
|
14
|
-
constructor(options = {}) {
|
|
15
|
-
this.velocity = options.velocity ?? new Vector2(0, 0);
|
|
16
|
-
this.rotationVelocity = options.rotationVelocity ?? 0;
|
|
17
|
-
this.mass = options.mass ?? 1;
|
|
18
|
-
this.inertia = options.inertia ?? 1;
|
|
19
|
-
this.force = options.force ?? new Vector2(0, 0);
|
|
20
|
-
this.torque = options.torque ?? 0;
|
|
21
|
-
this.restitution = options.restitution ?? 0;
|
|
22
|
-
this.sleepThreshold = options.sleepThreshold ?? 0.1;
|
|
23
|
-
this.sleepTimeThreshold = options.sleepTimeThreshold ?? 0.5;
|
|
24
|
-
this.isSleeping = options.isSleeping ?? false;
|
|
25
|
-
this.sleepTimer = options.sleepTimer ?? 0;
|
|
26
|
-
}
|
|
27
|
-
wake() {
|
|
28
|
-
this.isSleeping = false;
|
|
29
|
-
this.sleepTimer = 0;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
export class CircleCollider {
|
|
33
|
-
radius;
|
|
34
|
-
offset;
|
|
35
|
-
isTrigger;
|
|
36
|
-
layer;
|
|
37
|
-
mask;
|
|
38
|
-
constructor(options) {
|
|
39
|
-
this.radius = options.radius;
|
|
40
|
-
this.offset = options.offset ?? new Vector2(0, 0);
|
|
41
|
-
this.isTrigger = options.isTrigger ?? false;
|
|
42
|
-
this.layer = options.layer ?? (1 << 0);
|
|
43
|
-
this.mask = options.mask ?? 0xFFFFFFFF;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
export class BoxCollider {
|
|
47
|
-
width;
|
|
48
|
-
height;
|
|
49
|
-
offset;
|
|
50
|
-
isTrigger;
|
|
51
|
-
layer;
|
|
52
|
-
mask;
|
|
53
|
-
constructor(options) {
|
|
54
|
-
this.width = options.width;
|
|
55
|
-
this.height = options.height;
|
|
56
|
-
this.offset = options.offset ?? new Vector2(0, 0);
|
|
57
|
-
this.isTrigger = options.isTrigger ?? false;
|
|
58
|
-
this.layer = options.layer ?? (1 << 0);
|
|
59
|
-
this.mask = options.mask ?? 0xFFFFFFFF;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
export class SpatialGrid {
|
|
63
|
-
cellSize;
|
|
64
|
-
grid;
|
|
65
|
-
constructor(options = {}) {
|
|
66
|
-
this.cellSize = options.cellSize || 100;
|
|
67
|
-
this.grid = options.grid || new Map();
|
|
68
|
-
}
|
|
69
|
-
clear() {
|
|
70
|
-
this.grid.clear();
|
|
71
|
-
}
|
|
72
|
-
// Auto update cell size
|
|
73
|
-
adaptCellSize(entities) {
|
|
74
|
-
// Sample entity sizes
|
|
75
|
-
const sizes = [];
|
|
76
|
-
for (const entity of entities) {
|
|
77
|
-
const bounds = this.getEntityBounds(entity);
|
|
78
|
-
const width = bounds.maxX - bounds.minX;
|
|
79
|
-
const height = bounds.maxY - bounds.minY;
|
|
80
|
-
const maxDimension = Math.max(width, height);
|
|
81
|
-
sizes.push(maxDimension);
|
|
82
|
-
}
|
|
83
|
-
// Use median or 75th percentile (ignore outliers)
|
|
84
|
-
sizes.sort((a, b) => a - b);
|
|
85
|
-
const percentile75 = sizes[Math.floor(sizes.length * 0.75)];
|
|
86
|
-
// Multiply by 2-3x (common heuristic)
|
|
87
|
-
this.cellSize = percentile75 * 2.5;
|
|
88
|
-
}
|
|
89
|
-
// Insert entity into grid
|
|
90
|
-
insert(entity) {
|
|
91
|
-
if (entity.collider) {
|
|
92
|
-
// Get bounds of entity ('s collider)
|
|
93
|
-
const bounds = this.getEntityBounds(entity);
|
|
94
|
-
// Insert into all cells it overlaps
|
|
95
|
-
const minCellX = Math.floor(bounds.minX / this.cellSize);
|
|
96
|
-
const maxCellX = Math.floor(bounds.maxX / this.cellSize);
|
|
97
|
-
const minCellY = Math.floor(bounds.minY / this.cellSize);
|
|
98
|
-
const maxCellY = Math.floor(bounds.maxY / this.cellSize);
|
|
99
|
-
for (let cx = minCellX; cx <= maxCellX; cx++) {
|
|
100
|
-
for (let cy = minCellY; cy <= maxCellY; cy++) {
|
|
101
|
-
const key = `${cx},${cy}`;
|
|
102
|
-
if (!this.grid.has(key)) {
|
|
103
|
-
this.grid.set(key, new Set());
|
|
104
|
-
}
|
|
105
|
-
this.grid.get(key).add(entity);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// Get nearby entities (checks 3x3 grid around entity)
|
|
111
|
-
getNearby(entity) {
|
|
112
|
-
const centerX = Math.floor(entity.position.x / this.cellSize);
|
|
113
|
-
const centerY = Math.floor(entity.position.y / this.cellSize);
|
|
114
|
-
const nearby = new Set();
|
|
115
|
-
// Check 3x3 grid of cells
|
|
116
|
-
for (let dx = -1; dx <= 1; dx++) {
|
|
117
|
-
for (let dy = -1; dy <= 1; dy++) {
|
|
118
|
-
const key = `${centerX + dx},${centerY + dy}`;
|
|
119
|
-
const cell = this.grid.get(key);
|
|
120
|
-
if (cell) {
|
|
121
|
-
for (const e of cell) {
|
|
122
|
-
if (e !== entity) {
|
|
123
|
-
nearby.add(e);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return Array.from(nearby);
|
|
130
|
-
}
|
|
131
|
-
// Helper to get entity bounds
|
|
132
|
-
getEntityBounds(entity) {
|
|
133
|
-
if (entity.collider instanceof CircleCollider) {
|
|
134
|
-
const radius = entity.collider.radius;
|
|
135
|
-
return {
|
|
136
|
-
minX: entity.position.x - radius,
|
|
137
|
-
maxX: entity.position.x + radius,
|
|
138
|
-
minY: entity.position.y - radius,
|
|
139
|
-
maxY: entity.position.y + radius
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
else if (entity.collider instanceof BoxCollider) {
|
|
143
|
-
const halfWidth = entity.collider.width;
|
|
144
|
-
const halfHeight = entity.collider.height;
|
|
145
|
-
return {
|
|
146
|
-
minX: entity.position.x - halfWidth,
|
|
147
|
-
maxX: entity.position.x + halfWidth,
|
|
148
|
-
minY: entity.position.y - halfHeight,
|
|
149
|
-
maxY: entity.position.y + halfHeight
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
throw new Error("Collider type not supported");
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
// Physics engine
|
|
158
|
-
export class Physics {
|
|
159
|
-
collisionPairs = new Map(); // To store past collisions
|
|
160
|
-
spatialGrid = new SpatialGrid();
|
|
161
|
-
entityCount = 0;
|
|
162
|
-
update(entities, dt) {
|
|
163
|
-
// Update velocity/apply force
|
|
164
|
-
for (const entity of entities) {
|
|
165
|
-
if (entity.body instanceof RigidBody) {
|
|
166
|
-
// Wake if force applied
|
|
167
|
-
if (entity.body.force.magnitudeSquared() > 0 || entity.body.torque !== 0) {
|
|
168
|
-
entity.body.wake();
|
|
169
|
-
}
|
|
170
|
-
// Skip sleeping bodies with no forces
|
|
171
|
-
if (entity.body.isSleeping)
|
|
172
|
-
continue;
|
|
173
|
-
// Acceleration/apply force
|
|
174
|
-
entity.body.velocity.x += entity.body.force.x / entity.body.mass * dt;
|
|
175
|
-
entity.body.velocity.y += entity.body.force.y / entity.body.mass * dt;
|
|
176
|
-
entity.body.rotationVelocity += entity.body.torque / entity.body.inertia * dt;
|
|
177
|
-
// Clear force
|
|
178
|
-
entity.body.force.x = 0;
|
|
179
|
-
entity.body.force.y = 0;
|
|
180
|
-
entity.body.torque = 0;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
// Rebuild spatial grid for collision handling
|
|
184
|
-
this.spatialGrid.clear();
|
|
185
|
-
if (this.entityCount !== entities.length) {
|
|
186
|
-
this.entityCount = entities.length;
|
|
187
|
-
this.spatialGrid.adaptCellSize(entities);
|
|
188
|
-
}
|
|
189
|
-
for (const entity of entities) {
|
|
190
|
-
this.spatialGrid.insert(entity);
|
|
191
|
-
}
|
|
192
|
-
// Handle collisions - PHASE 1: Detect and collect all contacts
|
|
193
|
-
const currentCollisions = new Map(); // To update this.collisionPairs and check duplicates
|
|
194
|
-
const contacts = [];
|
|
195
|
-
for (const entity of entities) {
|
|
196
|
-
if (entity.collider) {
|
|
197
|
-
const nearby = this.spatialGrid.getNearby(entity);
|
|
198
|
-
for (const other of nearby) {
|
|
199
|
-
if (other.collider) {
|
|
200
|
-
// Check duplicate
|
|
201
|
-
if ((currentCollisions.has(entity) && currentCollisions.get(entity)?.has(other)) ||
|
|
202
|
-
(currentCollisions.has(other) && currentCollisions.get(other)?.has(entity))) {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
// Check collision
|
|
206
|
-
const collisionResult = this.checkCollision(entity, other);
|
|
207
|
-
if (collisionResult.info) {
|
|
208
|
-
// Wake if contacting an awake body
|
|
209
|
-
if (!entity.body?.isSleeping || !other.body?.isSleeping) {
|
|
210
|
-
entity.body?.wake();
|
|
211
|
-
other.body?.wake();
|
|
212
|
-
}
|
|
213
|
-
// Track collision
|
|
214
|
-
if (!currentCollisions.has(entity)) {
|
|
215
|
-
currentCollisions.set(entity, new Map());
|
|
216
|
-
}
|
|
217
|
-
currentCollisions.get(entity)?.set(other, collisionResult.info);
|
|
218
|
-
// Check if this is a new collision
|
|
219
|
-
const wasColliding = (this.collisionPairs.get(entity)?.has(other) ||
|
|
220
|
-
this.collisionPairs.get(other)?.has(entity));
|
|
221
|
-
if (!wasColliding) {
|
|
222
|
-
// ENTER
|
|
223
|
-
if (collisionResult.isTrigger) {
|
|
224
|
-
entity.onTriggerEnter?.(other);
|
|
225
|
-
other.onTriggerEnter?.(entity);
|
|
226
|
-
}
|
|
227
|
-
else {
|
|
228
|
-
entity.onCollisionEnter?.(other, collisionResult.info);
|
|
229
|
-
other.onCollisionEnter?.(entity, {
|
|
230
|
-
...collisionResult.info,
|
|
231
|
-
normal: new Vector2(-collisionResult.info.normal.x, -collisionResult.info.normal.y)
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
// STAY
|
|
237
|
-
if (collisionResult.isTrigger) {
|
|
238
|
-
entity.onTriggerStay?.(other);
|
|
239
|
-
other.onTriggerStay?.(entity);
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
entity.onCollisionStay?.(other, collisionResult.info);
|
|
243
|
-
other.onCollisionStay?.(entity, {
|
|
244
|
-
...collisionResult.info,
|
|
245
|
-
normal: new Vector2(-collisionResult.info.normal.x, -collisionResult.info.normal.y)
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
// Collect contact for solving
|
|
250
|
-
if (!collisionResult.isTrigger) {
|
|
251
|
-
contacts.push({
|
|
252
|
-
entityA: entity,
|
|
253
|
-
entityB: other,
|
|
254
|
-
info: collisionResult.info
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
// PHASE 2: Iteratively solve all contacts together
|
|
263
|
-
for (let iteration = 0; iteration < 6; iteration++) {
|
|
264
|
-
for (const contact of contacts) {
|
|
265
|
-
this.resolveCollision(contact.entityA, contact.entityB, contact.info, dt);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
// EXIT
|
|
269
|
-
for (const [entity, others] of this.collisionPairs) {
|
|
270
|
-
for (const [other, lastInfo] of others) {
|
|
271
|
-
// Check if still colliding
|
|
272
|
-
const stillColliding = (currentCollisions.get(entity)?.has(other) ||
|
|
273
|
-
currentCollisions.get(other)?.has(entity));
|
|
274
|
-
if (!stillColliding) {
|
|
275
|
-
// Determine if was a trigger
|
|
276
|
-
const wasTrigger = entity.collider?.isTrigger || other.collider?.isTrigger;
|
|
277
|
-
if (wasTrigger) {
|
|
278
|
-
entity.onTriggerExit?.(other);
|
|
279
|
-
other.onTriggerExit?.(entity);
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
entity.onCollisionExit?.(other, lastInfo);
|
|
283
|
-
other.onCollisionExit?.(entity, {
|
|
284
|
-
...lastInfo,
|
|
285
|
-
normal: new Vector2(-lastInfo.normal.x, -lastInfo.normal.y)
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
// Update tracked collisions
|
|
292
|
-
this.collisionPairs = currentCollisions;
|
|
293
|
-
// Update position
|
|
294
|
-
for (const entity of entities) {
|
|
295
|
-
if (entity.body instanceof RigidBody) {
|
|
296
|
-
// Skip sleeping bodies
|
|
297
|
-
if (entity.body.isSleeping)
|
|
298
|
-
continue;
|
|
299
|
-
// Positional update
|
|
300
|
-
entity.position.x += entity.body.velocity.x * dt;
|
|
301
|
-
entity.position.y += entity.body.velocity.y * dt;
|
|
302
|
-
entity.rotation += entity.body.rotationVelocity * dt;
|
|
303
|
-
// Sleep accumulation
|
|
304
|
-
const speed = entity.body.velocity.magnitude();
|
|
305
|
-
const angularSpeed = Math.abs(entity.body.rotationVelocity);
|
|
306
|
-
if (speed < entity.body.sleepThreshold && angularSpeed < entity.body.sleepThreshold) {
|
|
307
|
-
entity.body.sleepTimer += dt;
|
|
308
|
-
if (entity.body.sleepTimer >= entity.body.sleepTimeThreshold) {
|
|
309
|
-
// Go to sleep
|
|
310
|
-
entity.body.isSleeping = true;
|
|
311
|
-
entity.body.velocity.x = 0;
|
|
312
|
-
entity.body.velocity.y = 0;
|
|
313
|
-
entity.body.rotationVelocity = 0;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
else {
|
|
317
|
-
// Reset timer if moving too fast
|
|
318
|
-
entity.body.sleepTimer = 0;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
checkCollision(entityA, entityB) {
|
|
324
|
-
if (entityA.collider && entityB.collider) {
|
|
325
|
-
// Layer/mask filtering
|
|
326
|
-
if ((entityA.collider.mask & entityB.collider.layer) === 0 ||
|
|
327
|
-
(entityB.collider.mask & entityA.collider.layer) === 0) {
|
|
328
|
-
return {
|
|
329
|
-
isTrigger: false
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
// Get trigger info
|
|
333
|
-
const isTrigger = entityA.collider.isTrigger || entityB.collider.isTrigger;
|
|
334
|
-
// Check collision
|
|
335
|
-
const posA = entityA.position.add(entityA.collider.offset);
|
|
336
|
-
const posB = entityB.position.add(entityB.collider.offset);
|
|
337
|
-
// Check different types of colliders
|
|
338
|
-
if (entityA.collider instanceof CircleCollider && entityB.collider instanceof CircleCollider) {
|
|
339
|
-
const distance = posA.distance(posB);
|
|
340
|
-
const radiusSum = entityA.collider.radius + entityB.collider.radius;
|
|
341
|
-
if (distance >= radiusSum) {
|
|
342
|
-
return {
|
|
343
|
-
isTrigger: false
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
const penetration = radiusSum - distance;
|
|
347
|
-
const direction = posB.sub(posA);
|
|
348
|
-
let normal = distance > 0 ? direction.scale(1 / distance) : new Vector2(1, 0);
|
|
349
|
-
// Warm starting - copy accumulated impulse from last frame if contact persists
|
|
350
|
-
let accumulatedImpulse = 0;
|
|
351
|
-
const lastContactInfo = this.collisionPairs.get(entityA)?.get(entityB) ||
|
|
352
|
-
this.collisionPairs.get(entityB)?.get(entityA);
|
|
353
|
-
if (lastContactInfo) {
|
|
354
|
-
accumulatedImpulse = lastContactInfo.accumulatedNormalImpulse;
|
|
355
|
-
}
|
|
356
|
-
return {
|
|
357
|
-
isTrigger,
|
|
358
|
-
info: {
|
|
359
|
-
normal,
|
|
360
|
-
penetration,
|
|
361
|
-
contact: posA.add(normal.scale(entityA.collider.radius)),
|
|
362
|
-
accumulatedNormalImpulse: accumulatedImpulse
|
|
363
|
-
}
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
else if (entityA.collider instanceof BoxCollider && entityB.collider instanceof BoxCollider) {
|
|
367
|
-
const halfAx = entityA.collider.width / 2;
|
|
368
|
-
const halfAy = entityA.collider.height / 2;
|
|
369
|
-
const halfBx = entityB.collider.width / 2;
|
|
370
|
-
const halfBy = entityB.collider.height / 2;
|
|
371
|
-
const delta = posB.sub(posA);
|
|
372
|
-
const overlapX = halfAx + halfBx - Math.abs(delta.x);
|
|
373
|
-
const overlapY = halfAy + halfBy - Math.abs(delta.y);
|
|
374
|
-
if (overlapX <= 0 || overlapY <= 0) {
|
|
375
|
-
return { isTrigger: false };
|
|
376
|
-
}
|
|
377
|
-
let normal;
|
|
378
|
-
let penetration;
|
|
379
|
-
if (overlapX < overlapY) {
|
|
380
|
-
penetration = overlapX;
|
|
381
|
-
normal = new Vector2(delta.x < 0 ? -1 : 1, 0);
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
penetration = overlapY;
|
|
385
|
-
normal = new Vector2(0, delta.y < 0 ? -1 : 1);
|
|
386
|
-
}
|
|
387
|
-
// Contact point: center of overlapping region
|
|
388
|
-
const contactX = posA.x + Math.sign(delta.x) * (halfAx - overlapX / 2);
|
|
389
|
-
const contactY = posA.y + Math.sign(delta.y) * (halfAy - overlapY / 2);
|
|
390
|
-
let accumulatedImpulse = 0;
|
|
391
|
-
const lastContactInfo = this.collisionPairs.get(entityA)?.get(entityB) ||
|
|
392
|
-
this.collisionPairs.get(entityB)?.get(entityA);
|
|
393
|
-
if (lastContactInfo) {
|
|
394
|
-
accumulatedImpulse = lastContactInfo.accumulatedNormalImpulse;
|
|
395
|
-
}
|
|
396
|
-
return {
|
|
397
|
-
isTrigger,
|
|
398
|
-
info: {
|
|
399
|
-
normal,
|
|
400
|
-
penetration,
|
|
401
|
-
contact: new Vector2(contactX, contactY),
|
|
402
|
-
accumulatedNormalImpulse: accumulatedImpulse
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
else if ((entityA.collider instanceof CircleCollider && entityB.collider instanceof BoxCollider) ||
|
|
407
|
-
(entityA.collider instanceof BoxCollider && entityB.collider instanceof CircleCollider)) {
|
|
408
|
-
const isCircleA = entityA.collider instanceof CircleCollider;
|
|
409
|
-
const circlePos = isCircleA ? posA : posB;
|
|
410
|
-
const boxPos = isCircleA ? posB : posA;
|
|
411
|
-
const circle = (isCircleA ? entityA : entityB).collider;
|
|
412
|
-
const box = (isCircleA ? entityB : entityA).collider;
|
|
413
|
-
const halfW = box.width / 2;
|
|
414
|
-
const halfH = box.height / 2;
|
|
415
|
-
// Box center -> circle center
|
|
416
|
-
const delta = circlePos.sub(boxPos);
|
|
417
|
-
const inside = Math.abs(delta.x) < halfW && Math.abs(delta.y) < halfH;
|
|
418
|
-
let normal;
|
|
419
|
-
let penetration;
|
|
420
|
-
let contact;
|
|
421
|
-
if (inside) {
|
|
422
|
-
const overlapX = halfW - Math.abs(delta.x);
|
|
423
|
-
const overlapY = halfH - Math.abs(delta.y);
|
|
424
|
-
if (overlapX < overlapY) {
|
|
425
|
-
const sign = delta.x < 0 ? -1 : 1;
|
|
426
|
-
const boxToCircle = new Vector2(sign, 0);
|
|
427
|
-
normal = isCircleA ? boxToCircle.neg() : boxToCircle; // A→B
|
|
428
|
-
penetration = circle.radius + overlapX;
|
|
429
|
-
contact = new Vector2(boxPos.x + sign * halfW, circlePos.y);
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
const sign = delta.y < 0 ? -1 : 1;
|
|
433
|
-
const boxToCircle = new Vector2(0, sign);
|
|
434
|
-
normal = isCircleA ? boxToCircle.neg() : boxToCircle;
|
|
435
|
-
penetration = circle.radius + overlapY;
|
|
436
|
-
contact = new Vector2(circlePos.x, boxPos.y + sign * halfH);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
const clampedX = Math.max(-halfW, Math.min(halfW, delta.x));
|
|
441
|
-
const clampedY = Math.max(-halfH, Math.min(halfH, delta.y));
|
|
442
|
-
const closest = boxPos.add(new Vector2(clampedX, clampedY));
|
|
443
|
-
const diff = circlePos.sub(closest);
|
|
444
|
-
const distSq = diff.magnitudeSquared();
|
|
445
|
-
if (distSq >= circle.radius * circle.radius) {
|
|
446
|
-
return { isTrigger: false };
|
|
447
|
-
}
|
|
448
|
-
const dist = Math.sqrt(distSq);
|
|
449
|
-
const boxToCircle = dist > 0 ? diff.scale(1 / dist) : new Vector2(1, 0);
|
|
450
|
-
normal = isCircleA ? boxToCircle.neg() : boxToCircle;
|
|
451
|
-
penetration = circle.radius - dist;
|
|
452
|
-
contact = closest;
|
|
453
|
-
}
|
|
454
|
-
let accumulatedImpulse = 0;
|
|
455
|
-
const lastContactInfo = this.collisionPairs.get(entityA)?.get(entityB) ||
|
|
456
|
-
this.collisionPairs.get(entityB)?.get(entityA);
|
|
457
|
-
if (lastContactInfo) {
|
|
458
|
-
accumulatedImpulse = lastContactInfo.accumulatedNormalImpulse;
|
|
459
|
-
}
|
|
460
|
-
return {
|
|
461
|
-
isTrigger,
|
|
462
|
-
info: {
|
|
463
|
-
normal,
|
|
464
|
-
penetration,
|
|
465
|
-
contact,
|
|
466
|
-
accumulatedNormalImpulse: accumulatedImpulse
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
return {
|
|
471
|
-
isTrigger: false
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
return {
|
|
475
|
-
isTrigger: false
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
resolveCollision(entityA, entityB, info, dt) {
|
|
479
|
-
if (!entityA.body || !entityB.body)
|
|
480
|
-
return;
|
|
481
|
-
const bodyA = entityA.body;
|
|
482
|
-
const bodyB = entityB.body;
|
|
483
|
-
// Check for infinite mass
|
|
484
|
-
const invMassA = isFinite(bodyA.mass) ? 1 / bodyA.mass : 0;
|
|
485
|
-
const invMassB = isFinite(bodyB.mass) ? 1 / bodyB.mass : 0;
|
|
486
|
-
const totalInvMass = invMassA + invMassB;
|
|
487
|
-
// If both infinite mass, do nothing
|
|
488
|
-
if (totalInvMass === 0)
|
|
489
|
-
return;
|
|
490
|
-
// Relative velocity
|
|
491
|
-
const relVel = bodyB.velocity.sub(bodyA.velocity);
|
|
492
|
-
// Velocity along normal
|
|
493
|
-
const velAlongNormal = relVel.dot(info.normal);
|
|
494
|
-
// Don't resolve if velocities are separating
|
|
495
|
-
if (velAlongNormal > 0)
|
|
496
|
-
return;
|
|
497
|
-
// Restitution (bounciness) with slop
|
|
498
|
-
const restitutionSlop = 0.5; // Kill bounce below 0.5 unit/sec
|
|
499
|
-
let restitution = Math.max(bodyA.restitution, bodyB.restitution);
|
|
500
|
-
// No bounce for slow collisions (helps objects settle)
|
|
501
|
-
if (Math.abs(velAlongNormal) < restitutionSlop) {
|
|
502
|
-
restitution = 0;
|
|
503
|
-
}
|
|
504
|
-
// Position correction with slop (Box2D/PhysX style)
|
|
505
|
-
const slop = 0.01; // Allow small penetration without correction
|
|
506
|
-
const baumgarte = 0.2; // Correct 20% of penetration per frame
|
|
507
|
-
// Bias velocity - only apply when no bounce (restitution = 0)
|
|
508
|
-
// When bouncing, let restitution handle it naturally
|
|
509
|
-
const biasVelocity = restitution === 0 ? (Math.max(0, info.penetration - slop) * baumgarte) / dt : 0;
|
|
510
|
-
// Calculate impulse using inverse mass
|
|
511
|
-
const jn = (-(1 + restitution) * velAlongNormal + biasVelocity) / totalInvMass;
|
|
512
|
-
// Clamp accumulated impulse
|
|
513
|
-
const oldImpulse = info.accumulatedNormalImpulse || 0;
|
|
514
|
-
info.accumulatedNormalImpulse = Math.max(0, oldImpulse + jn);
|
|
515
|
-
const actualImpulse = info.accumulatedNormalImpulse - oldImpulse;
|
|
516
|
-
// Apply actual impulse
|
|
517
|
-
bodyA.velocity = bodyA.velocity.sub(info.normal.scale(actualImpulse * invMassA));
|
|
518
|
-
bodyB.velocity = bodyB.velocity.add(info.normal.scale(actualImpulse * invMassB));
|
|
519
|
-
// Apply angular impulse (torque from contact point)
|
|
520
|
-
// Disabled for now because colliders must rotate too and there isn't polygon collider solver for now
|
|
521
|
-
/*const rA = info.contact.sub(entityA.position);
|
|
522
|
-
const rB = info.contact.sub(entityB.position);
|
|
523
|
-
|
|
524
|
-
const angularImpulseA = rA.cross(info.normal.scale(-actualImpulse));
|
|
525
|
-
const angularImpulseB = rB.cross(info.normal.scale(actualImpulse));
|
|
526
|
-
|
|
527
|
-
bodyA.rotationVelocity += angularImpulseA * (isFinite(bodyA.inertia) ? 1 / bodyA.inertia : 0);
|
|
528
|
-
bodyB.rotationVelocity += angularImpulseB * (isFinite(bodyB.inertia) ? 1 / bodyB.inertia : 0);*/
|
|
529
|
-
}
|
|
530
|
-
}
|
package/dist/scene.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Camera } from "./camera.js";
|
|
2
|
-
export class Scene {
|
|
3
|
-
ctx;
|
|
4
|
-
camera = new Camera({ scene: this });
|
|
5
|
-
entities = [];
|
|
6
|
-
init() { }
|
|
7
|
-
update(deltaTime) { }
|
|
8
|
-
exit() { }
|
|
9
|
-
addEntity(entity) {
|
|
10
|
-
this.entities.push(entity);
|
|
11
|
-
}
|
|
12
|
-
removeEntity(entity) {
|
|
13
|
-
this.entities = this.entities.filter(childEntities => childEntities !== entity);
|
|
14
|
-
}
|
|
15
|
-
render() {
|
|
16
|
-
const ctx = this.ctx;
|
|
17
|
-
if (ctx) {
|
|
18
|
-
// Preserve canvas context
|
|
19
|
-
ctx.save();
|
|
20
|
-
// Apply camera config (position, zoom, rotate)
|
|
21
|
-
this.camera.apply();
|
|
22
|
-
for (const entity of this.entities) {
|
|
23
|
-
entity.render(ctx);
|
|
24
|
-
}
|
|
25
|
-
// Restore so camera/entity stuff does not affect original context
|
|
26
|
-
ctx.restore();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
package/dist/sprite.js
DELETED
package/dist/vector.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
export class Vector2 {
|
|
2
|
-
x;
|
|
3
|
-
y;
|
|
4
|
-
static ZERO = new Vector2(0, 0);
|
|
5
|
-
static ONE = new Vector2(1, 1);
|
|
6
|
-
static UP = new Vector2(0, -1);
|
|
7
|
-
static DOWN = new Vector2(0, 1);
|
|
8
|
-
static LEFT = new Vector2(-1, 0);
|
|
9
|
-
static RIGHT = new Vector2(1, 0);
|
|
10
|
-
constructor(x, y) {
|
|
11
|
-
this.x = x;
|
|
12
|
-
this.y = y;
|
|
13
|
-
}
|
|
14
|
-
toString() {
|
|
15
|
-
return `Vector2(${this.x}, ${this.y})`;
|
|
16
|
-
}
|
|
17
|
-
add(other) {
|
|
18
|
-
return new Vector2(this.x + other.x, this.y + other.y);
|
|
19
|
-
}
|
|
20
|
-
sub(other) {
|
|
21
|
-
return new Vector2(this.x - other.x, this.y - other.y);
|
|
22
|
-
}
|
|
23
|
-
mul(other) {
|
|
24
|
-
return new Vector2(this.x * other.x, this.y * other.y);
|
|
25
|
-
}
|
|
26
|
-
div(other) {
|
|
27
|
-
return new Vector2(this.x / other.x, this.y / other.y);
|
|
28
|
-
}
|
|
29
|
-
neg() {
|
|
30
|
-
return new Vector2(-this.x, -this.y);
|
|
31
|
-
}
|
|
32
|
-
scale(scale) {
|
|
33
|
-
return new Vector2(this.x * scale, this.y * scale);
|
|
34
|
-
}
|
|
35
|
-
magnitude() {
|
|
36
|
-
return Math.sqrt(this.x * this.x + this.y * this.y);
|
|
37
|
-
}
|
|
38
|
-
magnitudeSquared() {
|
|
39
|
-
return this.x * this.x + this.y * this.y;
|
|
40
|
-
}
|
|
41
|
-
normalize() {
|
|
42
|
-
const mag = this.magnitude();
|
|
43
|
-
return mag > 0 ? new Vector2(this.x / mag, this.y / mag) : new Vector2(0, 0);
|
|
44
|
-
}
|
|
45
|
-
dot(other) {
|
|
46
|
-
return this.x * other.x + this.y * other.y;
|
|
47
|
-
}
|
|
48
|
-
cross(other) {
|
|
49
|
-
return this.x * other.y - this.y * other.x;
|
|
50
|
-
}
|
|
51
|
-
project(other) {
|
|
52
|
-
const scalar = this.dot(other) / other.magnitudeSquared();
|
|
53
|
-
return other.scale(scalar);
|
|
54
|
-
}
|
|
55
|
-
min(other) {
|
|
56
|
-
return new Vector2(Math.min(this.x, other.x), Math.min(this.y, other.y));
|
|
57
|
-
}
|
|
58
|
-
max(other) {
|
|
59
|
-
return new Vector2(Math.max(this.x, other.x), Math.max(this.y, other.y));
|
|
60
|
-
}
|
|
61
|
-
floor() {
|
|
62
|
-
return new Vector2(Math.floor(this.x), Math.floor(this.y));
|
|
63
|
-
}
|
|
64
|
-
ceil() {
|
|
65
|
-
return new Vector2(Math.ceil(this.x), Math.ceil(this.y));
|
|
66
|
-
}
|
|
67
|
-
round() {
|
|
68
|
-
return new Vector2(Math.round(this.x), Math.round(this.y));
|
|
69
|
-
}
|
|
70
|
-
distance(other) {
|
|
71
|
-
return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2);
|
|
72
|
-
}
|
|
73
|
-
distanceSquared(other) {
|
|
74
|
-
return (this.x - other.x) ** 2 + (this.y - other.y) ** 2;
|
|
75
|
-
}
|
|
76
|
-
copy() {
|
|
77
|
-
return new Vector2(this.x, this.y);
|
|
78
|
-
}
|
|
79
|
-
lerp(other, scale) {
|
|
80
|
-
return this.add(other.sub(this).scale(scale));
|
|
81
|
-
}
|
|
82
|
-
clamp(maxLength) {
|
|
83
|
-
const mag = this.magnitude();
|
|
84
|
-
return mag > maxLength ? this.scale(maxLength / mag) : this.copy();
|
|
85
|
-
}
|
|
86
|
-
rotate(angle) {
|
|
87
|
-
const cos = Math.cos(angle);
|
|
88
|
-
const sin = Math.sin(angle);
|
|
89
|
-
return new Vector2(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
|
|
90
|
-
}
|
|
91
|
-
orthogonal() {
|
|
92
|
-
return new Vector2(-this.y, this.x);
|
|
93
|
-
}
|
|
94
|
-
angle() {
|
|
95
|
-
return Math.atan2(this.y, this.x);
|
|
96
|
-
}
|
|
97
|
-
angleTo(other) {
|
|
98
|
-
return Math.atan2(other.y - this.y, other.x - this.x);
|
|
99
|
-
}
|
|
100
|
-
reflect(normal) {
|
|
101
|
-
const d = this.dot(normal);
|
|
102
|
-
return this.sub(normal.scale(2 * d));
|
|
103
|
-
}
|
|
104
|
-
equals(other) {
|
|
105
|
-
return this.x === other.x && this.y === other.y;
|
|
106
|
-
}
|
|
107
|
-
}
|
package/index.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export * from "./dist/game.js";
|
|
2
|
-
export * from "./dist/scene.js";
|
|
3
|
-
export * from "./dist/entity.js";
|
|
4
|
-
export * from "./dist/sprite.js";
|
|
5
|
-
export * from "./dist/input.js";
|
|
6
|
-
export * from "./dist/physics.js";
|
|
7
|
-
export * from "./dist/vector.js";
|
|
8
|
-
export * from "./dist/camera.js";
|
package/index.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export * from "./dist/game.js";
|
|
2
|
-
export * from "./dist/scene.js";
|
|
3
|
-
export * from "./dist/entity.js";
|
|
4
|
-
export * from "./dist/sprite.js";
|
|
5
|
-
export * from "./dist/input.js";
|
|
6
|
-
export * from "./dist/physics.js";
|
|
7
|
-
export * from "./dist/vector.js";
|
|
8
|
-
export * from "./dist/camera.js";
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|