UNPKG

@rpgjs/physic

Version:

A deterministic 2D top-down physics library for RPG, sandbox and MMO games

230 lines (229 loc) 6.86 kB
import { Vector2 } from "./index2.js"; import { AABB } from "./index4.js"; import { CircleCollider } from "./index11.js"; import { AABBCollider } from "./index12.js"; class CapsuleCollider { constructor(entity) { this.entity = entity; } getBounds() { const { radius, height } = this.getCapsuleConfig(); const pos = this.entity.position; const halfHeight = Math.max(0, height / 2 - radius); return new AABB( pos.x - radius, pos.y - halfHeight - radius, pos.x + radius, pos.y + halfHeight + radius ); } getEntity() { return this.entity; } raycast(ray) { const bounds = this.getBounds(); const tMin = (bounds.minX - ray.origin.x) / ray.direction.x; const tMax = (bounds.maxX - ray.origin.x) / ray.direction.x; const tymin = (bounds.minY - ray.origin.y) / ray.direction.y; const tymax = (bounds.maxY - ray.origin.y) / ray.direction.y; const t1 = Math.min(tMin, tMax); const t2 = Math.max(tMin, tMax); const t3 = Math.min(tymin, tymax); const t4 = Math.max(tymin, tymax); const tNear = Math.max(t1, t3); const tFar = Math.min(t2, t4); if (tNear > tFar || tFar < 0) return null; if (tNear > ray.length) return null; const t = tNear < 0 ? tFar : tNear; if (t < 0) return null; const point = ray.getPoint(t); let normal = new Vector2(0, 0); if (Math.abs(point.x - bounds.minX) < 1e-5) normal.set(-1, 0); else if (Math.abs(point.x - bounds.maxX) < 1e-5) normal.set(1, 0); else if (Math.abs(point.y - bounds.minY) < 1e-5) normal.set(0, -1); else if (Math.abs(point.y - bounds.maxY) < 1e-5) normal.set(0, 1); return { entity: this.entity, point, normal, distance: t }; } testCollision(other) { if (other instanceof CircleCollider) { return this.testCircle(other); } else if (other instanceof AABBCollider) { return this.testAABB(other); } else if (other instanceof CapsuleCollider) { return this.testCapsule(other); } return other.testCollision(this); } getContactPoints(other) { const collision = this.testCollision(other); return collision ? collision.contacts : []; } getCapsuleConfig() { if (this.entity.capsule) { return this.entity.capsule; } return { radius: this.entity.radius || 10, height: this.entity.height || 30 }; } getSegment() { const { radius, height } = this.getCapsuleConfig(); const pos = this.entity.position; const halfSegment = Math.max(0, height / 2 - radius); return { a: new Vector2(pos.x, pos.y - halfSegment), b: new Vector2(pos.x, pos.y + halfSegment) }; } testCircle(circle) { const seg = this.getSegment(); const circleCenter = circle.getCenter(); const circleRadius = circle.getRadius(); const capRadius = this.getCapsuleConfig().radius; const closest = this.closestPointOnSegment(seg.a, seg.b, circleCenter); const distSq = closest.distanceToSquared(circleCenter); const minDist = capRadius + circleRadius; if (distSq > minDist * minDist) { return null; } const dist = Math.sqrt(distSq); const normal = dist > 0 ? circleCenter.sub(closest).normalize() : new Vector2(1, 0); const depth = minDist - dist; return { entityA: this.entity, entityB: circle.getEntity(), // Convention: Normal points from A to B. // Here A is Capsule, B is Circle. // Vector from closest (on capsule) to center (circle) is B - A. // So normal should be (circleCenter - closest).normalized(). // Wait, if I return normal, it should be the direction to push B out of A? // Usually normal points from A to B. // Let's stick to: normal points from A to B. contacts: [{ point: closest.add(normal.mul(capRadius)), normal, depth }], normal, depth }; } testAABB(box) { const seg = this.getSegment(); const capRadius = this.getCapsuleConfig().radius; const boxBounds = box.getBounds(); const closestOnSeg = this.closestPointOnSegment(seg.a, seg.b, box.getBounds().getCenter()); const closestOnBox = boxBounds.clamp(closestOnSeg); const distSq = closestOnSeg.distanceToSquared(closestOnBox); if (distSq > capRadius * capRadius) { return null; } const dist = Math.sqrt(distSq); const normal = dist > 0 ? closestOnBox.sub(closestOnSeg).normalize() : new Vector2(0, 1); const depth = capRadius - dist; return { entityA: this.entity, entityB: box.getEntity(), contacts: [{ point: closestOnBox, normal, depth }], normal, depth }; } testCapsule(other) { const segA = this.getSegment(); const segB = other.getSegment(); const rA = this.getCapsuleConfig().radius; const rB = other.getCapsuleConfig().radius; const { p1, p2 } = this.closestPointsSegmentSegment(segA.a, segA.b, segB.a, segB.b); const distSq = p1.distanceToSquared(p2); const minDist = rA + rB; if (distSq > minDist * minDist) { return null; } const dist = Math.sqrt(distSq); const normal = dist > 0 ? p2.sub(p1).normalize() : new Vector2(1, 0); const depth = minDist - dist; return { entityA: this.entity, entityB: other.getEntity(), contacts: [{ point: p1.add(normal.mul(rA)), normal, depth }], normal, depth }; } closestPointOnSegment(a, b, p) { const ab = b.sub(a); const t = p.sub(a).dot(ab) / ab.dot(ab); return a.add(ab.mul(Math.max(0, Math.min(1, t)))); } // http://geomalgorithms.com/a07-_distance.html closestPointsSegmentSegment(p1, p2, p3, p4) { const u = p2.sub(p1); const v = p4.sub(p3); const w = p1.sub(p3); const a = u.dot(u); const b = u.dot(v); const c = v.dot(v); const d = u.dot(w); const e = v.dot(w); const D = a * c - b * b; let sc, sN, sD = D; let tc, tN, tD = D; if (D < 1e-8) { sN = 0; sD = 1; tN = e; tD = c; } else { sN = b * e - c * d; tN = a * e - b * d; if (sN < 0) { sN = 0; tN = e; tD = c; } else if (sN > sD) { sN = sD; tN = e + b; tD = c; } } if (tN < 0) { tN = 0; if (-d < 0) sN = 0; else if (-d > a) sN = sD; else { sN = -d; sD = a; } } else if (tN > tD) { tN = tD; if (-d + b < 0) sN = 0; else if (-d + b > a) sN = sD; else { sN = -d + b; sD = a; } } sc = Math.abs(sN) < 1e-8 ? 0 : sN / sD; tc = Math.abs(tN) < 1e-8 ? 0 : tN / tD; return { p1: p1.add(u.mul(sc)), p2: p3.add(v.mul(tc)) }; } } export { CapsuleCollider }; //# sourceMappingURL=index13.js.map