311 lines
11 KiB
JavaScript
311 lines
11 KiB
JavaScript
import Component from '@/ecs/component.js';
|
|
import {distance, intersects, transform} from '@/util/math.js';
|
|
|
|
import vector2d from './helpers/vector-2d';
|
|
|
|
export default class Collider extends Component {
|
|
instanceFromSchema() {
|
|
const {ecs} = this;
|
|
return class ColliderInstance extends super.instanceFromSchema() {
|
|
$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
|
$$aabbs = [];
|
|
$$collisionStart;
|
|
$$collisionEnd;
|
|
$$intersections = new Map();
|
|
get aabb() {
|
|
const {Position: {x: px, y: py}} = ecs.get(this.entity);
|
|
return {
|
|
x0: this.$$aabb.x0 + px,
|
|
x1: this.$$aabb.x1 + px,
|
|
y0: this.$$aabb.y0 + py,
|
|
y1: this.$$aabb.y1 + py,
|
|
};
|
|
}
|
|
get aabbs() {
|
|
const {Position: {x: px, y: py}} = ecs.get(this.entity);
|
|
const aabbs = [];
|
|
for (const aabb of this.$$aabbs) {
|
|
aabbs.push({
|
|
x0: aabb.x0 + px,
|
|
x1: aabb.x1 + px,
|
|
y0: aabb.y0 + py,
|
|
y1: aabb.y1 + py,
|
|
})
|
|
}
|
|
return aabbs;
|
|
}
|
|
checkCollision(other) {
|
|
const otherEntity = ecs.get(other.entity);
|
|
const thisEntity = ecs.get(this.entity);
|
|
const intersections = this.intersectionsWith(other);
|
|
const activeIntersections = this.$$intersections.get(other) || new Set();
|
|
if (0 === intersections.length) {
|
|
// had none; have none
|
|
if (0 === activeIntersections.size) {
|
|
return;
|
|
}
|
|
this.endIntersections(other, intersections);
|
|
return;
|
|
}
|
|
for (const intersection of intersections) {
|
|
// new pair - start
|
|
const [body, otherBody] = [
|
|
intersection.entity.bodies[intersection.i],
|
|
intersection.other.bodies[intersection.j],
|
|
];
|
|
let hasMatchingIntersection = false;
|
|
for (const activeIntersection of activeIntersections) {
|
|
if (
|
|
(
|
|
activeIntersection.entity === intersection.entity
|
|
&& activeIntersection.other === intersection.other
|
|
&& activeIntersection.i === intersection.i
|
|
&& activeIntersection.j === intersection.j
|
|
)
|
|
|| (
|
|
activeIntersection.entity === intersection.other
|
|
&& activeIntersection.other === intersection.entity
|
|
&& activeIntersection.i === intersection.j
|
|
&& activeIntersection.j === intersection.i
|
|
)
|
|
) {
|
|
hasMatchingIntersection = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!hasMatchingIntersection) {
|
|
if (this.$$collisionStart) {
|
|
const script = this.$$collisionStart.clone();
|
|
script.locals.entity = thisEntity;
|
|
script.locals.other = otherEntity;
|
|
script.locals.pair = [body, otherBody];
|
|
const ticker = script.ticker();
|
|
const promise = thisEntity.Ticking.add(ticker);
|
|
ecs.addDestructionDependency(otherEntity.id, promise);
|
|
ecs.addDestructionDependency(thisEntity.id, promise);
|
|
}
|
|
if (other.$$collisionStart) {
|
|
const script = other.$$collisionStart.clone();
|
|
script.locals.entity = otherEntity;
|
|
script.locals.other = thisEntity;
|
|
script.locals.pair = [otherBody, body];
|
|
const ticker = script.ticker();
|
|
const promise = otherEntity.Ticking.add(ticker);
|
|
ecs.addDestructionDependency(otherEntity.id, promise);
|
|
ecs.addDestructionDependency(thisEntity.id, promise);
|
|
}
|
|
activeIntersections.add(intersection);
|
|
}
|
|
// undo restricted movement
|
|
if (!body.unstoppable && otherBody.impassable) {
|
|
const j = this.bodies.indexOf(body);
|
|
const oj = other.bodies.indexOf(otherBody);
|
|
const aabb = this.$$aabbs[j];
|
|
const otherAabb = other.aabbs[oj];
|
|
const {Position} = thisEntity;
|
|
if (!intersects(
|
|
{
|
|
x0: aabb.x0 + Position.lastX,
|
|
x1: aabb.x1 + Position.lastX,
|
|
y0: aabb.y0 + Position.y,
|
|
y1: aabb.y1 + Position.y,
|
|
},
|
|
otherAabb,
|
|
)) {
|
|
Position.x = Position.lastX
|
|
}
|
|
else if (!intersects(
|
|
{
|
|
x0: aabb.x0 + Position.x,
|
|
x1: aabb.x1 + Position.x,
|
|
y0: aabb.y0 + Position.lastY,
|
|
y1: aabb.y1 + Position.lastY,
|
|
},
|
|
otherAabb,
|
|
)) {
|
|
Position.y = Position.lastY
|
|
}
|
|
else {
|
|
Position.x = Position.lastX
|
|
Position.y = Position.lastY
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (activeIntersections.size > 0) {
|
|
this.$$intersections.set(other, activeIntersections);
|
|
other.$$intersections.set(this, activeIntersections);
|
|
}
|
|
}
|
|
closest(aabb) {
|
|
const entity = ecs.get(this.entity);
|
|
return Array.from(ecs.system('MaintainColliderHash').within(aabb))
|
|
.filter((other) => other !== entity)
|
|
.sort(({Position: l}, {Position: r}) => {
|
|
return distance(entity.Position, l) > distance(entity.Position, r) ? -1 : 1;
|
|
});
|
|
}
|
|
destroy() {
|
|
for (const [other] of this.$$intersections) {
|
|
other.$$intersections.delete(this);
|
|
}
|
|
this.$$intersections.clear();
|
|
}
|
|
endIntersections(other, intersections) {
|
|
const otherEntity = ecs.get(other.entity);
|
|
const thisEntity = ecs.get(this.entity);
|
|
for (const intersection of intersections) {
|
|
const [body, otherBody] = [
|
|
intersection.entity.bodies[intersection.i],
|
|
intersection.other.bodies[intersection.j],
|
|
];
|
|
if (this.$$collisionEnd) {
|
|
const script = this.$$collisionEnd.clone();
|
|
script.locals.other = otherEntity;
|
|
script.locals.pair = [body, otherBody];
|
|
const ticker = script.ticker();
|
|
const promise = thisEntity.Ticking.add(ticker);
|
|
ecs.addDestructionDependency(thisEntity.id, promise);
|
|
ecs.addDestructionDependency(otherEntity.id, promise);
|
|
}
|
|
if (other.$$collisionEnd) {
|
|
const script = other.$$collisionEnd.clone();
|
|
script.locals.other = thisEntity;
|
|
script.locals.pair = [otherBody, body];
|
|
const ticker = script.ticker();
|
|
const promise = otherEntity.Ticking.add(ticker);
|
|
ecs.addDestructionDependency(thisEntity.id, promise);
|
|
ecs.addDestructionDependency(otherEntity.id, promise);
|
|
}
|
|
}
|
|
this.$$intersections.delete(other);
|
|
other.$$intersections.delete(this);
|
|
}
|
|
intersectionsWith(other) {
|
|
const {aabb, aabbs} = this;
|
|
const {aabb: otherAabb, aabbs: otherAabbs} = other;
|
|
const intersections = [];
|
|
if (!intersects(aabb, otherAabb)) {
|
|
return intersections;
|
|
}
|
|
for (const i in aabbs) {
|
|
const aabb = aabbs[i];
|
|
const body = this.bodies[i];
|
|
for (const j in otherAabbs) {
|
|
const otherAabb = otherAabbs[j];
|
|
const otherBody = other.bodies[j];
|
|
if (body.group === otherBody.group && body.group < 0) {
|
|
continue;
|
|
}
|
|
if (body.group !== otherBody.group || 0 === body.group) {
|
|
if (0 === (otherBody.mask & body.bits) || 0 === (body.mask & otherBody.bits)) {
|
|
continue;
|
|
}
|
|
}
|
|
// todo accuracy
|
|
if (intersects(aabb, otherAabb)) {
|
|
intersections.push({
|
|
entity: this,
|
|
other,
|
|
i,
|
|
j,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return intersections;
|
|
}
|
|
isWithin(query) {
|
|
const {aabb, aabbs} = this;
|
|
if (!intersects(aabb, query)) {
|
|
return false;
|
|
}
|
|
for (const aabb of aabbs) {
|
|
if (intersects(aabb, query)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
updateAabbs() {
|
|
this.$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
|
this.$$aabbs = [];
|
|
const {bodies} = this;
|
|
const {Direction: {direction = 0} = {}} = ecs.get(this.entity) || {};
|
|
for (const body of bodies) {
|
|
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
for (const point of transform(body.points, {rotation: direction})) {
|
|
const {x, y} = point;
|
|
if (x < x0) x0 = x;
|
|
if (x < this.$$aabb.x0) this.$$aabb.x0 = x;
|
|
if (x > x1) x1 = x;
|
|
if (x > this.$$aabb.x1) this.$$aabb.x1 = x;
|
|
if (y < y0) y0 = y;
|
|
if (y < this.$$aabb.y0) this.$$aabb.y0 = y;
|
|
if (y > y1) y1 = y;
|
|
if (y > this.$$aabb.y1) this.$$aabb.y1 = y;
|
|
}
|
|
this.$$aabbs.push({
|
|
x0: x0 > x1 ? x1 : x0,
|
|
x1: x0 > x1 ? x0 : x1,
|
|
y0: y0 > y1 ? y1 : y0,
|
|
y1: y0 > y1 ? y0 : y1,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
load(instance) {
|
|
for (const i in instance.bodies) {
|
|
instance.bodies[i] = {
|
|
...this.constructor.schema.constructor.defaultValue(
|
|
this.constructor.schema.specification.concrete.properties.bodies.concrete.subtype,
|
|
),
|
|
...instance.bodies[i],
|
|
};
|
|
}
|
|
instance.updateAabbs();
|
|
// heavy handed...
|
|
if ('undefined' !== typeof window) {
|
|
return;
|
|
}
|
|
instance.$$collisionEnd = this.ecs.readScript(
|
|
instance.collisionEndScript,
|
|
{
|
|
ecs: this.ecs,
|
|
},
|
|
);
|
|
instance.$$collisionStart = this.ecs.readScript(
|
|
instance.collisionStartScript,
|
|
{
|
|
ecs: this.ecs,
|
|
},
|
|
);
|
|
}
|
|
static properties = {
|
|
bodies: {
|
|
type: 'array',
|
|
subtype: {
|
|
type: 'object',
|
|
properties: {
|
|
// if either body has a group of zero, use bits
|
|
// if both groups are non-zero but different, use bits
|
|
// if both groups are the same and positive, collide
|
|
// if both groups are the same and negative, don't collide
|
|
bits: {defaultValue: 0x00000001, type: 'uint32'},
|
|
group: {type: 'int32'},
|
|
impassable: {type: 'uint8'},
|
|
mask: {defaultValue: 0xFFFFFFFF, type: 'uint32'},
|
|
points: {
|
|
type: 'array',
|
|
subtype: vector2d('int16'),
|
|
},
|
|
unstoppable: {type: 'uint8'},
|
|
},
|
|
},
|
|
},
|
|
collisionEndScript: {type: 'string'},
|
|
collisionStartScript: {type: 'string'},
|
|
};
|
|
}
|