silphius/app/ecs/components/collider.js

307 lines
10 KiB
JavaScript
Raw Normal View History

2024-07-02 16:16:39 -05:00
import Component from '@/ecs/component.js';
import {distance, intersects, transform} from '@/util/math.js';
2024-07-02 16:16:39 -05:00
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 = [];
2024-07-25 16:39:05 -05:00
$$collisionStart;
$$collisionEnd;
2024-07-26 06:00:37 -05:00
$$intersections = new Map();
2024-07-04 21:47:14 -05:00
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;
}
2024-07-26 06:00:37 -05:00
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.context.other = otherEntity;
script.context.pair = [body, otherBody];
2024-07-26 10:36:59 -05:00
const ticker = script.ticker();
ecs.addDestructionDependency(thisEntity.id, ticker);
ecs.addDestructionDependency(otherEntity.id, ticker);
thisEntity.Ticking.add(ticker);
2024-07-26 06:00:37 -05:00
}
if (other.$$collisionEnd) {
const script = other.$$collisionEnd.clone();
script.context.other = thisEntity;
script.context.pair = [otherBody, body];
2024-07-26 10:36:59 -05:00
const ticker = script.ticker();
ecs.addDestructionDependency(thisEntity.id, ticker);
ecs.addDestructionDependency(otherEntity.id, ticker);
otherEntity.Ticking.add(ticker);
2024-07-26 06:00:37 -05:00
}
}
this.$$intersections.delete(other);
other.$$intersections.delete(this);
}
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],
];
2024-07-26 08:40:53 -05:00
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) {
2024-07-26 06:00:37 -05:00
if (this.$$collisionStart) {
const script = this.$$collisionStart.clone();
script.context.other = otherEntity;
script.context.pair = [body, otherBody];
2024-07-26 10:36:59 -05:00
const ticker = script.ticker();
ecs.addDestructionDependency(otherEntity.id, ticker);
ecs.addDestructionDependency(thisEntity.id, ticker);
thisEntity.Ticking.add(ticker);
2024-07-26 06:00:37 -05:00
}
if (other.$$collisionStart) {
const script = other.$$collisionStart.clone();
script.context.other = thisEntity;
script.context.pair = [otherBody, body];
2024-07-26 10:36:59 -05:00
const ticker = script.ticker();
ecs.addDestructionDependency(otherEntity.id, ticker);
ecs.addDestructionDependency(thisEntity.id, ticker);
otherEntity.Ticking.add(ticker);
2024-07-26 06:00:37 -05:00
}
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);
}
}
2024-07-22 03:17:43 -05:00
closest(aabb) {
const entity = ecs.get(this.entity);
return Array.from(ecs.system('Colliders').within(aabb))
.filter((other) => other !== entity)
.sort(({Position: l}, {Position: r}) => {
return distance(entity.Position, l) > distance(entity.Position, r) ? -1 : 1;
});
}
2024-07-23 15:05:55 -05:00
destroy() {
2024-07-26 06:00:37 -05:00
for (const [other] of this.$$intersections) {
other.$$intersections.delete(this);
2024-07-23 15:05:55 -05:00
}
2024-07-26 06:00:37 -05:00
this.$$intersections.clear();
2024-07-23 15:05:55 -05:00
}
2024-07-26 06:00:37 -05:00
intersectionsWith(other) {
2024-07-02 16:16:39 -05:00
const {aabb, aabbs} = this;
const {aabb: otherAabb, aabbs: otherAabbs} = other;
2024-07-03 16:13:14 -05:00
const intersections = [];
2024-07-02 16:16:39 -05:00
if (!intersects(aabb, otherAabb)) {
2024-07-03 16:13:14 -05:00
return intersections;
2024-07-02 16:16:39 -05:00
}
2024-07-03 16:13:14 -05:00
for (const i in aabbs) {
const aabb = aabbs[i];
2024-07-26 19:28:28 -05:00
const body = this.bodies[i];
2024-07-03 16:13:14 -05:00
for (const j in otherAabbs) {
const otherAabb = otherAabbs[j];
2024-07-26 19:28:28 -05:00
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;
}
}
2024-07-26 06:00:37 -05:00
// todo accuracy
2024-07-02 16:16:39 -05:00
if (intersects(aabb, otherAabb)) {
2024-07-26 06:00:37 -05:00
intersections.push({
entity: this,
other,
i,
j,
});
2024-07-02 16:16:39 -05:00
}
}
}
2024-07-03 16:13:14 -05:00
return intersections;
2024-07-02 16:16:39 -05:00
}
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,
});
}
}
2024-07-02 16:16:39 -05:00
}
}
2024-07-02 22:42:56 -05:00
async load(instance) {
2024-07-26 19:28:28 -05:00
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();
2024-07-02 22:42:56 -05:00
// heavy handed...
if ('undefined' !== typeof window) {
return;
}
2024-07-25 16:39:05 -05:00
instance.$$collisionEnd = await this.ecs.readScript(
2024-07-02 22:42:56 -05:00
instance.collisionEndScript,
{
ecs: this.ecs,
entity: this.ecs.get(instance.entity),
},
);
2024-07-25 16:39:05 -05:00
instance.$$collisionStart = await this.ecs.readScript(
2024-07-02 22:42:56 -05:00
instance.collisionStartScript,
{
ecs: this.ecs,
entity: this.ecs.get(instance.entity),
},
);
}
2024-07-02 16:16:39 -05:00
static properties = {
bodies: {
type: 'array',
subtype: {
2024-07-03 16:13:14 -05:00
type: 'object',
properties: {
2024-07-26 19:28:28 -05:00
bits: {defaultValue: 0x00000001, type: 'uint32'},
2024-07-03 16:13:14 -05:00
impassable: {type: 'uint8'},
2024-07-26 19:28:28 -05:00
group: {type: 'int32'},
mask: {defaultValue: 0xFFFFFFFF, type: 'uint32'},
2024-07-03 16:13:14 -05:00
points: {
type: 'array',
subtype: vector2d('int16'),
},
2024-07-25 11:00:25 -05:00
unstoppable: {type: 'uint8'},
2024-07-03 16:13:14 -05:00
},
2024-07-02 16:16:39 -05:00
},
},
2024-07-02 22:42:56 -05:00
collisionEndScript: {type: 'string'},
collisionStartScript: {type: 'string'},
2024-07-02 16:16:39 -05:00
};
}