diff --git a/app/ecs/components/collider.js b/app/ecs/components/collider.js index 947ffcc..ec7e672 100644 --- a/app/ecs/components/collider.js +++ b/app/ecs/components/collider.js @@ -1,5 +1,5 @@ import Component from '@/ecs/component.js'; -import {distance, intersects} from '@/util/math.js'; +import {distance, intersects, transform} from '@/util/math.js'; import vector2d from './helpers/vector-2d'; @@ -7,6 +7,8 @@ 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 = []; collidingWith = {}; get aabb() { const {Position: {x: px, y: py}} = ecs.get(this.entity); @@ -76,34 +78,36 @@ export default class Collider extends Component { } 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.$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity}; - instance.$$aabbs = []; - const {bodies} = instance; - for (const body of bodies) { - let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity; - for (const point of body.points) { - const x = point.x; - const y = point.y; - if (x < x0) x0 = x; - if (x < instance.$$aabb.x0) instance.$$aabb.x0 = x; - if (x > x1) x1 = x; - if (x > instance.$$aabb.x1) instance.$$aabb.x1 = x; - if (y < y0) y0 = y; - if (y < instance.$$aabb.y0) instance.$$aabb.y0 = y; - if (y > y1) y1 = y; - if (y > instance.$$aabb.y1) instance.$$aabb.y1 = y; - } - instance.$$aabbs.push({ - x0: x0 > x1 ? x1 : x0, - x1: x0 > x1 ? x0 : x1, - y0: y0 > y1 ? y1 : y0, - y1: y0 > y1 ? y0 : y1, - }); - } - + instance.updateAabbs(); // heavy handed... if ('undefined' !== typeof window) { return; diff --git a/app/ecs/systems/colliders.js b/app/ecs/systems/colliders.js index 4c96a68..068792a 100644 --- a/app/ecs/systems/colliders.js +++ b/app/ecs/systems/colliders.js @@ -40,7 +40,16 @@ export default class Colliders extends System { tick() { const collisions = new Map(); + for (const entity of this.ecs.changed(['Direction'])) { + if (!entity.Collider) { + continue; + } + entity.Collider.updateAabbs(); + } for (const entity of this.ecs.changed(['Position'])) { + if (!entity.Collider) { + continue; + } this.updateHash(entity); } for (const entity of this.ecs.changed(['Position'])) { diff --git a/app/react/components/ui.jsx b/app/react/components/ui.jsx index 02c2da4..dd2f110 100644 --- a/app/react/components/ui.jsx +++ b/app/react/components/ui.jsx @@ -390,6 +390,9 @@ function Ui({disconnected}) { if (!update) { continue; } + if (update.Direction && entity.Collider) { + entity.Collider.updateAabbs(); + } if (update.Sound?.play) { for (const sound of update.Sound.play) { (new Audio(sound)).play(); diff --git a/app/util/math.js b/app/util/math.js index fd5c254..f7b1b08 100644 --- a/app/util/math.js +++ b/app/util/math.js @@ -316,3 +316,52 @@ export function removeCollinear([...vertices]) { } return trimmed; } + +export function transform( + vertices, + { + rotation = 0, + scale = 1, + translation = {x: 0, y: 0}, + origin = {x: 0, y: 0}, + }, +) { + // nop + if (0 === rotation && 1 === scale && 0 === translation.x && 0 === translation.y) { + return vertices; + } + const transformed = []; + // scale + for (const vertice of vertices) { + if (1 === scale) { + transformed.push({x: vertice.x, y: vertice.y}); + continue; + } + transformed.push({ + x: origin.x + (vertice.x - origin.x) * scale, + y: origin.y + (vertice.y - origin.y) * scale, + }); + } + // rotation + rotation = rotation % TAU; + if (0 !== rotation) { + for (const vertice of transformed) { + let a = rotation + Math.atan2( + vertice.y - origin.y, + vertice.x - origin.x, + ); + a = (a >= 0 || a < TAU) ? a : (a % TAU + TAU) % TAU; + const d = distance(vertice, origin); + vertice.x = origin.x + d * Math.cos(a); + vertice.y = origin.y + d * Math.sin(a); + } + } + // translation + if (0 !== translation.x || 0 !== translation.y) { + for (const vertice of transformed) { + vertice.x += translation.x; + vertice.y += translation.y; + } + } + return transformed; +} diff --git a/app/util/math.test.js b/app/util/math.test.js new file mode 100644 index 0000000..1db7f12 --- /dev/null +++ b/app/util/math.test.js @@ -0,0 +1,87 @@ +import {expect, test} from 'vitest'; + +import * as MathUtil from './math.js'; + +test('transforms vertices', async () => { + const expectCloseTo = (l, r) => { + expect(l.length) + .to.equal(r.length); + for (let i = 0; i < l.length; ++i) { + expect(l[i].x) + .to.be.closeTo(r[i].x, 0.0001); + expect(l[i].y) + .to.be.closeTo(r[i].y, 0.0001); + } + } + const vertices = [ + {x: -1, y: -1}, + {x: 1, y: -1}, + {x: 1, y: 1}, + {x: -1, y: 1}, + ]; + expect(MathUtil.transform(vertices, {})) + .to.deep.equal(vertices); + expectCloseTo( + MathUtil.transform(vertices, {scale: 2}), + [ + {x: -2, y: -2}, + {x: 2, y: -2}, + {x: 2, y: 2}, + {x: -2, y: 2}, + ], + ); + expectCloseTo( + MathUtil.transform(vertices, {rotation: MathUtil.QUARTER_PI}), + [ + {x: 0, y: -Math.sqrt(2)}, + {x: Math.sqrt(2), y: 0}, + {x: 0, y: Math.sqrt(2)}, + {x: -Math.sqrt(2), y: 0}, + ], + ); + expectCloseTo( + MathUtil.transform(vertices, {rotation: MathUtil.QUARTER_PI, scale: 2}), + [ + {x: 0, y: -Math.sqrt(2) * 2}, + {x: Math.sqrt(2) * 2, y: 0}, + {x: 0, y: Math.sqrt(2) * 2}, + {x: -Math.sqrt(2) * 2, y: 0}, + ], + ); + expectCloseTo( + MathUtil.transform(vertices, {translation: {x: 10, y: 10}}), + [ + {x: 9, y: 9}, + {x: 11, y: 9}, + {x: 11, y: 11}, + {x: 9, y: 11}, + ], + ); + expectCloseTo( + MathUtil.transform(vertices, {rotation: MathUtil.QUARTER_PI, translation: {x: 10, y: 10}}), + [ + {x: 10, y: 10 - Math.sqrt(2)}, + {x: 10 + Math.sqrt(2), y: 10}, + {x: 10, y: 10 + Math.sqrt(2)}, + {x: 10 - Math.sqrt(2), y: 10}, + ], + ); + expectCloseTo( + MathUtil.transform(vertices, {scale: 2, translation: {x: 10, y: 10}}), + [ + {x: 8, y: 8}, + {x: 12, y: 8}, + {x: 12, y: 12}, + {x: 8, y: 12}, + ], + ); + expectCloseTo( + MathUtil.transform(vertices, {rotation: MathUtil.QUARTER_PI, scale: 2, translation: {x: 10, y: 10}}), + [ + {x: 10, y: 10 - Math.sqrt(2) * 2}, + {x: 10 + Math.sqrt(2) * 2, y: 10}, + {x: 10, y: 10 + Math.sqrt(2) * 2}, + {x: 10 - Math.sqrt(2) * 2, y: 10}, + ], + ); +});