kippy 0.5.0 → 0.6.0

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
@@ -229,6 +229,27 @@ collider.layer; // Set with the matching parameter above, default is (1 << 0)
229
229
  collider.mask; // Set with the matching parameter above, default is 0xFFFFFFFF
230
230
  ```
231
231
 
232
+ or a `BoxCollider`:
233
+ ```js
234
+ const collider = new BoxCollider({
235
+ width, // Circle collider's width, type number
236
+ height, // Circle collider's height, type number
237
+ offset, // Offset from entity's position, type Vector2
238
+ isTrigger, // If true, trigger callbacks are called and collision physics like bouncing
239
+ // will not apply. Otherwise, collision callbacks are called and physics apply
240
+ layer, // A bit mask to determine what collision layer this collider is at
241
+ mask, // A bit mask to check what colliders to collide
242
+ });
243
+
244
+ // You can mutate these props to configure the box collider
245
+ collider.width; // Set with the matching parameter above, required
246
+ collider.height; // Set with the matching parameter above, required
247
+ collider.offset; // Set with the matching parameter above, default is Vector2(0, 0)
248
+ collider.isTrigger; // Set with the matching parameter above, default is false
249
+ collider.layer; // Set with the matching parameter above, default is (1 << 0)
250
+ collider.mask; // Set with the matching parameter above, default is 0xFFFFFFFF
251
+ ```
252
+
232
253
  And you can handle when two objects collide:
233
254
  ```js
234
255
  collider.onCollisionEnter = (other, info) => {};
package/dist/physics.d.ts CHANGED
@@ -44,7 +44,24 @@ export declare class CircleCollider {
44
44
  mask: number;
45
45
  constructor(options: CircleColliderOptions);
46
46
  }
47
- export type Collider = CircleCollider;
47
+ export interface BoxColliderOptions {
48
+ width: number;
49
+ height: number;
50
+ offset?: Vector2;
51
+ isTrigger?: boolean;
52
+ layer?: number;
53
+ mask?: number;
54
+ }
55
+ export declare class BoxCollider {
56
+ width: number;
57
+ height: number;
58
+ offset: Vector2;
59
+ isTrigger: boolean;
60
+ layer: number;
61
+ mask: number;
62
+ constructor(options: BoxColliderOptions);
63
+ }
64
+ export type Collider = CircleCollider | BoxCollider;
48
65
  export interface SpatialGridOptions {
49
66
  cellSize?: number;
50
67
  grid?: Map<string, Set<Entity>>;
package/dist/physics.js CHANGED
@@ -43,6 +43,22 @@ export class CircleCollider {
43
43
  this.mask = options.mask ?? 0xFFFFFFFF;
44
44
  }
45
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
+ }
46
62
  export class SpatialGrid {
47
63
  cellSize;
48
64
  grid;
@@ -123,6 +139,16 @@ export class SpatialGrid {
123
139
  maxY: entity.position.y + radius
124
140
  };
125
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
+ }
126
152
  else {
127
153
  throw new Error("Collider type not supported");
128
154
  }
@@ -179,6 +205,11 @@ export class Physics {
179
205
  // Check collision
180
206
  const collisionResult = this.checkCollision(entity, other);
181
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
+ }
182
213
  // Track collision
183
214
  if (!currentCollisions.has(entity)) {
184
215
  currentCollisions.set(entity, new Map());
@@ -189,8 +220,6 @@ export class Physics {
189
220
  this.collisionPairs.get(other)?.has(entity));
190
221
  if (!wasColliding) {
191
222
  // ENTER
192
- entity.body?.wake();
193
- other.body?.wake();
194
223
  if (collisionResult.isTrigger) {
195
224
  entity.onTriggerEnter?.(other);
196
225
  other.onTriggerEnter?.(entity);
@@ -305,7 +334,7 @@ export class Physics {
305
334
  // Check collision
306
335
  const posA = entityA.position.add(entityA.collider.offset);
307
336
  const posB = entityB.position.add(entityB.collider.offset);
308
- // Check different types of colliders, only circle collider for now
337
+ // Check different types of colliders
309
338
  if (entityA.collider instanceof CircleCollider && entityB.collider instanceof CircleCollider) {
310
339
  const distance = posA.distance(posB);
311
340
  const radiusSum = entityA.collider.radius + entityB.collider.radius;
@@ -334,6 +363,110 @@ export class Physics {
334
363
  }
335
364
  };
336
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
+ }
337
470
  return {
338
471
  isTrigger: false
339
472
  };
@@ -384,11 +517,14 @@ export class Physics {
384
517
  bodyA.velocity = bodyA.velocity.sub(info.normal.scale(actualImpulse * invMassA));
385
518
  bodyB.velocity = bodyB.velocity.add(info.normal.scale(actualImpulse * invMassB));
386
519
  // Apply angular impulse (torque from contact point)
387
- const rA = info.contact.sub(entityA.position);
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);
388
522
  const rB = info.contact.sub(entityB.position);
523
+
389
524
  const angularImpulseA = rA.cross(info.normal.scale(-actualImpulse));
390
525
  const angularImpulseB = rB.cross(info.normal.scale(actualImpulse));
526
+
391
527
  bodyA.rotationVelocity += angularImpulseA * (isFinite(bodyA.inertia) ? 1 / bodyA.inertia : 0);
392
- bodyB.rotationVelocity += angularImpulseB * (isFinite(bodyB.inertia) ? 1 / bodyB.inertia : 0);
528
+ bodyB.rotationVelocity += angularImpulseB * (isFinite(bodyB.inertia) ? 1 / bodyB.inertia : 0);*/
393
529
  }
394
530
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kippy",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Kippy 2D web game engine for JS",
5
5
  "keywords": [
6
6
  "kippy",