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; } 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]; thisEntity.Ticking.add(script.ticker()); } if (other.$$collisionEnd) { const script = other.$$collisionEnd.clone(); script.context.other = thisEntity; script.context.pair = [otherBody, body]; otherEntity.Ticking.add(script.ticker()); } } 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], ]; if (!activeIntersections.has(intersection)) { if (this.$$collisionStart) { const script = this.$$collisionStart.clone(); script.context.other = otherEntity; script.context.pair = [body, otherBody]; thisEntity.Ticking.add(script.ticker()); } if (other.$$collisionStart) { const script = other.$$collisionStart.clone(); script.context.other = thisEntity; script.context.pair = [otherBody, body]; otherEntity.Ticking.add(script.ticker()); } 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('Colliders').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(); } 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]; for (const j in otherAabbs) { const otherAabb = otherAabbs[j]; // 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, }); } } } } async load(instance) { instance.updateAabbs(); // heavy handed... if ('undefined' !== typeof window) { return; } instance.$$collisionEnd = await this.ecs.readScript( instance.collisionEndScript, { ecs: this.ecs, entity: this.ecs.get(instance.entity), }, ); instance.$$collisionStart = await this.ecs.readScript( instance.collisionStartScript, { ecs: this.ecs, entity: this.ecs.get(instance.entity), }, ); } static properties = { bodies: { type: 'array', subtype: { type: 'object', properties: { impassable: {type: 'uint8'}, points: { type: 'array', subtype: vector2d('int16'), }, tags: { type: 'array', subtype: {type: 'string'}, }, unstoppable: {type: 'uint8'}, }, }, }, collisionEndScript: {type: 'string'}, collisionStartScript: {type: 'string'}, }; }