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 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:
@@ -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 or a collision happens, this is to prevent jittering and optimize performance.
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};
@@ -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.0",
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": "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
  },
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
@@ -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