TQ57VE45BHV7MOZ6GKTYZEAMAOTXLPQ3ROCWJ2FUCITQWOYVMUIAC export const IS_PRODUCTION = !window.location.href.includes("localhost") || window.location.href.includes("debug=false");export const IS_DEVELOPMENT = !IS_PRODUCTION;export const IS_DEBUG = (IS_DEVELOPMENT || window.location.href.includes("debug=true")) && !(window.location.href.includes("debug=false"));
const KeyInfo = () => ({Q : false,W : false,E : false,R : false,T : false,Y : false,U : false,I : false,O : false,P : false,A : false,S : false,D : false,F : false,G : false,H : false,J : false,K : false,L : false,Z : false,X : false,C : false,V : false,B : false,N : false,M : false,Up : false,Down : false,Left : false,Right : false,Shift : false,Spacebar: false,Enter : false,});export type KeyInfoType = ReturnType<typeof KeyInfo>;interface QueuedKeyboardEvent {isDown: boolean;event : KeyboardEvent;}export class KeyboardState {public down = KeyInfo();public justDown = KeyInfo();public justUp = KeyInfo();private _queuedEvents: QueuedKeyboardEvent[] = [];constructor() {document.addEventListener("keydown", e => this.keyDown(e), false);document.addEventListener("keyup" , e => this.keyUp(e), false);window.addEventListener("blur" , () => {this.clear();}, false);}public clear() {this.down = KeyInfo();this.justDown = KeyInfo();this.justUp = KeyInfo();this._queuedEvents = [];}private keyUp(e: KeyboardEvent): void {// Since events usually happen between two ticks, we queue them up to be// processed on the next tick.this._queuedEvents.push({ event: e, isDown: false });}private keyDown(e: KeyboardEvent): void {this._queuedEvents.push({ event: e, isDown: true });}private eventToKey(event: KeyboardEvent): string {const number = event.keyCode || event.which;let str: string;switch (number) {case 13: str = "Enter"; break;case 16: str = "Shift"; break;case 37: str = "Left" ; break;case 38: str = "Up" ; break;case 39: str = "Right"; break;case 40: str = "Down" ; break;/* A-Z */default: str = String.fromCharCode(number);}if (str === " ") {return "Spacebar";}if (str.length === 1) {return str.toUpperCase();}return str[0].toUpperCase() + str.slice(1);}update(): void {for (const key of Object.keys(this.justDown)) {this.justDown[key as keyof KeyInfoType] = false;this.justUp[key as keyof KeyInfoType] = false;}for (const queuedEvent of this._queuedEvents) {const key = this.eventToKey(queuedEvent.event);if (queuedEvent.isDown) {if (!this.down[key as keyof KeyInfoType]) {this.justDown[key as keyof KeyInfoType] = true;}this.down[key as keyof KeyInfoType] = true;} else {if (this.down[key as keyof KeyInfoType]) {this.justUp[key as keyof KeyInfoType] = true;}this.down[key as keyof KeyInfoType] = false;}}this._queuedEvents = [];}}
// 2D array that allows for negative indicesexport class DefaultGrid<T> {private _data: { [key: number]: { [key: number]: T} } = {};private _makeDefault: (x: number, y: number) => T;private _count = 0;constructor(makeDefault: (x: number, y: number) => T) {this._makeDefault = makeDefault;}getCount() {return this._count;}keys(): { x: number, y: number }[] {const result: { x: number, y: number }[] = [];for (const x of Object.keys(this._data)) {const inner = this._data[Number(x)];for (const y of Object.keys(inner)) {result.push({x: Number(x),y: Number(y),});}}return result;}values(): T[] {const result: T[] = [];for (const x of Object.keys(this._data)) {const inner = this._data[Number(x)];for (const y of Object.keys(inner)) {result.push(inner[Number(y)]);}}return result;}set(x: number, y: number, value: T) {if (!this._data[x]) {this._data[x] = {};}if (!this._data[x][y]) {this._count++;}this._data[x][y] = value;}get(x: number, y: number): T {if (!this._data[x]) {this._data[x] = {};}if (this._data[x][y] === undefined) {this._data[x][y] = this._makeDefault(x, y);}return this._data[x][y];}}
// 2D array that allows for negative indicesexport class Grid<T> {private _data: { [key: number]: { [key: number]: T} } = {};getCount() {let count = 0;for (const key of Object.keys(this._data)) {const inner = this._data[Number(key)];count += Object.keys(inner).length;}return count;}keys(): { x: number, y: number }[] {const result: { x: number, y: number }[] = [];for (const x of Object.keys(this._data)) {const inner = this._data[Number(x)];for (const y of Object.keys(inner)) {result.push({x: Number(x),y: Number(y),});}}return result;}set(x: number, y: number, value: T) {if (!this._data[x]) {this._data[x] = {};}this._data[x][y] = value;}get(x: number, y: number): T | null {if (!this._data[x]) {return null;}if (this._data[x][y] === undefined) {return null;}return this._data[x][y];}getOrDefault(x: number, y: number, otherwise: T): T {const result = this.get(x, y);if (result === null) {return otherwise;} else {return result;}}}
export class HashSet<K extends { hash(): string }> {private _values: HashMap<K, K>;constructor(initialValues: K[] = []) {this._values = new HashMap<K, K>();for (const value of initialValues) {this.put(value);}}remove(key: K): void {this._values.remove(key);}put(key: K): void {this._values.put(key, key);}get(key: K): boolean {return this._values.get(key) !== undefined;}values(): K[] {return this._values.values();}}export class HashMap<K extends { hash(): string }, V> {private _values: { [key: string]: V } = {};put(key: K, value: V) {this._values[key.hash()] = value;}remove(key: K): void {delete this._values[key.hash()];}get(key: K): V {return this._values[key.hash()];}values(): V[] {return Object.keys(this._values).map(key => this._values[key]);}}export class DefaultHashMap<K extends { hash(): string }, V> {private _values: { [key: string]: V } = {};private _makeDefault: () => V;constructor(makeDefaultValue: () => V) {this._makeDefault = makeDefaultValue;}put(key: K, value: V) {this._values[key.hash()] = value;}get(key: K): V {if (this._values[key.hash()] === undefined) {this._values[key.hash()] = this._makeDefault();}return this._values[key.hash()];}}
export class Pair<T extends { hash(): string }, U extends { hash(): string }> {private _first: T;private _second: U;constructor(first: T, second: U) {this._first = first;this._second = second;}hash(): string {return `${ this._first.hash() }|${ this._second.hash() }`}get first() {return this._first;}get second() {return this._second;}}
export const EPSILON = 0.0000001;export const epsEqual = (x: number, y: number) => {return Math.abs(x - y) < EPSILON;}export const epsGreaterThan = (x: number, y: number) => {return (x + EPSILON - y) > 0;}export const epsLessThan = (x: number, y: number) => {return (x - EPSILON - y) < 0;}
import { Util } from '../util';import { Line } from './line';import { Rect } from './rect';import { Vector2 } from './vector2';const MAX_SIZE = 500;export class ArbitrarySelection {cover: Rect[] = [];outlinesDirty = true;oldOutlines: Line[][] = [];constructor(cover: Rect[] = []) {this.cover = cover;}public get x(): number {if (this.cover.length === 0) { return 0; }return Util.MinBy(this.cover, r => r.x)!.x;}public get y(): number {if (this.cover.length === 0) { return 0; }return Util.MinBy(this.cover, r => r.y)!.y;}public get w(): number {if (this.cover.length === 0) { return 0; }return Util.MaxBy(this.cover, r => r.right)!.right -Util.MinBy(this.cover, r => r.x)!.x;}public get h(): number {if (this.cover.length === 0) { return 0; }return Util.MaxBy(this.cover, r => r.bottom)!.bottom -Util.MinBy(this.cover, r => r.y)!.y;}public get pos(): Vector2 {return new Vector2({ x: this.x, y: this.y });}public get bounds(): Rect {return new Rect({x: this.x,y: this.y,width: this.w,height: this.h,});}public get isEmpty(): boolean {return this.cover.length === 0;}reset(): void {this.cover = [];this.oldOutlines = [];}addRect(rectToAdd: Rect): void {const subsumingRects = this.cover.filter(r => r.completelyContains(rectToAdd));const intersectingRects = this.cover.filter(r => r.intersects(rectToAdd, { edgesOnlyIsAnIntersection: false }));if (subsumingRects.length > 0) {return;}for (const rect of intersectingRects) {this.subtractRect(rect.getIntersection(rectToAdd)!);}this.cover.push(rectToAdd);this.outlinesDirty = true;}subtractRect(rectToSubtract: Rect): void {const intersectingRects = this.cover.filter(r => r.intersects(rectToSubtract, { edgesOnlyIsAnIntersection: false }));for (const rect of intersectingRects) {// rectToSubtract completely contains rectif (rectToSubtract.completelyContains(rect)) {continue;}// rectToSubtract partially contains rectconst subrectToRemove = rectToSubtract.getIntersection(rect)!;// rect completely contains subtractedRect// -------------------------// | A |// | |// |-----------------------|// | B | hole | C |// |-----------------------|// | |// | D |// -------------------------const newRects = [{ x: rect.x , y: rect.y , width: rect.width , height: subrectToRemove.y - rect.y }, // A{ x: rect.x , y: subrectToRemove.y , width: subrectToRemove.x - rect.x , height: subrectToRemove.height }, // B{ x: subrectToRemove.x + subrectToRemove.width, y: subrectToRemove.y , width: rect.x + rect.width - (subrectToRemove.width + subrectToRemove.x), height: subrectToRemove.height }, // C{ x: rect.x , y: subrectToRemove.y + subrectToRemove.height, width: rect.width , height: rect.y + rect.height - (subrectToRemove.y + subrectToRemove.height) }, // D].filter(r => r.width > 0 && r.height > 0).map(r => new Rect(r));this.cover = this.cover.concat(newRects);}for (const rect of intersectingRects) {this.cover.splice(this.cover.indexOf(rect), 1);}this.outlinesDirty = true;if (this.isEmpty) {this.reset();}}// O(n^2) scc algorithm until someone convinces me I need a faster onegetConnectedComponents(): Rect[][] {const components: Rect[][] = [];const seenRects: { [key: string]: boolean } = {}for (const rect of this.cover) {if (seenRects[rect.serialize()]) { continue; }const component = this.getConnectedComponentFrom(rect);components.push(component);for (const seen of component) {seenRects[seen.serialize()] = true;}}return components;}private getConnectedComponentFrom(start: Rect): Rect[] {const component: { [key: string]: boolean } = { };let edge = [start];while (edge.length > 0) {let newEdge: Rect[] = [];for (const rect of edge) {if (component[rect.serialize()]) { continue; }const intersectingRects = this.cover.filter(r => r.intersects(rect, { edgesOnlyIsAnIntersection: true }));component[rect.serialize()] = true;newEdge = newEdge.concat(intersectingRects);}edge = newEdge;}return Object.keys(component).map(r => Rect.DeserializeRect(r));}getOutlines(): Line[][] {if (!this.outlinesDirty) {return this.oldOutlines;}let result: Line[][] = [];const components = this.getConnectedComponents();for (const c of components) {const outline = this.getOutlineFor(c);const outlineComponents = this.getComponentsOfOutline(outline);result = result.concat(outlineComponents)}this.oldOutlines = result;this.outlinesDirty = false;return result;}private getOutlineFor(comp: Rect[]): Line[] {let allLines: (Line | undefined)[] = [];for (const rect of comp) {allLines.push.apply(allLines, rect.getLinesFromRect());}// Alternate solution if this proves too hard:// Subdivide all lines on intersection points, then remove all// duplicates.// Actually that might even be better heh// The strategy here is to basically remove all overlapping segments. it's// hard because a single line could be overlapping with multiple other// lines.for (let i = 0; i < allLines.length; i++) {const line1 = allLines[i];if (!line1) { continue; }for (let j = 0; j < allLines.length; j++) {const line2 = allLines[j];if (!line2) { continue; }if (line1 === line2) { continue; }const intersection = line1.getOverlap(line2);if (intersection) {allLines[i] = undefined;allLines[j] = undefined;const newLines = line1.getNonOverlappingSections(line2);allLines = allLines.concat(newLines);break;}}}return allLines.filter(l => l !== undefined) as Line[];}private getComponentsOfOutline(outline: Line[]): Line[][] {// Store lookup table by start and end vertexlet lookupTable: { [key: number]: Line[] } = [];for (const line of outline) {const idx1 = line.x1 * MAX_SIZE + line.y1;const idx2 = line.x2 * MAX_SIZE + line.y2;if (!lookupTable[idx1]) { lookupTable[idx1] = []; }if (!lookupTable[idx2]) { lookupTable[idx2] = []; }lookupTable[idx1].push(line);lookupTable[idx2].push(line);}let result: Line[][] = [];let visited: { [key: string]: boolean } = {};for (const line of outline) {if (visited[line.serialized]) { continue; }visited[line.serialized] = true;const sequence = [line];while (true) {const current = sequence[sequence.length - 1];const candidates = lookupTable[current.x1 * MAX_SIZE + current.y1].concat(lookupTable[current.x2 * MAX_SIZE + current.y2]);const next = candidates.filter(l => l !== current && !visited[l.serialized])[0];if (!next) { break; }visited[next.serialized] = true;sequence.push(next);}result.push(sequence);}return result;}addArbitraryShape(pixels: Vector2[], canvasSize: Vector2): void {this.outlinesDirty = true;const covered: boolean[] = new Array(MAX_SIZE * MAX_SIZE);const rects: Rect[] = [];const ll = pixels.length;for (let i = 0; i < ll; i++) {const p = pixels[i];covered[p.x * MAX_SIZE + p.y] = false;}for (let x = 0; x < canvasSize.x; x++) {for (let y = 0; y < canvasSize.y; y++) {if (covered[x * MAX_SIZE + y] !== false) { continue; }let squareSize = 2;outer:for (; squareSize < MAX_SIZE; squareSize++) {const endSquareX = x + squareSize;const endSquareY = y + squareSize;for (let bottomLineX = x; bottomLineX < endSquareX; bottomLineX++) {if (covered[bottomLineX * MAX_SIZE + (y + squareSize - 1)] === undefined ||covered[bottomLineX * MAX_SIZE + (y + squareSize - 1)] === true) {squareSize--;break outer;}}for (let bottomLineY = y; bottomLineY < endSquareY; bottomLineY++) {if (covered[(x + squareSize - 1) * MAX_SIZE + bottomLineY] === undefined ||covered[(x + squareSize - 1) * MAX_SIZE + bottomLineY] === true) {squareSize--;break outer;}}}for (let sx = x; sx < x + squareSize; sx++) {for (let sy = y; sy < y + squareSize; sy++) {covered[sx * MAX_SIZE + sy] = true;}}rects.push(new Rect({x: x,y: y,width: squareSize,height: squareSize,}));}}for (const r of rects) {this.addRect(r);}}clone(): ArbitrarySelection {const result = new ArbitrarySelection(this.cover.slice(0));result.outlinesDirty = this.outlinesDirty;result.oldOutlines = this.oldOutlines;return result;}translate(p: Vector2): void {this.cover = this.cover.map(x => x.translate(p));this.oldOutlines = this.oldOutlines.map(l => l.map(ll => ll.translate(p)));}contains(p: Vector2): boolean {if (this.cover.length === 0) { return true; }for (const r of this.cover) {if (r.contains(p)) { return true; }}return false;}}
import { Vector2 } from "./vector2"import { Graphics } from "pixi.js";import { epsGreaterThan, epsLessThan } from "../epsilon_math";export class Line {private _x1: number;private _x2: number;private _y1: number;private _y2: number;public get x1(): number { return this._x1; }public get x2(): number { return this._x2; }public get y1(): number { return this._y1; }public get y2(): number { return this._y2; }public get start(): Vector2 { return new Vector2({ x: this.x1, y: this.y1 }); }public get end() : Vector2 { return new Vector2({ x: this.x2, y: this.y2 }); }public get angleInDegrees(): number {const cx = this._x1;const cy = this._y1;const ex = this._x2;const ey = this._y2;const dy = ey - cy;const dx = ex - cx;let theta = Math.atan2(dy, dx);theta *= 180 / Math.PI;if (theta < 0) {theta = 360 + theta;}return theta;}public serialized = "";constructor(props: { x1: number, x2: number, y1: number, y2: number } |{ start: Vector2, end: Vector2 }) {let x1, x2, y1, y2;if ('x1' in props) {x1 = props.x1;x2 = props.x2;y1 = props.y1;y2 = props.y2;} else {x1 = props.start.x;x2 = props.end.x;y1 = props.start.y;y2 = props.end.y;}this._x1 = x1;this._y1 = y1;this._x2 = x2;this._y2 = y2;this.serialized = `${ this.x1 }|${ this.x2 }|${ this.y1 }|${ this.y2 }`;}public get length(): number {return Math.sqrt((this.x2 - this.x1) * (this.x2 - this.x1) +(this.y2 - this.y1) * (this.y2 - this.y1));}public get isDegenerate(): boolean {return this.length === 0;}public rotateAbout(origin: Vector2, angle: number): Line {const start = this.start;const end = this.end;return new Line({start: start.rotate(origin, angle),end: end.rotate(origin, angle),});}public scaleAbout(about: Vector2, amount: Vector2): Line {return new Line({start: this.start.scale(about, amount),end: this.end.scale(about, amount),});}sharesAVertexWith(other: Line): Vector2 | null {if (this.start.equals(other.start)) { return this.start; }if (this.start.equals(other.end)) { return this.start; }if (this.end.equals(other.start)) { return this.end; }if (this.end.equals(other.end)) { return this.end; }return null;}static DeserializeLine(s: string): Line {const [ x1, x2, y1, y2 ] = s.split("|").map(x => Number(x));return new Line({ x1, x2, y1, y2 });}isXAligned(): boolean {return this.x1 === this.x2;}isYAligned(): boolean {return this.y1 === this.y2;}// Must be horizontally/vertically oriented lines// Does not consider intersection, only overlapgetOverlap(other: Line): Line | undefined {const orientedByX = (this.x1 === this.x2 &&this.x1 === other.x1 &&this.x1 === other.x2);const orientedByY = (this.y1 === this.y2 &&this.y1 === other.y1 &&this.y1 === other.y2);if (!orientedByX && !orientedByY) { return undefined; }const summedLength = this.length + other.length;const overallLength = new Line({x1: Math.min(this.x1, other.x1),y1: Math.min(this.y1, other.y1),x2: Math.max(this.x2, other.x2),y2: Math.max(this.y2, other.y2),}).length;if (overallLength >= summedLength) {// These lines do not overlap.return undefined;}if (orientedByX) {return new Line({x1: this.x1,x2: this.x2,y1: Math.max(this.y1, other.y1),y2: Math.min(this.y2, other.y2),});} else /* if (orientedByY) */ {return new Line({y1: this.y1,y2: this.y2,x1: Math.max(this.x1, other.x1),x2: Math.min(this.x2, other.x2),});}}// A----B----C----D// AD - BC returns AB and CD.getNonOverlappingSections(other: Line): Line[] | undefined {const orientedByX = (this.x1 === this.x2 &&this.x1 === other.x1 &&this.x1 === other.x2);const orientedByY = (this.y1 === this.y2 &&this.y1 === other.y1 &&this.y1 === other.y2);if (!orientedByX && !orientedByY) { return undefined; }const summedLength = new Line(this).length + new Line(other).length;const overallLength = new Line({x1: Math.min(this.x1, other.x1),y1: Math.min(this.y1, other.y1),x2: Math.max(this.x1, other.x1),y2: Math.max(this.y1, other.y1),}).length;if (overallLength >= summedLength) {// These lines do not overlap.return undefined;}if (orientedByX) {return [new Line({ x1: this.x1, x2: this.x2, y1: Math.min(this.y1, other.y1), y2: Math.max(this.y1, other.y1), }),new Line({ x1: this.x1, x2: this.x2, y1: Math.min(this.y2, other.y2), y2: Math.max(this.y2, other.y2), }),].filter(l => !l.isDegenerate);} else /* if (orientedByY) */ {return [new Line({ y1: this.y1, y2: this.y2, x1: Math.min(this.x1, other.x1), x2: Math.max(this.x1, other.x1), }),new Line({ y1: this.y1, y2: this.y2, x1: Math.min(this.x2, other.x2), x2: Math.max(this.x2, other.x2), }),].filter(l => !l.isDegenerate);}}clone(): Line {return new Line({ x1: this.x1, x2: this.x2, y1: this.y1, y2: this.y2 });}translate(p: Vector2): Line {return new Line({x1: this.x1 + p.x,x2: this.x2 + p.x,y1: this.y1 + p.y,y2: this.y2 + p.y,});}transform(trans: Vector2, scale: number): Line {return new Line({start: this.start.transform(trans, scale),end: this.end.transform(trans, scale),});}toJSON(): any {return {x1 : this.x1,x2 : this.x2,y1 : this.y1,y2 : this.y2,reviver: "Line",};}toString(): string {return `Line: [(${ this.x1 },${ this.y1 }) -> (${ this.x2 },${ this.y2 })]`;}equals(other: Line | null) {if (other === null) { return false; }return (this.x1 === other.x1 &&this.x2 === other.x2 &&this.y1 === other.y1 &&this.y2 === other.y2) || (this.x1 === other.x2 &&this.x2 === other.x1 &&this.y1 === other.y2 &&this.y2 === other.y1);}withNewEnd(newEnd: Vector2): Line {return new Line({x1: this.x1,y1: this.y1,x2: newEnd.x,y2: newEnd.y,});}withNewStart(newStart: Vector2): Line {return new Line({x1: newStart.x,y1: newStart.y,x2: this.x2,y2: this.y2,});}static Deserialize(obj: any): Line {if (!obj.hasOwnProperty("x1") ||!obj.hasOwnProperty("y1") ||!obj.hasOwnProperty("x2") ||!obj.hasOwnProperty("y2")) {console.error("Failed deserializing Rect");}return new Line({x1: obj.x1,y1: obj.y1,x2: obj.x2,y2: obj.y2,});}static Serialize(obj: Line): string {return JSON.stringify({x1: obj.x1,y1: obj.y1,x2: obj.x2,y2: obj.y2,});}drawOnto(graphics: Graphics, color = 0xff0000) {graphics.lineStyle(3, color, 1);graphics.moveTo(this.x1, this.y1);graphics.lineTo(this.x2, this.y2);}/*** Returns the point where these two lines, if extended arbitrarily, would* intersect.*/lineIntersection(other: Line): Vector2 {const p1 = this.start;const p2 = this.end;const p3 = other.start;const p4 = other.end;const s = ((p4.x - p3.x) *(p1.y - p3.y) -(p4.y - p3.y) *(p1.x - p3.x)) / ((p4.y - p3.y) *(p2.x - p1.x) -(p4.x - p3.x) *(p2.y - p1.y));const x = p1.x + s * (p2.x - p1.x);const y = p1.y + s * (p2.y - p1.y);return new Vector2({ x, y });}/*** Returns the point where these two segments exist, if there is one.*/segmentIntersection(other: Line): Vector2 | null {const lineIntersection = this.lineIntersection(other);const x = lineIntersection.x;const y = lineIntersection.y;if ((// within usepsGreaterThan(x, Math.min(this.x1, this.x2)) &&epsLessThan (x, Math.max(this.x1, this.x2)) &&epsGreaterThan(y, Math.min(this.y1, this.y2)) &&epsLessThan (y, Math.max(this.y1, this.y2))) && (// within otherepsGreaterThan(x, Math.min(other.x1, other.x2)) &&epsLessThan (x, Math.max(other.x1, other.x2)) &&epsGreaterThan(y, Math.min(other.y1, other.y2)) &&epsLessThan (y, Math.max(other.y1, other.y2)))) {return lineIntersection;}return null;}normalize(): Line {const mag = Math.sqrt((this.x1 - this.x2) ** 2 +(this.y1 - this.y2) ** 2);return new Line({start: this.start,end: new Vector2({x: this.start.x + (this.end.x - this.start.x) / mag,y: this.start.x + (this.end.y - this.start.y) / mag,})})}hash(): string {return this.toString();}add(x: Vector2): Line {return new Line({start: this.start.add(x),end: this.end.add(x),})}}
import { Line } from "./line";import { Vector2, IVector2 } from "./vector2";/*** Immutable rectangle class.*/export class Rect {private _x: number;private _y: number;private _width: number;private _height: number;public get x(): number {return this._x;}public get y(): number {return this._y;}public get width(): number {return this._width;}public get height(): number {return this._height;}public get centerX(): number {return this._x + this._width / 2;}public get centerY(): number {return this._y + this._height / 2;}public get right(): number {return this._x + this._width;}public get bottom(): number {return this._y + this._height;}public get top(): number {return this._y;}public get left(): number {return this._x;}public get center(): Vector2 {return new Vector2({x: this.x + this.width / 2,y: this.y + this.height / 2,});}public get dimensions(): Vector2 {return new Vector2({ x: this.width, y: this.height });}public static FromPoint(point: IVector2, size: number): Rect {return new Rect({x: point.x,y: point.y,width: size,height: size,});}public static FromPoints(p1: IVector2, p2: IVector2): Rect {return new Rect({x: Math.min(p1.x, p2.x),y: Math.min(p1.y, p2.y),width: Math.abs(p1.x - p2.x),height: Math.abs(p1.y - p2.y),});}public static FromCenter(props: {center: IVector2;dimensions: IVector2;}): Rect {return new Rect({x: props.center.x - props.dimensions.x / 2,y: props.center.y - props.dimensions.y / 2,width: props.dimensions.x,height: props.dimensions.y,});}public withRight(value: number): Rect {return new Rect({x: this.x,y: this.y,width: value - this.x,height: this.height,});}public withWidth(value: number): Rect {return new Rect({x: this.x,y: this.y,width: value,height: this.height,});}public withHeight(value: number): Rect {return new Rect({x: this.x,y: this.y,width: this.width,height: value,});}public withBottom(value: number): Rect {return new Rect({x: this.x,y: this.y,width: this.width,height: value - this.y,});}public withX(value: number): Rect {return new Rect({x: value,y: this.y,width: this.width,height: this.height,});}public withY(value: number): Rect {return new Rect({x: this.x,y: value,width: this.width,height: this.height,});}public withTop(value: number): Rect {return this.withY(value);}public withLeft(value: number): Rect {return this.withX(value);}/*** bottomRight is held constant.*/public withTopLeft(topLeft: IVector2): Rect {return Rect.FromPoints(topLeft, this.bottomRight);}/*** bottomLeft is held constant.*/public withTopRight(topRight: IVector2): Rect {return Rect.FromPoints(topRight, this.bottomLeft);}/*** topLeft is held constant.*/public withBottomRight(bottomRight: IVector2): Rect {return Rect.FromPoints(bottomRight, this.topLeft);}/*** topRight is held constant.*/public withBottomLeft(bottomLeft: IVector2): Rect {return Rect.FromPoints(bottomLeft, this.topRight);}public withCenter(center: IVector2): Rect {return new Rect({x: center.x - this.width / 2,y: center.y - this.height / 2,width: this.width,height: this.height,});}/*** center is held constant*/public withScale(props: { width?: number; height?: number }): Rect {return new Rect({x: this.centerX - (props.width || this.width) / 2,y: this.centerY - (props.height || this.height) / 2,width: props.width || this.width,height: props.height || this.height,});}public get topLeft(): Vector2 {return new Vector2({x: this.x,y: this.y,});}public get topRight(): Vector2 {return new Vector2({x: this.right,y: this.y,});}public get bottomRight(): Vector2 {return new Vector2({x: this.right,y: this.bottom,});}public get bottomLeft(): Vector2 {return new Vector2({x: this.x,y: this.bottom,});}constructor(props: { x: number; y: number; width: number; height: number }) {this._x = props.x;this._y = props.y;this._width = props.width;this._height = props.height;}static DeserializeRect(s: string): Rect {const [x, y, w, h] = s.split("|").map((x) => Number(x));return new Rect({ x, y, width: w, height: h });}/*** Return the four edges of this Rect as Lines.*/getLinesFromRect(): Line[] {return [new Line({ x1: this.x, y1: this.y, x2: this.x + this.width, y2: this.y }),new Line({x1: this.x,y1: this.y,x2: this.x,y2: this.y + this.height,}),new Line({x1: this.x + this.width,y1: this.y + this.height,x2: this.x + this.width,y2: this.y,}),new Line({x1: this.x + this.width,y1: this.y + this.height,x2: this.x,y2: this.y + this.height,}),];}/*** Return the four corners of this Rect.*/getCorners(): Vector2[] {return [new Vector2({ x: this.x, y: this.y }),new Vector2({ x: this.x + this.width, y: this.y }),new Vector2({ x: this.x, y: this.y + this.height }),new Vector2({ x: this.x + this.width, y: this.y + this.height }),];}serialize(): string {return `${this.x}|${this.y}|${this.width}|${this.height}`;}// consider overlapping edges as intersection, but not overlapping corners.intersects(other: Rect,props: { edgesOnlyIsAnIntersection: boolean } = {edgesOnlyIsAnIntersection: false,}): boolean {const intersection = this.getIntersection(other, true);if (props.edgesOnlyIsAnIntersection) {return (!!intersection && (intersection.width > 0 || intersection.height > 0));} else {return !!intersection && intersection.width * intersection.height > 0;}}completelyContains(smaller: Rect): boolean {return (this.x <= smaller.x &&this.x + this.width >= smaller.x + smaller.width &&this.y <= smaller.y &&this.y + this.height >= smaller.y + smaller.height);}getIntersection(other: Rect,edgesOnlyIsAnIntersection = false): Rect | undefined {const xmin = Math.max(this.x, other.x);const xmax1 = this.x + this.width;const xmax2 = other.x + other.width;const xmax = Math.min(xmax1, xmax2);if (xmax > xmin || (edgesOnlyIsAnIntersection && xmax >= xmin)) {const ymin = Math.max(this.y, other.y);const ymax1 = this.y + this.height;const ymax2 = other.y + other.height;const ymax = Math.min(ymax1, ymax2);if (ymax >= ymin || (edgesOnlyIsAnIntersection && ymax >= ymin)) {return new Rect({x: xmin,y: ymin,width: xmax - xmin,height: ymax - ymin,});}}return undefined;}contains(p: IVector2): boolean {return (p.x >= this.x &&p.x < this.x + this.width &&p.y >= this.y &&p.y < this.y + this.height);}clone(): Rect {return new Rect({x: this.x,y: this.y,width: this.width,height: this.height,});}add(p: IVector2): Rect {return this.translate(p);}subtract(p: IVector2): Rect {return this.translate({ x: -p.x, y: -p.y });}translate(p: IVector2): Rect {return new Rect({x: this.x + p.x,y: this.y + p.y,width: this.width,height: this.height,});}scale(p: Vector2): Rect {return new Rect({x: this.x,y: this.y,width: this.width * p.x,height: this.height * p.y,});}centeredAtOrigin(): Rect {return new Rect({x: -this.width / 2,y: -this.height / 2,width: this.width,height: this.height,});}equals(o: Rect | undefined | null): boolean {if (!o) {return false;}return (this.x === o.x &&this.y === o.y &&this.width === o.width &&this.height === o.height);}toJSON(): any {return {x: this.x,y: this.y,w: this.width,h: this.height,reviver: "Rect",};}hash(): string {return this.toString();}/*** Adds amount to both width and height.*/extend(amount: number): Rect {return new Rect({x: this.x,y: this.y,width: this.width + amount,height: this.height + amount,});}shrink(amount: number): Rect {return new Rect({x: this.x + amount,y: this.y + amount,width: Math.max(this.width - amount * 2, 0),height: Math.max(this.height - amount * 2, 0),});}floor(): Rect {return new Rect({x: Math.floor(this.x),y: Math.floor(this.y),width: Math.floor(this.width),height: Math.floor(this.height),});}/*** Grow the Rect by amount in all directions.*/expand(amount: number): Rect {return this.shrink(-amount);}transform(trans: Vector2, scale: number): Rect {const topLeft = this.topLeft.transform(trans, scale);const botRight = this.bottomRight.transform(trans, scale);return new Rect({x: topLeft.x,y: topLeft.y,width: botRight.x - topLeft.x,height: botRight.y - topLeft.y,});}static Deserialize(obj: any): Rect {if (!obj.hasOwnProperty("x") ||!obj.hasOwnProperty("y") ||!obj.hasOwnProperty("w") ||!obj.hasOwnProperty("h")) {console.error("Failed deserializing Rect");}return new Rect({x: obj.x,y: obj.y,width: obj.w,height: obj.h,});}static Serialize(r: Rect): string {return JSON.stringify({x: r.x,y: r.y,w: r.width,h: r.height,});}toString(): string {return `[${this.x}, ${this.y}]`;}}
import { Rect } from "./rect";import { Vector2 } from "./vector2";export class RectGroup {private _rects: Rect[];constructor(rects: Rect[]) {this._rects = rects;}intersects(other: Rect | RectGroup) {if (other instanceof Rect) {for (const rect of this._rects) {if (rect.intersects(other)) {return true;}}return false;}if (other instanceof RectGroup) {for (const r1 of this._rects) {for (const r2 of this._rects) {if (r1.intersects(r2)) {return true;}}}return false;}}getRects(): Rect[] {return this._rects;}add(delta: Vector2): RectGroup {const newRects = this._rects.map(rect => rect.add(delta));return new RectGroup(newRects);}subtract(delta: Vector2): RectGroup {const newRects = this._rects.map(rect => rect.subtract(delta));return new RectGroup(newRects);}}
import { EPSILON } from "../epsilon_math";import { Util } from "../util";export interface IVector2 {x: number;y: number;}export class Vector2 {private _x: number;private _y: number;public get x(): number { return this._x; }public get y(): number { return this._y; }constructor();constructor(x: number, y: number);constructor(props: { x: number, y: number });constructor(propsOrX: { x: number, y: number } | number = { x: 0, y: 0 }, y?: number) {if (typeof propsOrX === "number") {this._x = propsOrX;this._y = y!;} else {this._x = propsOrX.x;this._y = propsOrX.y;}}public get half(): Vector2 {return new Vector2({ x: this.x / 2, y: this.y / 2 });}public static Zero: Vector2 = new Vector2(0, 0);public static One: Vector2 = new Vector2(1, 1);static IsVector2(x: any): x is Vector2 {return x instanceof Vector2;}static Random(highX: number, highY: number, lowX = 0, lowY = 0) {return new Vector2({x: Util.RandRange(lowX, highX),y: Util.RandRange(lowY, highY),});}hash(): string {return this.toString();}toString(): string {return `[${this.x}, ${this.y}]`;}invert(): Vector2 {return new Vector2({x: -this.x,y: -this.y,});}round(): Vector2 {return new Vector2({x: Math.round(this.x),y: Math.round(this.y),});}floor(): Vector2 {return new Vector2({x: Math.floor(this.x),y: Math.floor(this.y),});}taxicabDistance(p: Vector2): number {return Math.abs(p.x - this.x) + Math.abs(p.y - this.y);}diagonalDistance(p: IVector2): number {return Math.max(Math.abs(p.x - this.x), Math.abs(p.y - this.y));}distance(p: IVector2): number {let dx = Math.abs(p.x - this.x);let dy = Math.abs(p.y - this.y);return Math.sqrt(dx * dx + dy * dy);}translate(p: { x: number, y: number }): Vector2 {return new Vector2({x: this.x + p.x,y: this.y + p.y,});}subtract(p: { x: number, y: number }): Vector2 {return new Vector2({x: this.x - p.x,y: this.y - p.y,});}add(p: { x: number, y: number }): Vector2 {return new Vector2({x: this.x + p.x,y: this.y + p.y,});}addX(x: number): Vector2 {return new Vector2({x: this.x + x,y: this.y,});}addY(y: number): Vector2 {return new Vector2({x: this.x,y: this.y + y,});}subtractX(x: number): Vector2 {return new Vector2({x: this.x - x,y: this.y,});}subtractY(y: number): Vector2 {return new Vector2({x: this.x,y: this.y - y,});}clampY(low: number, high: number): Vector2 {let newY = this.y;if (newY < low) { newY = low; }if (newY > high) { newY = high; }return new Vector2({x: this.x,y: newY,});}scale(about: { x: number; y: number }, amount: { x: number; y: number }): Vector2 {return new Vector2({x: (this.x - about.x) * amount.x + about.x,y: (this.y - about.y) * amount.y + about.y,});}rotate(origin: Vector2, angle: number): Vector2 {angle = angle / (180 / Math.PI);return new Vector2({x: Math.cos(angle) * (this.x - origin.x) - Math.sin(angle) * (this.y - origin.y) + origin.x,y: Math.sin(angle) * (this.x - origin.x) + Math.cos(angle) * (this.y - origin.y) + origin.y,});}equals(other: Vector2 | undefined): boolean {if (other === undefined) {return false;}return (Math.abs(this.x - other.x) < EPSILON &&Math.abs(this.y - other.y) < EPSILON);}multiply(other: Vector2 | number): Vector2 {if (typeof other === "number") {return new Vector2({x: this.x * other,y: this.y * other,});} else {return new Vector2({x: this.x * other.x,y: this.y * other.y,});}}divide(other: Vector2 | number): Vector2 {if (typeof other === "number") {return new Vector2({x: this.x / other,y: this.y / other,});} else {return new Vector2({x: this.x / other.x,y: this.y / other.y,});}}toJSON(): any {return {__type: "Vector2",x: this.x,y: this.y,}}transform(trans: Vector2, scale: number): Vector2 {return new Vector2({x: Math.floor((this.x - trans.x) * scale),y: Math.floor((this.y - trans.y) * scale),});}normalize(): Vector2 {if (this.x === 0 && this.y === 0) {return this;}const length = Math.sqrt(this.x * this.x + this.y * this.y);return new Vector2({x: this.x / length,y: this.y / length});}withX(newX: number): Vector2 {return new Vector2({x: newX,y: this.y,});}withY(newY: number): Vector2 {return new Vector2({x: this.x,y: newY,});}invertX(): Vector2 {return new Vector2({x: -this.x,y: this.y,});}lerp(other: Vector2, t: number): Vector2 {if (t > 1 || t < 0) { console.error("Lerp t must be between 0 and 1."); }if (t === 0) return this;if (t === 1) return other;return this.scale({ x: 0, y: 0 }, { x: 1 - t, y: 1 - t }).add(other.scale({ x: 0, y: 0 }, { x: t, y: t }))}lerp2D(other: Vector2, tx: number, ty: number): Vector2 {if (tx > 1 || tx < 0 || ty > 1 || ty < 0) { console.error("Lerp t must be between 0 and 1."); }return this.scale({ x: 0, y: 0 }, { x: 1 - tx, y: 1 - ty }).add(other.scale({ x: 0, y: 0 }, { x: tx, y: ty }))}coserp(other: Vector2, t: number): Vector2 {t = 0.5 * (1 + Math.cos(2 * t * Math.PI));return this.lerp(other, t);}static Deserialize(obj: any): Vector2 {if (!obj.hasOwnProperty("x") || !obj.hasOwnProperty("y")) {console.error("Failed deserializing point");}return new Vector2({x: obj.x,y: obj.y,});}static Serialize(obj: Vector2): string {return JSON.stringify({ x: obj.x, y: obj.y });}}
let lastUsedId = 0;export const getUniqueID = () => {return lastUsedId++;};export class Util {static MinBy<T>(list: T[], fn: (T: T) => number): T | null {let lowestT: T | null = null;let lowestValue: number | null = null;for (const item of list) {const value = fn(item);if (lowestValue === null || value < lowestValue) {lowestT = item;lowestValue = value;}}return lowestT;}static MinByAndValue<T>(list: T[], fn: (T: T) => number): { obj: T, value: number } | null {let lowestT: T | null = null;let lowestValue: number | null = null;for (const item of list) {const value = fn(item);if (lowestValue === null || value < lowestValue) {lowestT = item;lowestValue = value;}}return lowestT === null || lowestValue === null ? null : { obj: lowestT, value: lowestValue };}static MaxBy<T>(list: T[], fn: (T: T) => number): T | null {let highestT: T | null = null;let highestValue: number | null = null;for (const item of list) {const value = fn(item);if (highestValue === null || value > highestValue) {highestT = item;highestValue = value;}}return highestT;}static RandRange(low: number, high: number): number {return Math.floor(Math.random() * (high - low) + low);}public static SortByKey<T>(array: T[], key: (x: T) => number): T[] {return array.sort((a, b) => {return key(a) - key(b)});}public static ReplaceAll(str: string,mapObj: { [key: string]: string }): string {const re = new RegExp(Object.keys(mapObj).join('|'), 'gi')return str.replace(re, matched => {return mapObj[matched.toLowerCase()]});}public static Debounce<F extends (...args: any[]) => void>(func: F,waitMilliseconds = 50,options = {isImmediate: false,}): F {let timeoutId: any; // types are different on node vs client, so we have to use any.const result = (...args: any[]) => {const doLater = () => {timeoutId = undefined;if (!options.isImmediate) {func.apply(this, args);}}const shouldCallNow = options.isImmediate && timeoutId === undefined;if (timeoutId !== undefined) {clearTimeout(timeoutId);}timeoutId = setTimeout(doLater, waitMilliseconds);if (shouldCallNow) {func.apply(this, args);}}return result as any;}public static FormatDate(d: Date): string {const monthName = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec',][d.getMonth()]return `${monthName} ${d.getDate()}, ${('00' + d.getHours()).substr(-2)}:${('00' + d.getMinutes()).substr(-2)}:${('00' + d.getSeconds()).substr(-2)}`;}public static FlattenByOne<T>(arr: T[][]): T[] {let result: T[] = []for (const obj of arr) {result = result.concat(obj)}return result}public static PadString(string: string, length: number, intersperse = "", character = " ") {return string + intersperse + character.repeat(length - string.length);}}