kippy 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- ## Setup
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:
@@ -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};
@@ -0,0 +1,8 @@
1
+ export * from "./game.js";
2
+ export * from "./scene.js";
3
+ export * from "./entity.js";
4
+ export * from "./sprite.js";
5
+ export * from "./input.js";
6
+ export * from "./physics.js";
7
+ export * from "./vector.js";
8
+ export * from "./camera.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kippy",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
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": "index.js",
28
+ "main": "./dist/bundle.min.js",
29
29
  "scripts": {
30
- "test": "echo \"Error: no test specified\" && exit 1"
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
  },
@@ -39,5 +43,6 @@
39
43
  "index.js",
40
44
  "LICENSE",
41
45
  "README.md"
42
- ]
46
+ ],
47
+ "types": "dist/types/index.d.ts"
43
48
  }
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 game logic
47
- this.scene.update(dt);
48
- // Update physics info
49
- this.physics.update(this.scene.entities, dt);
50
- // Render
51
- this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
52
- this.scene.render();
53
- // Update input info
54
- this.input.update();
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,533 +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 or velocity above threshold
167
- if (entity.body.force.magnitudeSquared() > 0 ||
168
- entity.body.torque !== 0 ||
169
- entity.body.velocity.magnitudeSquared() > entity.body.sleepThreshold ** 2 ||
170
- Math.abs(entity.body.rotationVelocity) > entity.body.sleepThreshold) {
171
- entity.body.wake();
172
- }
173
- // Skip sleeping bodies with no forces
174
- if (entity.body.isSleeping)
175
- continue;
176
- // Acceleration/apply force
177
- entity.body.velocity.x += entity.body.force.x / entity.body.mass * dt;
178
- entity.body.velocity.y += entity.body.force.y / entity.body.mass * dt;
179
- entity.body.rotationVelocity += entity.body.torque / entity.body.inertia * dt;
180
- // Clear force
181
- entity.body.force.x = 0;
182
- entity.body.force.y = 0;
183
- entity.body.torque = 0;
184
- }
185
- }
186
- // Rebuild spatial grid for collision handling
187
- this.spatialGrid.clear();
188
- if (this.entityCount !== entities.length) {
189
- this.entityCount = entities.length;
190
- this.spatialGrid.adaptCellSize(entities);
191
- }
192
- for (const entity of entities) {
193
- this.spatialGrid.insert(entity);
194
- }
195
- // Handle collisions - PHASE 1: Detect and collect all contacts
196
- const currentCollisions = new Map(); // To update this.collisionPairs and check duplicates
197
- const contacts = [];
198
- for (const entity of entities) {
199
- if (entity.collider) {
200
- const nearby = this.spatialGrid.getNearby(entity);
201
- for (const other of nearby) {
202
- if (other.collider) {
203
- // Check duplicate
204
- if ((currentCollisions.has(entity) && currentCollisions.get(entity)?.has(other)) ||
205
- (currentCollisions.has(other) && currentCollisions.get(other)?.has(entity))) {
206
- continue;
207
- }
208
- // Check collision
209
- const collisionResult = this.checkCollision(entity, other);
210
- if (collisionResult.info) {
211
- // Wake if contacting an awake body
212
- if (!entity.body?.isSleeping || !other.body?.isSleeping) {
213
- entity.body?.wake();
214
- other.body?.wake();
215
- }
216
- // Track collision
217
- if (!currentCollisions.has(entity)) {
218
- currentCollisions.set(entity, new Map());
219
- }
220
- currentCollisions.get(entity)?.set(other, collisionResult.info);
221
- // Check if this is a new collision
222
- const wasColliding = (this.collisionPairs.get(entity)?.has(other) ||
223
- this.collisionPairs.get(other)?.has(entity));
224
- if (!wasColliding) {
225
- // ENTER
226
- if (collisionResult.isTrigger) {
227
- entity.onTriggerEnter?.(other);
228
- other.onTriggerEnter?.(entity);
229
- }
230
- else {
231
- entity.onCollisionEnter?.(other, collisionResult.info);
232
- other.onCollisionEnter?.(entity, {
233
- ...collisionResult.info,
234
- normal: new Vector2(-collisionResult.info.normal.x, -collisionResult.info.normal.y)
235
- });
236
- }
237
- }
238
- else {
239
- // STAY
240
- if (collisionResult.isTrigger) {
241
- entity.onTriggerStay?.(other);
242
- other.onTriggerStay?.(entity);
243
- }
244
- else {
245
- entity.onCollisionStay?.(other, collisionResult.info);
246
- other.onCollisionStay?.(entity, {
247
- ...collisionResult.info,
248
- normal: new Vector2(-collisionResult.info.normal.x, -collisionResult.info.normal.y)
249
- });
250
- }
251
- }
252
- // Collect contact for solving
253
- if (!collisionResult.isTrigger) {
254
- contacts.push({
255
- entityA: entity,
256
- entityB: other,
257
- info: collisionResult.info
258
- });
259
- }
260
- }
261
- }
262
- }
263
- }
264
- }
265
- // PHASE 2: Iteratively solve all contacts together
266
- for (let iteration = 0; iteration < 6; iteration++) {
267
- for (const contact of contacts) {
268
- this.resolveCollision(contact.entityA, contact.entityB, contact.info, dt);
269
- }
270
- }
271
- // EXIT
272
- for (const [entity, others] of this.collisionPairs) {
273
- for (const [other, lastInfo] of others) {
274
- // Check if still colliding
275
- const stillColliding = (currentCollisions.get(entity)?.has(other) ||
276
- currentCollisions.get(other)?.has(entity));
277
- if (!stillColliding) {
278
- // Determine if was a trigger
279
- const wasTrigger = entity.collider?.isTrigger || other.collider?.isTrigger;
280
- if (wasTrigger) {
281
- entity.onTriggerExit?.(other);
282
- other.onTriggerExit?.(entity);
283
- }
284
- else {
285
- entity.onCollisionExit?.(other, lastInfo);
286
- other.onCollisionExit?.(entity, {
287
- ...lastInfo,
288
- normal: new Vector2(-lastInfo.normal.x, -lastInfo.normal.y)
289
- });
290
- }
291
- }
292
- }
293
- }
294
- // Update tracked collisions
295
- this.collisionPairs = currentCollisions;
296
- // Update position
297
- for (const entity of entities) {
298
- if (entity.body instanceof RigidBody) {
299
- // Skip sleeping bodies
300
- if (entity.body.isSleeping)
301
- continue;
302
- // Positional update
303
- entity.position.x += entity.body.velocity.x * dt;
304
- entity.position.y += entity.body.velocity.y * dt;
305
- entity.rotation += entity.body.rotationVelocity * dt;
306
- // Sleep accumulation
307
- const speed = entity.body.velocity.magnitude();
308
- const angularSpeed = Math.abs(entity.body.rotationVelocity);
309
- if (speed < entity.body.sleepThreshold && angularSpeed < entity.body.sleepThreshold) {
310
- entity.body.sleepTimer += dt;
311
- if (entity.body.sleepTimer >= entity.body.sleepTimeThreshold) {
312
- // Go to sleep
313
- entity.body.isSleeping = true;
314
- entity.body.velocity.x = 0;
315
- entity.body.velocity.y = 0;
316
- entity.body.rotationVelocity = 0;
317
- }
318
- }
319
- else {
320
- // Reset timer if moving too fast
321
- entity.body.sleepTimer = 0;
322
- }
323
- }
324
- }
325
- }
326
- checkCollision(entityA, entityB) {
327
- if (entityA.collider && entityB.collider) {
328
- // Layer/mask filtering
329
- if ((entityA.collider.mask & entityB.collider.layer) === 0 ||
330
- (entityB.collider.mask & entityA.collider.layer) === 0) {
331
- return {
332
- isTrigger: false
333
- };
334
- }
335
- // Get trigger info
336
- const isTrigger = entityA.collider.isTrigger || entityB.collider.isTrigger;
337
- // Check collision
338
- const posA = entityA.position.add(entityA.collider.offset);
339
- const posB = entityB.position.add(entityB.collider.offset);
340
- // Check different types of colliders
341
- if (entityA.collider instanceof CircleCollider && entityB.collider instanceof CircleCollider) {
342
- const distance = posA.distance(posB);
343
- const radiusSum = entityA.collider.radius + entityB.collider.radius;
344
- if (distance >= radiusSum) {
345
- return {
346
- isTrigger: false
347
- };
348
- }
349
- const penetration = radiusSum - distance;
350
- const direction = posB.sub(posA);
351
- let normal = distance > 0 ? direction.scale(1 / distance) : new Vector2(1, 0);
352
- // Warm starting - copy accumulated impulse from last frame if contact persists
353
- let accumulatedImpulse = 0;
354
- const lastContactInfo = this.collisionPairs.get(entityA)?.get(entityB) ||
355
- this.collisionPairs.get(entityB)?.get(entityA);
356
- if (lastContactInfo) {
357
- accumulatedImpulse = lastContactInfo.accumulatedNormalImpulse;
358
- }
359
- return {
360
- isTrigger,
361
- info: {
362
- normal,
363
- penetration,
364
- contact: posA.add(normal.scale(entityA.collider.radius)),
365
- accumulatedNormalImpulse: accumulatedImpulse
366
- }
367
- };
368
- }
369
- else if (entityA.collider instanceof BoxCollider && entityB.collider instanceof BoxCollider) {
370
- const halfAx = entityA.collider.width / 2;
371
- const halfAy = entityA.collider.height / 2;
372
- const halfBx = entityB.collider.width / 2;
373
- const halfBy = entityB.collider.height / 2;
374
- const delta = posB.sub(posA);
375
- const overlapX = halfAx + halfBx - Math.abs(delta.x);
376
- const overlapY = halfAy + halfBy - Math.abs(delta.y);
377
- if (overlapX <= 0 || overlapY <= 0) {
378
- return { isTrigger: false };
379
- }
380
- let normal;
381
- let penetration;
382
- if (overlapX < overlapY) {
383
- penetration = overlapX;
384
- normal = new Vector2(delta.x < 0 ? -1 : 1, 0);
385
- }
386
- else {
387
- penetration = overlapY;
388
- normal = new Vector2(0, delta.y < 0 ? -1 : 1);
389
- }
390
- // Contact point: center of overlapping region
391
- const contactX = posA.x + Math.sign(delta.x) * (halfAx - overlapX / 2);
392
- const contactY = posA.y + Math.sign(delta.y) * (halfAy - overlapY / 2);
393
- let accumulatedImpulse = 0;
394
- const lastContactInfo = this.collisionPairs.get(entityA)?.get(entityB) ||
395
- this.collisionPairs.get(entityB)?.get(entityA);
396
- if (lastContactInfo) {
397
- accumulatedImpulse = lastContactInfo.accumulatedNormalImpulse;
398
- }
399
- return {
400
- isTrigger,
401
- info: {
402
- normal,
403
- penetration,
404
- contact: new Vector2(contactX, contactY),
405
- accumulatedNormalImpulse: accumulatedImpulse
406
- }
407
- };
408
- }
409
- else if ((entityA.collider instanceof CircleCollider && entityB.collider instanceof BoxCollider) ||
410
- (entityA.collider instanceof BoxCollider && entityB.collider instanceof CircleCollider)) {
411
- const isCircleA = entityA.collider instanceof CircleCollider;
412
- const circlePos = isCircleA ? posA : posB;
413
- const boxPos = isCircleA ? posB : posA;
414
- const circle = (isCircleA ? entityA : entityB).collider;
415
- const box = (isCircleA ? entityB : entityA).collider;
416
- const halfW = box.width / 2;
417
- const halfH = box.height / 2;
418
- // Box center -> circle center
419
- const delta = circlePos.sub(boxPos);
420
- const inside = Math.abs(delta.x) < halfW && Math.abs(delta.y) < halfH;
421
- let normal;
422
- let penetration;
423
- let contact;
424
- if (inside) {
425
- const overlapX = halfW - Math.abs(delta.x);
426
- const overlapY = halfH - Math.abs(delta.y);
427
- if (overlapX < overlapY) {
428
- const sign = delta.x < 0 ? -1 : 1;
429
- const boxToCircle = new Vector2(sign, 0);
430
- normal = isCircleA ? boxToCircle.neg() : boxToCircle; // A→B
431
- penetration = circle.radius + overlapX;
432
- contact = new Vector2(boxPos.x + sign * halfW, circlePos.y);
433
- }
434
- else {
435
- const sign = delta.y < 0 ? -1 : 1;
436
- const boxToCircle = new Vector2(0, sign);
437
- normal = isCircleA ? boxToCircle.neg() : boxToCircle;
438
- penetration = circle.radius + overlapY;
439
- contact = new Vector2(circlePos.x, boxPos.y + sign * halfH);
440
- }
441
- }
442
- else {
443
- const clampedX = Math.max(-halfW, Math.min(halfW, delta.x));
444
- const clampedY = Math.max(-halfH, Math.min(halfH, delta.y));
445
- const closest = boxPos.add(new Vector2(clampedX, clampedY));
446
- const diff = circlePos.sub(closest);
447
- const distSq = diff.magnitudeSquared();
448
- if (distSq >= circle.radius * circle.radius) {
449
- return { isTrigger: false };
450
- }
451
- const dist = Math.sqrt(distSq);
452
- const boxToCircle = dist > 0 ? diff.scale(1 / dist) : new Vector2(1, 0);
453
- normal = isCircleA ? boxToCircle.neg() : boxToCircle;
454
- penetration = circle.radius - dist;
455
- contact = closest;
456
- }
457
- let accumulatedImpulse = 0;
458
- const lastContactInfo = this.collisionPairs.get(entityA)?.get(entityB) ||
459
- this.collisionPairs.get(entityB)?.get(entityA);
460
- if (lastContactInfo) {
461
- accumulatedImpulse = lastContactInfo.accumulatedNormalImpulse;
462
- }
463
- return {
464
- isTrigger,
465
- info: {
466
- normal,
467
- penetration,
468
- contact,
469
- accumulatedNormalImpulse: accumulatedImpulse
470
- }
471
- };
472
- }
473
- return {
474
- isTrigger: false
475
- };
476
- }
477
- return {
478
- isTrigger: false
479
- };
480
- }
481
- resolveCollision(entityA, entityB, info, dt) {
482
- if (!entityA.body || !entityB.body)
483
- return;
484
- const bodyA = entityA.body;
485
- const bodyB = entityB.body;
486
- // Check for infinite mass
487
- const invMassA = isFinite(bodyA.mass) ? 1 / bodyA.mass : 0;
488
- const invMassB = isFinite(bodyB.mass) ? 1 / bodyB.mass : 0;
489
- const totalInvMass = invMassA + invMassB;
490
- // If both infinite mass, do nothing
491
- if (totalInvMass === 0)
492
- return;
493
- // Relative velocity
494
- const relVel = bodyB.velocity.sub(bodyA.velocity);
495
- // Velocity along normal
496
- const velAlongNormal = relVel.dot(info.normal);
497
- // Don't resolve if velocities are separating
498
- if (velAlongNormal > 0)
499
- return;
500
- // Restitution (bounciness) with slop
501
- const restitutionSlop = 0.5; // Kill bounce below 0.5 unit/sec
502
- let restitution = Math.max(bodyA.restitution, bodyB.restitution);
503
- // No bounce for slow collisions (helps objects settle)
504
- if (Math.abs(velAlongNormal) < restitutionSlop) {
505
- restitution = 0;
506
- }
507
- // Position correction with slop (Box2D/PhysX style)
508
- const slop = 0.01; // Allow small penetration without correction
509
- const baumgarte = 0.2; // Correct 20% of penetration per frame
510
- // Bias velocity - only apply when no bounce (restitution = 0)
511
- // When bouncing, let restitution handle it naturally
512
- const biasVelocity = restitution === 0 ? (Math.max(0, info.penetration - slop) * baumgarte) / dt : 0;
513
- // Calculate impulse using inverse mass
514
- const jn = (-(1 + restitution) * velAlongNormal + biasVelocity) / totalInvMass;
515
- // Clamp accumulated impulse
516
- const oldImpulse = info.accumulatedNormalImpulse || 0;
517
- info.accumulatedNormalImpulse = Math.max(0, oldImpulse + jn);
518
- const actualImpulse = info.accumulatedNormalImpulse - oldImpulse;
519
- // Apply actual impulse
520
- bodyA.velocity = bodyA.velocity.sub(info.normal.scale(actualImpulse * invMassA));
521
- bodyB.velocity = bodyB.velocity.add(info.normal.scale(actualImpulse * invMassB));
522
- // Apply angular impulse (torque from contact point)
523
- // Disabled for now because colliders must rotate too and there isn't polygon collider solver for now
524
- /*const rA = info.contact.sub(entityA.position);
525
- const rB = info.contact.sub(entityB.position);
526
-
527
- const angularImpulseA = rA.cross(info.normal.scale(-actualImpulse));
528
- const angularImpulseB = rB.cross(info.normal.scale(actualImpulse));
529
-
530
- bodyA.rotationVelocity += angularImpulseA * (isFinite(bodyA.inertia) ? 1 / bodyA.inertia : 0);
531
- bodyB.rotationVelocity += angularImpulseB * (isFinite(bodyB.inertia) ? 1 / bodyB.inertia : 0);*/
532
- }
533
- }
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
@@ -1,10 +0,0 @@
1
- export class Sprite {
2
- texture;
3
- width;
4
- height;
5
- constructor(options) {
6
- this.texture = options.texture;
7
- this.width = options.width ?? this.texture.width;
8
- this.height = options.height ?? this.texture.height;
9
- }
10
- }
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