PECLRX4APYEWDBPHDM2YDWFF4O6VHK2MJYETSQGKIRKAEBS254YQC
2EGPRWETVM2XPSRMGYM25Y46RXPW7IOCN6MDIV6POCPHNOEGSMHAC
627ZCA4JTONT2DQPA2FU44FRSCXZGA6P3YAGTPL76K3UJVYGMFWAC
T46XFRUL5T6SFCNY34NJFKJXDO3UTN6RZFLBVTAQZ4XFUEFBHRLQC
KHAKSWQANUZL7B3EYF6GWTDGSUR2WVZGU5BPPI6OKYIPCD3QMWAQC
LDXXGEDWISRDMLBW7L2NG6X5757WVFHO7VULU4CINL2FN7MHLEKQC
JG36CDUKVUWJT25PJVMWKWI4KFRHM24PFBKPXXRY3D2XVDKUVI7QC
M7KYKQQLNUSF3YVPTEJCYGJRW2RS3N7AWSSZ75IDYP3IM2LNRT4QC
NOJDNQ5JIUX4ZIAQQL3KKIIUC5W6QZJ3H6RWUIUZFV5GQEJDFLLQC
M4DT5MAD3TATSVAT3MFNABHONWXOW443XD5W6DGEVVYHVITXVBDAC
NQA6YRMRCF7LIHPYWLC355FPCH7CYA3QKSKQHSNJHA2XHOCWZXCAC
FK53AOEMTSFJOB5TCHLDOE2EHAE5W5P2LV3ZNENYSGQGV3UCBWRAC
import { Application, Renderer, Point } from "pixi.js";
import { Entity } from "./entity";
import { Debug } from "./debug";
import { HashSet } from "./data_structures/hash";
import { TypesafeLoader, AllResourcesType } from "./typesafe_loader";
import { CreateGame as ReactMountGame } from "./react/react_root";
import { Camera } from "./camera";
import { DebugFlagsType } from "./react/debug_flag_buttons";
import { CollisionHandler } from "./collision_handler";
import { Rect } from "./geometry/rect";
// import { CoroutineManager } from "./coroutine_manager";
import { BaseGameState } from "./base_state";
export let GameReference: BaseGame<any>;
export type GameArgs = {
scale: number;
canvasWidth: number;
canvasHeight: number;
tileHeight: number;
tileWidth: number;
backgroundColor: number;
debugFlags: DebugFlagsType;
state: Omit<IGameState, keyof BaseGameState>;
assets: TypesafeLoader<any>;
};
export const StageName = "Stage";
export const FixedStageName = "FixedStage";
export const ParallaxStageName = "ParallaxStage";
export class BaseGame<TResources extends AllResourcesType = {}> {
app: PIXI.Application;
state: IGameState;
* The root of the display hierarchy for the game. Everything that exists in
* the game that isn't fixed as the camera moves should be under this.
*/
stage: Entity;
parallaxStage: Entity;
/**
* A stage for things in the game that don't move when the camera move and are
* instead fixed to the screen. For example, the HUD.
*/
fixedCameraStage: Entity;
private assets: TypesafeLoader<TResources>;
renderer: Renderer;
camera: Camera;
collisionHandler: CollisionHandler;
// coroutineManager: CoroutineManager;
constructor(props: GameArgs) {
GameReference = this;
// this.coroutineManager = new CoroutineManager(this);
this.state = {
...props.state,
if (!view) {
}
this.collisionHandler = new CollisionHandler({
canvasWidth: props.canvasWidth / props.scale,
canvasHeight: props.canvasHeight / props.scale,
tileHeight: props.tileHeight,
tileWidth: props.tileWidth,
});
this.app = new Application({
width: props.canvasWidth,
height: props.canvasHeight,
powerPreference: "low-power",
antialias: false,
transparent: false,
resolution: window.devicePixelRatio,
autoDensity: true,
view: view as HTMLCanvasElement,
});
this.app.stage.scale = new Point(props.scale, props.scale);
this.parallaxStage = new Entity({ name: ParallaxStageName });
this.stage = new Entity({ name: StageName });
this.fixedCameraStage = new Entity({ name: FixedStageName });
this.state.stage = this.stage;
this.app.stage.addChild(this.parallaxStage.sprite);
this.app.stage.addChild(this.stage.sprite);
this.app.stage.addChild(this.fixedCameraStage.sprite);
this.state.renderer = this.app.renderer;
this.state.stage = this.stage;
this.assets = props.assets;
this.assets.onLoadComplete(() => this.startGameLoop());
this.assets.onLoadComplete(() => this.initialize());
this.renderer = this.app.renderer;
this.camera = new Camera({
stage: this.stage,
state: this.state,
canvasWidth: props.canvasWidth,
canvasHeight: props.canvasHeight,
scale: props.scale,
bounds: new Rect({ x: -5000, y: -5000, width: 10000, height: 10000 }),
});
this.state.camera = this.camera;
this.stage.sprite.sortableChildren = true;
this.fixedCameraStage.sprite.sortableChildren = true;
}
/**
* Called after resources are finished loading.
*/
startGameLoop = () => {
this.app.ticker.add(() => this.gameLoop());
};
gameLoop() {
Debug.Clear();
const { entities } = this.state;
if (!this.state.lastCollisionGrid) {
const grid = this.collisionHandler.buildCollisionGrid({
bounds: new Rect({ x: 0, y: 0, width: 5000, height: 5000 }),
entities: this.state.entities,
});
this.state.lastCollisionGrid = grid;
}
this.state.tick++;
this.state.keys.update();
for (const entity of entities.values()) {
entity.baseUpdate(this.state);
}
for (const entity of this.state.toBeDestroyed) {
if (entity.sprite.parent) {
entity.sprite.parent.removeChild(entity.sprite);
}
// this.coroutineManager.stopCoroutinesOwnedBy(entity);
}
this.state.toBeDestroyed = [];
const grid = this.collisionHandler.buildCollisionGrid({
bounds: this.camera.getBounds(),
entities: activeEntities,
});
this.state.lastCollisionGrid = grid;
this.collisionHandler.resolveCollisions({
entities: activeEntities,
grid: grid,
});
this.camera.update(this.state);
// this.coroutineManager.updateCoroutines(this.state);
// let foo = Debug.GetDrawnObjects();
// for (const f of Debug.GetDrawnObjects()) {
// if (f instanceof AugmentedSprite) {
// }
// }
// }
// let foo = Debug.GetDrawn();
Debug.ResetDrawCount();
}
}
// if (f.width > 1024) {
// f.visible = false;
const activeEntities = new HashSet(
this.state.entities
.values()
.filter((e) => e.activeModes.includes(this.state.mode))
);
this.state.entities = new HashSet(
entities.values().filter((ent) => !this.state.toBeDestroyed.includes(ent))
);
initialize() {}
// ReactMountGame(this, props.debugFlags);
backgroundColor: props.backgroundColor,
// throw new Error("I couldn't find an element named #canvas on initialization. Giving up!")
const view = document.getElementById("canvas");
};
...new BaseGameState(),
/**
import { IGameState } from "Library";
import { Renderer } from "pixi.js";
import { KeyboardState } from "./keyboard";
import { Entity } from "./entity";
import { HashSet } from "./data_structures/hash";
import { IGameState } from "Library";
import { Mode } from "Library";
import { CollisionGrid } from "./collision_grid";
import { Camera } from "./camera";
export class BaseGameState implements Partial<IGameState> {
keys: KeyboardState;
entities = new HashSet<Entity>();
toBeDestroyed: Entity[] = [];
spriteToEntity: { [key: number]: Entity } = {};
mode: Mode = "Normal";
lastCollisionGrid!: CollisionGrid;
constructor() {
this.keys = new KeyboardState();
}
}
stage!: Entity;
renderer!: Renderer;
camera!: Camera;
// 1. Encode font into dataurl
// 2. Use dataurl in SVG (otherwise you wouldnt be able to refer to the font in the SVG).
// 3. Load the SVG into an image
// 4. Render the image to a canvas
// 5. Use the canvas as a texture for a Sprite
// 6. Waste 30 minutes trying to debug your code only to realize it was because
// there was a missing ' in font_data_url
export const PIXEL_RATIO = (() => {
const ctx = document.createElement("canvas").getContext("2d")!,
return dpr / bsr;
})();
export class BaseTextEntity<T extends BaseGameState> extends Entity {
protected _html: string;
constructor(html: string, width: number, height: number) {
super({
texture: Texture.WHITE,
});
this.sprite.height = height;
this.buildTextGraphic();
}
set html(value: string) {
if (this._html !== value) {
this._html = value;
}
}
// converting woff into dataurl:
// https://gist.github.com/viljamis/c4016ff88745a0846b94
// https://stackoverflow.com/questions/12652769/rendering-html-elements-to-canvas
const wrappedHtml = `
</div>
`;
<foreignObject width="100%" height="100%">
<defs>
<style type="text/css">
@font-face {
font-family: FreePixel;
}
</style>
</defs>
</foreignObject>
</svg>`;
const img = new Image();
img.onload = () => {
ctx.clearRect(0, 0, this.width, this.height);
ctx.drawImage(img, x, y);
resolve();
};
img.src = data;
});
}
private htmlToXML(html: string): string {
doc.write(html);
// the HTML document to a string as opposed to appending it to a
// <foreignObject> in the DOM
// Get well-formed markup
return html;
}
}
const can = document.createElement("canvas");
can.style.height = h + "px";
can.getContext("2d")!.setTransform(ratio, 0, 0, ratio, 0, 0);
return can;
}
protected async buildTextGraphic() {
this.sprite.texture = Texture.from(this.canvas);
this.sprite.texture.update();
}
clear() {
this.context.clearRect(0, 0, this.width, this.height);
this.sprite.texture = Texture.from(this.canvas);
this.sprite.texture.update();
}
}
await this.renderHTMLToCanvas(
this._html,
this.context,
0,
0,
this.width,
this.height
);
can.width = w * ratio;
can.height = h * ratio;
can.style.width = w + "px";
private createHiDPICanvas(
w: number,
h: number,
ratio: number | undefined = undefined
) {
if (ratio === undefined) {
ratio = PIXEL_RATIO;
html = new XMLSerializer().serializeToString(doc.body);
doc.documentElement.setAttribute(
"xmlns",
doc.documentElement.namespaceURI!
);
// You must manually set the xmlns if you intend to immediately serialize
const doc = document.implementation.createHTMLDocument("");
await new Promise((resolve) => {
${this.htmlToXML(wrappedHtml)}
src: ${FontDataUrl}
const data = `data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
<div style="width: ${this.width}">
${html}
private async renderHTMLToCanvas(
html: string,
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number
) {
// reference used for this insanity:
update() {}
this.buildTextGraphic();
this._html = html;
this.canvas = this.createHiDPICanvas(this.width, this.height);
this.context = this.canvas.getContext("2d")!;
this.sprite.width = width;
name: "BaseTextEntity",
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
dpr = window.devicePixelRatio || 1,
bsr =
(ctx as any).webkitBackingStorePixelRatio ||
(ctx as any).mozBackingStorePixelRatio ||
(ctx as any).msBackingStorePixelRatio ||
(ctx as any).oBackingStorePixelRatio ||
(ctx as any).backingStorePixelRatio ||
1;
import { Texture } from "pixi.js";
import { FontDataUrl } from "./font_data_url";
import { Entity } from "./entity";
import { BaseGameState } from "./base_state";
import { Vector2, IVector2 } from "./geometry/vector2";
import { Entity } from "./entity";
import { Rect } from "./geometry/rect";
import { Debug } from "./debug";
import { IGameState } from "Library";
export class Camera {
private static LERP_SPEED_X = 0.03;
private static LERP_SPEED_Y = 0.4;
/**
* Top left coordinate of the camera.
*/
private _position = Vector2.Zero;
private _desiredPosition = Vector2.Zero;
private _stage: Entity;
private _canvasWidth: number;
private _canvasHeight: number;
private _currentBounds: Rect;
constructor(props: {
stage: Entity;
state: IGameState;
canvasWidth: number;
canvasHeight: number;
scale: number;
bounds: Rect;
}) {
this._stage = props.stage;
this._canvasWidth = props.canvasWidth / props.scale;
this._canvasHeight = props.canvasHeight / props.scale;
this._currentBounds = props.bounds;
this._desiredPosition = this._position;
}
public get center(): Vector2 {
return new Vector2({
x: this._position.x + this._canvasWidth / 2,
});
}
public setBounds(newBounds: Rect) {
this._currentBounds = newBounds;
}
public getBounds(): Rect {
return this._currentBounds;
}
public cameraFrame(): Rect {
return new Rect({
x: this.center.x - this._canvasWidth / 2,
y: this.center.y - this._canvasHeight / 2,
width: this._canvasWidth,
height: this._canvasHeight,
});
}
private halfDimensions(): Vector2 {
return new Vector2({
x: this._canvasWidth / 2,
});
}
private _immediatelyCenterOn = (position: IVector2) => {
this._position = new Vector2(position).subtract(this.halfDimensions());
};
centerOn = (position: IVector2, immediate = false) => {
if (immediate) {
this._immediatelyCenterOn(position);
} else {
}
};
calculateDesiredPosition = (): Vector2 => {
let desiredPosition = this._desiredPosition;
const currentBounds = this._currentBounds;
if (!currentBounds) {
console.error("no region for camera!");
return desiredPosition;
}
}
// fit the camera rect into the regions rect
if (desiredPosition.x < currentBounds.left) {
desiredPosition = desiredPosition.withX(currentBounds.left);
}
if (desiredPosition.x + this.cameraFrame().width > currentBounds.right) {
}
if (desiredPosition.y < currentBounds.top) {
desiredPosition = desiredPosition.withY(currentBounds.top);
}
if (desiredPosition.y + this.cameraFrame().height > currentBounds.bottom) {
}
return desiredPosition;
};
update = (state: IGameState) => {
if (Debug.DebugMode) {
return;
}
const desiredPosition = this.calculateDesiredPosition();
this._position = new Vector2(
Math.floor(this._position.x / 4) * 4,
Math.floor(this._position.y / 4) * 4
);
this._stage.x = Math.floor(-this._position.x);
this._stage.y = Math.floor(-this._position.y);
};
}
this._position = this._position.lerp2D(
desiredPosition,
Camera.LERP_SPEED_X,
Camera.LERP_SPEED_Y
);
desiredPosition = desiredPosition.withY(
currentBounds.bottom - this._canvasHeight
);
desiredPosition = desiredPosition.withX(
currentBounds.right - this._canvasWidth
);
if (
currentBounds.width < this._canvasWidth ||
currentBounds.height < this._canvasHeight
) {
throw new Error(
`There is a region on the map which is too small for the camera at x: ${currentBounds.x} y: ${currentBounds.y}.`
);
this._desiredPosition = new Vector2(position).subtract(
this.halfDimensions()
);
y: this._canvasHeight / 2,
y: this._position.y + this._canvasHeight / 2,
this._immediatelyCenterOn(
new Vector2({
x: this._canvasWidth / 2,
y: this._canvasHeight / 2,
})
);
import { Graphics } from "pixi.js";
import { Rect } from "./geometry/rect";
import { Entity } from "./entity";
import { Vector2 } from "./geometry/vector2";
import { DefaultGrid } from "./data_structures/default_grid";
import { RectGroup } from "./geometry/rect_group";
import { Debug } from "./debug";
export type CollisionResultRect = {
firstRect: Rect;
secondRect: Rect;
otherEntity?: Entity;
thisEntity?: Entity;
overlap: Rect;
};
type CollisionResultPoint = {
firstRect: Rect;
secondRect: Rect;
firstEntity?: Entity;
secondEntity?: Entity;
overlap: Vector2;
};
export class CollisionGrid {
private _position: Vector2 = Vector2.Zero;
private _width: number;
private _height: number;
private _cellSize: number;
private _numCellsPerRow: number;
private _numCellsPerCol: number;
private _cells: DefaultGrid<Cell>;
private _renderLines: Graphics | null = null;
constructor(props: { width: number; height: number; cellSize: number }) {
const { width, height, cellSize } = props;
this._width = width;
this._height = height;
this._cellSize = cellSize;
this._numCellsPerRow = Math.ceil(width / cellSize);
this._numCellsPerCol = Math.ceil(height / cellSize);
this._cells = new DefaultGrid<Cell>(
(x, y) =>
new Cell(new Vector2({ x: x * cellSize, y: y * cellSize }), cellSize)
);
}
debug() {
for (let x = 0; x < 10; x++) {
for (let y = 0; y < 10; y++) {
// Draw cell
Debug.DrawRect(
new Rect({
x: x * this._cellSize,
y: y * this._cellSize,
width: this._cellSize,
height: this._cellSize,
}),
0xff0000,
true,
"fixed"
);
for (const obj of this._cells.get(x, y).colliders) {
Debug.DrawRect(obj.rect, 0xff0000, true, "fixed");
}
}
}
}
public get topLeft() {
return this._position;
}
public get center() {
return this._position.add({ x: this._width / 2, y: this._height / 2 });
}
/**
* Checks if the provided rect would collide with anything on the grid. If an
* entity is passed in, ignores that entity when checking for collisions.
* (Does not add the rect to the grid.)
*/
getRectCollisions = (
rect: Rect,
skipEntity?: Entity
): CollisionResultRect[] => {
const cells: Cell[] = [];
const lowX = Math.floor(rect.x / this._cellSize);
const highX = Math.ceil((rect.x + rect.width) / this._cellSize);
const lowY = Math.floor(rect.y / this._cellSize);
const highY = Math.ceil((rect.y + rect.height) / this._cellSize);
for (let x = lowX; x < highX; x++) {
for (let y = lowY; y < highY; y++) {
cells.push(this._cells.get(x, y));
}
}
const collisions: CollisionResultRect[] = [];
for (const cell of cells) {
for (const { rect: rectInCell, entity: entityInCell } of cell.colliders) {
if (entityInCell === skipEntity) {
continue;
}
const overlap = rect.getIntersection(rectInCell);
if (overlap) {
collisions.push({
firstRect: rectInCell,
otherEntity: entityInCell,
secondRect: rect,
thisEntity: skipEntity,
overlap,
});
}
}
}
return collisions;
};
getRectGroupCollisions = (
group: RectGroup,
entity?: Entity
): CollisionResultRect[] => {
let collisions: CollisionResultRect[] = [];
for (const rect of group.getRects()) {
collisions = [...collisions, ...this.getRectCollisions(rect, entity)];
}
return collisions;
};
/**
* Same as collidesRect but immediately returns true if there's a collision.
*/
collidesRectFast = (rect: Rect, entity?: Entity): boolean => {
const corners = rect.getCorners();
const cells = corners.map((corner) =>
this._cells.get(
Math.floor(corner.x / this._cellSize),
Math.floor(corner.y / this._cellSize)
)
);
const uniqueCells: { [key: string]: Cell } = {};
for (const cell of cells) {
uniqueCells[cell.hash()] = cell;
}
const values = Object.values(uniqueCells);
for (const cell of values) {
for (const { rect: rectInCell, entity: entityInCell } of cell.colliders) {
if (entityInCell === entity) {
continue;
}
const overlap = rect.intersects(rectInCell);
if (overlap) {
return true;
}
}
}
return false;
};
collidesPoint = (
point: Vector2,
takeFirst = false
): CollisionResultPoint[] => {
const cell = this._cells.get(
Math.floor(point.x / this._cellSize),
Math.floor(point.y / this._cellSize)
);
const collisions: CollisionResultPoint[] = [];
for (const { rect, entity: entityInCell } of cell.colliders) {
const overlap = rect.contains(point);
if (overlap) {
collisions.push({
firstRect: rect,
firstEntity: entityInCell,
secondRect: rect,
secondEntity: undefined,
overlap: point,
});
if (takeFirst) {
return collisions;
}
}
}
return collisions;
};
/**
* Get all collisions on the grid.
*/
getAllCollisions = (): CollisionResultRect[] => {
const result: CollisionResultRect[] = [];
for (let cell of this.cells) {
const cellRects = cell.colliders;
for (let i = 0; i < cellRects.length; i++) {
for (let j = i; j < cellRects.length; j++) {
if (i === j) continue;
const collider1 = cellRects[i];
const collider2 = cellRects[j];
const intersection = collider1.rect.getIntersection(
collider2.rect,
false
);
if (intersection !== undefined) {
result.push({
firstRect: collider1.rect,
secondRect: collider2.rect,
otherEntity: collider1.entity,
thisEntity: collider2.entity,
overlap: intersection,
});
}
}
}
}
return result;
};
public get cells(): Cell[] {
return this._cells.values();
}
clear = () => {
for (const cell of this._cells.values()) {
cell.removeAll();
}
};
// Add a rect to the hash grid.
// Checks each corner, to handle entities that span multiply grid cells.
add = (rect: Rect, associatedEntity?: Entity) => {
const startX = Math.floor(rect.x / this._cellSize);
const stopX = Math.floor(rect.right / this._cellSize);
const startY = Math.floor(rect.y / this._cellSize);
const stopY = Math.floor(rect.bottom / this._cellSize);
for (let x = startX; x <= stopX; x++) {
for (let y = startY; y <= stopY; y++) {
this._cells.get(x, y).add(rect, associatedEntity);
}
}
};
addRectGroup = (group: RectGroup, associatedEntity?: Entity) => {
for (const rect of group.getRects()) {
this.add(rect, associatedEntity);
}
};
}
type CellItem = {
rect: Rect;
entity?: Entity;
};
export class Cell {
private _bounds: Rect;
private _rects: CellItem[] = [];
constructor(topLeft: Vector2, cellSize: number) {
this._bounds = Rect.FromPoint(topLeft, cellSize);
}
public get colliders(): CellItem[] {
return this._rects;
}
add = (rect: Rect, entity?: Entity) => {
this._rects.push({ rect, entity });
};
removeAll = () => {
this._rects = [];
};
hash(): string {
return this._bounds.toString();
}
}
import { Entity } from "./entity";
import { Vector2 } from "./geometry/vector2";
import { CollisionGrid, CollisionResultRect } from "./collision_grid";
import { HashSet } from "./data_structures/hash";
import { Rect } from "./geometry/rect";
import { RectGroup } from "./geometry/rect_group";
export type HitInfo = {
hit: boolean;
left?: boolean;
right?: boolean;
up?: boolean;
down?: boolean;
collisions: CollisionResultRect[];
interactions: CollisionResultRect[];
};
export class CollisionHandler {
private _canvasWidth: number;
private _canvasHeight: number;
private _tileSize: number;
constructor(props: {
canvasWidth: number;
canvasHeight: number;
tileWidth: number;
tileHeight: number;
}) {
if (props.tileWidth !== props.tileHeight) {
}
this._canvasWidth = props.canvasWidth;
this._canvasHeight = props.canvasHeight;
this._tileSize = props.tileWidth;
}
buildCollisionGrid = (props: {
entities: HashSet<Entity>;
bounds: Rect;
}): CollisionGrid => {
const { entities, bounds } = props;
const grid = new CollisionGrid({
width: 2 * this._canvasWidth,
height: 2 * this._canvasHeight,
cellSize: 4 * this._tileSize,
});
for (const entity of collideableEntities) {
if (collisionRect.intersects(bounds)) {
const rectOrRectGroup = collisionRect;
if (rectOrRectGroup instanceof Rect) {
grid.add(rectOrRectGroup, entity);
} else {
grid.addRectGroup(rectOrRectGroup, entity);
}
}
}
return grid;
};
const xHits =
bounds instanceof Rect
? grid.getRectCollisions(bounds, entity)
: grid.getRectGroupCollisions(bounds, entity);
return {
hits,
interactions,
};
resolveCollisions = (props: {
entities: HashSet<Entity>;
grid: CollisionGrid;
}) => {
const { entities, grid } = props;
for (const entity of entities.values()) {
const hitInfo: HitInfo = {
hit: false,
collisions: [],
interactions: [],
};
const xVelocity = new Vector2({ x: entity.velocity.x, y: 0 });
const yVelocity = new Vector2({ x: 0, y: entity.velocity.y });
let delta = Vector2.Zero;
// resolve x-axis
delta = delta.add(xVelocity);
updatedBounds = updatedBounds.add(xVelocity);
if (xHits.length > 0) {
hitInfo.hit = true;
hitInfo.right = entity.velocity.x > 0;
hitInfo.left = entity.velocity.x < 0;
hitInfo.collisions = [...hitInfo.collisions, ...xHits];
delta = delta.subtract(xVelocity);
updatedBounds = updatedBounds.subtract(xVelocity);
for (let x = 0; x < xVelocity.x; x++) {
updatedBounds = updatedBounds.add(new Vector2(1, 0));
delta = delta.add(new Vector2(1, 0));
if (newXHits.length > 0) {
updatedBounds = updatedBounds.add(new Vector2(-1, 0));
delta = delta.add(new Vector2(-1, 0));
break;
}
}
}
if (xInteractions.length > 0) {
hitInfo.interactions = [...hitInfo.interactions, ...xInteractions];
}
// resolve y-axis
delta = delta.add(yVelocity);
updatedBounds = updatedBounds.add(yVelocity);
if (yHits.length > 0) {
hitInfo.hit = true;
hitInfo.up = entity.velocity.y < 0;
hitInfo.down = entity.velocity.y > 0;
hitInfo.collisions = [...hitInfo.collisions, ...yHits];
delta = delta.subtract(yVelocity);
updatedBounds = updatedBounds.subtract(yVelocity);
for (let y = 0; y < yVelocity.y; y++) {
updatedBounds = updatedBounds.add(new Vector2(0, 1));
delta = delta.add(new Vector2(0, 1));
if (newYHits.length > 0) {
updatedBounds = updatedBounds.add(new Vector2(0, -1));
delta = delta.add(new Vector2(0, -1));
break;
}
}
}
if (yInteractions.length > 0) {
hitInfo.interactions = [...hitInfo.interactions, ...yInteractions];
}
entity.hitInfo = hitInfo;
hitInfo.hit = hitInfo.collisions.length > 0;
entity.x = entity.x + delta.x;
entity.y = entity.y + delta.y;
}
};
}
const { hits: newYHits } = this.getHitsAt(
grid,
updatedBounds,
entity
);
const { hits: yHits, interactions: yInteractions } = this.getHitsAt(
grid,
updatedBounds,
entity
);
const { hits: newXHits } = this.getHitsAt(
grid,
updatedBounds,
entity
);
const { hits: xHits, interactions: xInteractions } = this.getHitsAt(
grid,
updatedBounds,
entity
);
let updatedBounds = entity
.collisionBounds()
.add(entity.positionAbsolute());
if (entity.velocity.x === 0 && entity.velocity.y === 0) {
continue;
}
};
const hits = xHits.filter(
(x) =>
!x.otherEntity || (x.otherEntity && !x.otherEntity.isInteractable())
);
const interactions = xHits.filter(
(x) => x.otherEntity && x.otherEntity.isInteractable()
);
getHitsAt = (
grid: CollisionGrid,
bounds: Rect | RectGroup,
entity: Entity
): { hits: CollisionResultRect[]; interactions: CollisionResultRect[] } => {
const collisionRect = entity
.collisionBounds()
.add(entity.positionAbsolute());
const collideableEntities = entities
.values()
.filter((x) => x.isCollideable() || x.isInteractable());
throw new Error(
"Collision handler does not currently support tileWidth != tileHeight"
);
import { KeyInfoType } from "./keyboard";
// import { IGameState } from "Library";
// import { Entity } from "./entity";
// import { Game } from "../game/game";
// import { BaseGame } from "./base_game";
// import { IS_DEBUG } from "./environment";
// /**
// * const state: GameState = yield CoroutineResult;
// */
// export type GameCoroutine = Generator<CoroutineResult, void, IGameState>
// export type CoroutineResult = "next" | { frames: number } | { untilKeyPress: keyof KeyInfoType };
// type ActiveCoroutine = {
// fn : GameCoroutine;
// | { waiting: false }
// | { waiting: true; type: "frames" ; frames: number }
// | { waiting: true; type: "untilKey"; untilKey: keyof KeyInfoType }
// name : string;
// owner : Entity | Game;
// };
// export type CoroutineId = number;
// export class CoroutineManager {
// private _lastCoroutineId: CoroutineId = -1;
// private _activeCoroutines: { [key: number]: ActiveCoroutine } = [];
// private _game: BaseGame<any>;
// constructor(game: BaseGame<any>) {
// this._game = game;
// }
// startCoroutine(name: string, co: GameCoroutine, owner: Entity | Game): CoroutineId {
// for (const activeCo of Object.values(this._activeCoroutines)) {
// if (activeCo.name === name) {
// if (IS_DEBUG) {
// throw new Error(`Two coroutines with the name ${ name }. Tell grant about this!!!`);
// } else {
// return 0;
// }
// }
// }
// this._activeCoroutines[++this._lastCoroutineId] = {
// fn : co,
// status : { waiting: false },
// name : name,
// owner : owner,
// };
// return this._lastCoroutineId;
// }
// public stopCoroutine(id: CoroutineId): void {
// delete this._activeCoroutines[id];
// }
// public updateCoroutines(state: IGameState): void {
// for (const key of Object.keys(this._activeCoroutines)) {
// const co = this._activeCoroutines[Number(key)];
// if (co.status.waiting) {
// if (co.status.type === "frames") {
// if (co.status.frames-- < 0) {
// co.status = { waiting: false };
// } else {
// continue;
// }
// } else if (co.status.type === "untilKey") {
// if (state.keys.justDown[co.status.untilKey]) {
// co.status = { waiting: false };
// } else {
// continue;
// }
// }
// }
// const { value, done } = co.fn.next(state);
// if (done) {
// this.stopCoroutine(Number(key));
// continue;
// }
// if (value === "next") {
// continue;
// if (typeof value === "object") {
// if ("frames" in value) {
// co.status = { waiting: true, type: 'frames', frames: value.frames };
// continue;
// } else if ("untilKeyPress" in value) {
// co.status = { waiting: true, type: 'untilKey', untilKey: value.untilKeyPress };
// continue;
// }
// }
// }
// }
// stopCoroutinesOwnedBy(entity: Entity) {
// const ids = Object.keys(this._activeCoroutines).map(k => Number(k));
// for (const id of ids) {
// if (this._activeCoroutines[id].owner === entity) {
// this.stopCoroutine(id);
// }
// }
// }
// }
//
//
//
//
// }
//
//
//
//
//
//
//
//
//
//
//
//
//
//
// status :
//
//
//
export class Pair<T extends { hash(): string }, U extends { hash(): string }> {
private _first: T;
private _second: U;
constructor(first: T, second: U) {
this._second = second;
}
hash(): string {
}
get first() {
return this._first;
}
get second() {
return this._second;
}
}
return `${this._first.hash()}|${this._second.hash()}`;
this._first = first;
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[] {
}
}
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()];
}
}
}
return Object.keys(this._values).map((key) => this._values[key]);
// 2D array that allows for negative indices
export class Grid<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;
}
for (const x of Object.keys(this._data)) {
const inner = this._data[Number(x)];
for (const y of Object.keys(inner)) {
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;
}
}
}
result.push({
x: Number(x),
keys(): { x: number; y: number }[] {
const result: { x: number; y: number }[] = [];
private _data: { [key: number]: { [key: number]: T } } = {};
// 2D array that allows for negative indices
export class DefaultGrid<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;
}
for (const x of Object.keys(this._data)) {
const inner = this._data[Number(x)];
for (const y of Object.keys(inner)) {
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];
}
}
result.push({
x: Number(x),
keys(): { x: number; y: number }[] {
const result: { x: number; y: number }[] = [];
private _data: { [key: number]: { [key: number]: T } } = {};
import { Vector2, IVector2 } from "./geometry/vector2";
import { Graphics, Sprite, Container } from "pixi.js";
import { Line } from "./geometry/line";
import { Entity } from "./entity";
import { Rect } from "./geometry/rect";
import { RectGroup } from "./geometry/rect_group";
import { GameReference } from "./base_game";
import { BaseGameState } from "./base_state";
import { IS_DEBUG, IS_PRODUCTION } from "./environment";
const MAX_DEBUGGING_GRAPHICS_COUNT = 500;
export class Debug {
public static stageReference: Entity;
public static DebugMode = false;
public static DebugGraphicStack: Graphics[] = [];
public static Clear(): void {
for (const debug of Debug.DebugGraphicStack) {
debug.parent.removeChild(debug);
debug.destroy();
}
Debug.DebugGraphicStack = [];
}
* Draw a point on the canvas.
* We expect this function to be called every tick in an update() function.
* If that's not what you want, pass persistent = true.
*/
if (IS_PRODUCTION) {
}
const graphics = new Graphics();
new Line({
x1: point.x - 40,
x2: point.x + 40,
y1: point.y - 40,
y2: point.y + 40,
}).drawOnto(graphics, color);
new Line({
x1: point.x + 40,
x2: point.x - 40,
y1: point.y - 40,
y2: point.y + 40,
}).drawOnto(graphics, color);
GameReference.stage.sprite.addChild(graphics);
if (!persistent) {
this.DebugGraphicStack.push(graphics);
if (this.DebugGraphicStack.length > MAX_DEBUGGING_GRAPHICS_COUNT) {
const toBeRemoved = this.DebugGraphicStack.shift()!;
toBeRemoved.parent.removeChild(toBeRemoved);
toBeRemoved.destroy();
}
}
return graphics;
}
* Draw a line from start to end on the canvas, for debugging.
* We expect this function to be called every tick in an update() function.
* Debug graphics drawn in the previous tick are removed in the game loop.
* If that's not what you want, pass persistent = true.
*/
if (IS_PRODUCTION) {
}
return Debug.DrawLine(new Line({ start, end }), color, persistent);
}
* Draw a line on the canvas, for debugging.
* We expect this function to be called every tick in an update() function.
* Debug graphics drawn in the previous tick are removed in the game loop.
* If that's not what you want, pass persistent = true.
*/
if (IS_PRODUCTION) {
}
const graphics = new Graphics();
line.drawOnto(graphics, color);
if (target === "fixed") {
GameReference.fixedCameraStage.sprite.addChild(graphics);
} else {
GameReference.stage.sprite.addChild(graphics);
}
if (!persistent) {
this.DebugGraphicStack.push(graphics);
if (this.DebugGraphicStack.length > MAX_DEBUGGING_GRAPHICS_COUNT) {
const toBeRemoved = this.DebugGraphicStack.shift()!;
toBeRemoved.parent.removeChild(toBeRemoved);
toBeRemoved.destroy();
}
}
return graphics;
}
* Draw a rectangle from start to end on the canvas, for debugging.
* We expect this function to be called every tick in an update() function.
* Debug graphics drawn in the previous tick are removed in the game loop.
* If that's not what you want, pass persistent = true.
*/
if (IS_PRODUCTION) {
}
const lines: Graphics[] = [];
for (const line of rect.getLinesFromRect()) {
lines.push(Debug.DrawLine(line, color, persistent, target));
}
return lines;
}
* Draw the bounds of a game object on the canvas, for debugging.
* We expect this function to be called every tick in an update() function.
* Debug graphics drawn in the previous tick are removed in the game loop.
* If that's not what you want, pass persistent = true.
*/
public static DrawBounds(
persistent = false,
target: "stage" | "fixed" = "stage"
): Graphics[] {
if (IS_PRODUCTION) {
}
if (entity instanceof Entity) {
if (entity instanceof RectGroup) {
const results: Graphics[] = [];
for (const rect of entity.getRects()) {
const lines = Debug.DrawRect(rect, color, persistent, target);
for (const line of lines) {
results.push(line);
}
}
return results;
} else {
}
}
private static profiles: { [key: string]: number[] } = {};
/**
* Performance test a block of code.
*/
public static Profile(name: string, cb: () => void): void {
Debug.profiles[name] = Debug.profiles[name] || [];
const start = window.performance.now();
const end = window.performance.now();
Debug.profiles[name].push(end - start);
if (Debug.profiles[name].length === 60) {
const average = Debug.profiles[name].reduce((a, b) => a + b) / 60;
const rounded = Math.floor(average * 100) / 100;
Debug.profiles[name] = [];
}
}
static ResetDrawCount() {
(Sprite as any).drawCount = 0;
(Container as any).drawCount = 0;
drawn = [];
}
static GetDrawnObjects() {
return drawn;
}
static GetDrawCount() {
}
public static DebugStuff(state: BaseGameState) {
if (state.keys.justDown.Z) {
Debug.DebugMode = true;
state.stage.x = 0;
state.stage.y = 0;
if (state.stage.scale.x === 0.2) {
state.stage.scale = new Vector2({ x: 1, y: 1 });
} else {
state.stage.scale = new Vector2({ x: 0.2, y: 0.2 });
}
}
if (Debug.DebugMode) {
if (state.keys.down.W) {
state.stage.y += 20;
}
if (state.keys.down.S) {
state.stage.y -= 20;
}
if (state.keys.down.D) {
state.stage.x -= 20;
}
if (state.keys.down.A) {
state.stage.x += 20;
}
}
}
public static DebugShowRect(state: BaseGameState, rect: Rect) {
state.stage.scale = new Vector2({ x: 0.2, y: 0.2 });
state.stage.x = -rect.x * 0.2;
state.stage.y = -rect.y * 0.2;
}
}
let drawn: any[] = [];
if (IS_DEBUG) {
(Sprite as any).drawCount = 0;
(Sprite.prototype as any).__render = (Sprite.prototype as any)._render;
(Sprite.prototype as any)._render = function (renderer: any) {
(Sprite as any).drawCount++;
this.__render(renderer);
drawn.push(this);
};
(Sprite.prototype as any).__renderCanvas = (Sprite.prototype as any)._renderCanvas;
(Sprite.prototype as any)._renderCanvas = function (renderer: any) {
(Sprite as any).drawCount++;
this.__renderCanvas(renderer);
drawn.push(this);
};
// PIXI.Container
(Container as any).drawCount = 0;
(Container.prototype as any).__render = (Container.prototype as any)._render;
(Container.prototype as any)._render = function (renderer: any) {
(Container as any).drawCount++;
this.__render(renderer);
drawn.push(this);
};
(Container.prototype as any).__renderCanvas = (Container.prototype as any)._renderCanvas;
(Container.prototype as any)._renderCanvas = function (renderer: any) {
(Container as any).drawCount++;
this.__renderCanvas(renderer);
drawn.push(this);
};
}
return (Sprite as any).drawCount + (Container as any).drawCount;
console.log(`${name}: ${rounded}ms`);
cb();
return Debug.DrawRect(
new Rect({
x: entity.x,
y: entity.y,
width: entity.width,
height: entity.height,
}),
color,
persistent,
target
);
entity = entity.collisionBounds().add(entity.positionAbsolute());
}
console.error("SHOULD NOT HAPPEN");
entity: Entity | Sprite | Graphics | RectGroup | Container | Rect,
color = 0xff0000,
*
*
/**
console.error("SHOULD NOT HAPPEN");
public static DrawRect(
rect: Rect,
color = 0xff0000,
persistent = false,
target: "stage" | "fixed" = "fixed"
): Graphics[] {
*
*
/**
console.error("SHOULD NOT HAPPEN");
public static DrawLine(
line: Line,
color = 0xff0000,
persistent = false,
target: "stage" | "fixed" = "fixed"
): Graphics {
*
*
/**
console.error("SHOULD NOT HAPPEN");
public static DrawLineV2(
start: Vector2,
end: Vector2,
color = 0xff0000,
persistent = false
): Graphics {
*
*
/**
console.error("SHOULD NOT HAPPEN");
public static DrawPoint(
point: IVector2,
color = 0xff0000,
persistent = false
): Graphics {
* Debug graphics drawn in the previous tick are removed in the game loop.
*
/**
import { Vector2, IVector2 } from "./geometry/vector2";
import { Rect } from "./geometry/rect";
import { Sprite, Texture, MaskData, Container } from "pixi.js";
import { getUniqueID } from "./util";
import { RectGroup } from "./geometry/rect_group";
import { BaseGameState } from "./base_state";
// import { CoroutineId, GameCoroutine } from "./coroutine_manager";
import { IGameState, Mode } from "Library";
import { HitInfo } from "./collision_handler";
import { serialized } from "./serializer";
export enum EntityType {
NormalEntity,
* The collision information for this entity will be calculated by the main
* game loop.
*/
MovingEntity,
}
export class AugmentedSprite extends Sprite {
entity!: Entity;
}
// export class ModeEntity extends Entity<GameState> {
// shouldUpdate(state: GameState) {
// return this.activeModes.includes(state.mode);
// }
// }
// TODO: probably make less of these methods abstract?
export class Entity {
/**
* This is the name that is displayed in the hierarchy.
*/
public name: string;
public activeModes: Mode[] = ["Normal"];
public id = getUniqueID();
public velocity = Vector2.Zero;
/**
* The PIXI Sprite that this Entity wraps.
*/
public sprite: AugmentedSprite;
public hitInfo: HitInfo = { hit: false, collisions: [], interactions: [] };
protected _collidable: boolean;
protected _interactable: boolean;
constructor(props: {
name: string;
collidable?: boolean;
texture?: Texture;
interactable?: boolean;
}) {
this.sprite = new AugmentedSprite(props.texture);
this.name = props.name;
this.sprite.entity = this;
this._collidable = props.collidable ?? false;
this._interactable = props.interactable ?? false;
if (props.interactable && props.collidable) {
throw new Error("Cant be both interactable and collideable");
}
this.startUpdating();
this.sprite.sortableChildren = true;
this.sprite.anchor.set(0);
}
addChild(child: Entity, x: number | null = null, y: number | null = null) {
this.sprite.addChild(child.sprite);
if (x !== null) child.x = x;
if (y !== null) child.y = y;
}
removeChild(child: Entity) {
this.sprite.removeChild(child.sprite);
}
// startCoroutine(name: string, coroutine: GameCoroutine): CoroutineId {
// return GameReference.coroutineManager.startCoroutine(name, coroutine, this);
// }
// stopCoroutine(id: CoroutineId): void {
// GameReference.coroutineManager.stopCoroutine(id);
// }
startUpdating() {
GameReference.state.entities.put(this);
}
stopUpdating() {
GameReference.state.entities.remove(this);
}
shouldUpdate(state: IGameState): boolean {
return this.activeModes.includes(state.mode);
}
setCollideable(isCollideable: boolean) {
this._collidable = isCollideable;
}
setTexture(newTexture: Texture) {
this.sprite.texture = newTexture;
}
* Used for collision detection. (x, y) is relative to the sprite, btw, not
* the map or anything else.
*/
public collisionBounds(): Rect | RectGroup {
return new Rect({
x: 0,
y: 0,
width: this.width,
}
* Returns the position of this Entity relative to the stage (rather than its
* parent, like position would).
*/
public positionAbsolute(): Vector2 {
return this.position;
}
return this.position.add(this.parent?.positionAbsolute() ?? new Vector2());
}
public get center(): Vector2 {
return new Vector2(this.position).add({
x: this.width / 2,
});
}
children(): Entity[] {
const children = this.sprite.children;
const result: Entity[] = [];
for (const child of children) {
if (child instanceof AugmentedSprite) {
result.push(child.entity);
}
}
return result;
}
destroy(state: BaseGameState) {
state.toBeDestroyed.push(this);
}
hash(): string {
return `[Entity ${this.id}]`;
}
isCollideable(): boolean {
return this._collidable;
}
isInteractable(): boolean {
return this._interactable;
}
dimensions(): Vector2 {
const bounds = this.collisionBounds();
if (bounds instanceof Rect) {
return new Vector2(bounds.x, bounds.y);
} else {
throw new Error("oh no grant doesnt handle this case!!!");
}
}
// Sprite wrapper stuff
public get parent(): Entity | null {
const parent = this.sprite.parent;
if (parent instanceof AugmentedSprite) {
const entityParent = parent.entity;
if (entityParent) {
return entityParent;
}
}
return null;
}
private queuedUpdates: ((state: IGameState) => void)[] = [];
private firstUpdateCalled = false;
baseUpdate(state: IGameState): void {
if (this.shouldUpdate(state)) {
for (const cb of this.queuedUpdates) {
cb(state);
}
if (!this.firstUpdateCalled) {
this.firstUpdateCalled = true;
this.firstUpdate(state);
}
this.update(state);
}
this.queuedUpdates = [];
}
addOnClick(listener: (state: IGameState) => void) {
this.sprite.interactive = true;
this.queuedUpdates.push(listener);
});
}
addOnMouseOver(listener: (state: IGameState) => void) {
this.sprite.interactive = true;
this.queuedUpdates.push(listener);
});
}
addOnMouseOut(listener: (state: IGameState) => void) {
this.sprite.interactive = true;
this.queuedUpdates.push(listener);
});
}
public set scale(value: Vector2) {
this.sprite.scale.x = value.x;
this.sprite.scale.y = value.y;
}
public distance(other: IVector2) {
return this.position.distance(other);
}
}
public get scale(): Vector2 {
return new Vector2({ x: this.sprite.scale.x, y: this.sprite.scale.y });
}
public set mask(value: Container | MaskData | null) {
this.sprite.mask = value;
}
public get mask(): Container | MaskData | null {
return this.sprite.mask;
}
public set texture(value: Texture) {
this.sprite.texture = value;
}
public get visible(): boolean {
return this.sprite.visible;
}
public set visible(value: boolean) {
this.sprite.visible = value;
}
public get zIndex(): number {
return this.sprite.zIndex;
}
public set zIndex(value: number) {
this.sprite.zIndex = value;
this.sprite.parent && this.sprite.parent.sortChildren();
}
public get position(): Vector2 {
return new Vector2({ x: this.x, y: this.y });
}
public set position(value: Vector2) {
this.x = value.x;
this.y = value.y;
}
public get alpha(): number {
return this.sprite.alpha;
}
public set alpha(value: number) {
this.sprite.alpha = value;
}
public get height(): number {
return this.sprite.height;
}
public set height(value: number) {
this.sprite.height = value;
}
public get width(): number {
return this.sprite.width;
}
public set width(value: number) {
this.sprite.width = value;
}
public get y(): number {
return this.sprite.y;
}
public set y(value: number) {
this.sprite.y = value;
}
public get x(): number {
return this.sprite.x;
}
public set x(value: number) {
this.sprite.x = value;
}
this.sprite.on("mouseout", () => {
this.sprite.on("mouseover", () => {
this.sprite.on("click", () => {
y: this.height / 2,
if (
this.parent &&
(this.parent.name === FixedStageName ||
this.parent.name === StageName ||
this.parent.name === ParallaxStageName)
) {
/**
height: this.height,
});
/**
firstUpdate(state: IGameState): void {}
update(state: IGameState): void {}
/**
import {
GameReference,
FixedStageName,
StageName,
ParallaxStageName,
} from "./base_game";
export const IS_DEVELOPMENT = !IS_PRODUCTION;
export const IS_DEBUG =
(IS_DEVELOPMENT || window.location.href.includes("debug=true")) &&
!window.location.href.includes("debug=false");
export const IS_PRODUCTION =
!window.location.href.includes("localhost") ||
window.location.href.includes("debug=false");
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) => {
export const epsLessThan = (x: number, y: number) => {
return x - EPSILON - y < 0;
};
return x + EPSILON - y > 0;
};
};
export const FontDataUrl =
"url('data:application/font-woff;charset=utf-8;base64,AAEAAAANAIAAAwBQRFNJRwAAAAEAAPXIAAAACEdERUYC2gAEAAD10AAAACBPUy8yQal3RwAAANwAAABgY21hcAvh29UAAPhIAAAFKGdhc3AAAAADAAD1wAAAAAhnbHlm8tOLKgAAFkQAAM8saGVhZP6TCFsAAPAoAAAANmhoZWELBQIBAAAWAAAAACRobXR4yeTAbwAA8GAAAAVgbG9jYQEk4ygAAOVwAAAKuG1heHAC0gCxAAAWJAAAACBuYW1leSf5mQAA9fAAAAJYcG9zdD0HyCoAAAE8AAAUwwADA/8BkAAFAAAFAAVHAAABAAUABUcAAAIAAIACgAAAAAAFCQAAAAAABAAAAAEAAAAAAAAAAAAAAAAgICAgAEAAIPsCBgD+AAEABwACAGAAAZ/f1wAAA4AFAQAAACAAAQACAAAAAAAA/zgAZAABAAAAAAAAAAAAAAAAAAAAAAAAAq0AAAJyAnMAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATABQAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAAxADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0ATgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF8AYABhAQIArACjAIQAhQC9AJYA6ACGAI4AiwCdAKkApAEDAIoA2gCDAJMA8gDzAI0AlwCIAQQA3gDxAJ4AqgD1APQA9gCiAK0AyQDHAK4AYgBjAJAAZADLAGUAyADKAM8AzADNAM4A6QBmANMA0ADRAK8AZwDwAJEA1gDUANUAaADrAO0AiQBqAGkAawBtAGwAbgCgAG8AcQBwAHIAcwB1AHQAdgB3AOoAeAB6AHkAewB9AHwAuAChAH8AfgCAAIEA7ADuALoBBQEGAQcBCAEJAQoA/QD+AP8BAAELAQwBDQEBAQ4BDwEQAREBEgETARQBFQD4APkBFgEXARgBGQEaARsA+gDXARwBHQEeAR8BIAEhASIBIwDiAOMBJAElASYBJwEoASkBKgErASwBLQCwALEBLgEvATABMQEyATMBNAE1APsA/ADkAOUBNgE3ATgBOQE6ATsBPAE9AT4BPwFAAUEAuwFCAUMBRAFFAOYA5wCmAUYBRwFIAUkBSgFLAUwBTQFOAU8BUAFRANgA4QDbANwA3QDgAVIA3wFTAVQBVQFWAVcBWAFZAVoBWwFcAV0BXgFfAWABYQFiAWMBZAFlAWYBZwFoAWkBagFrAWwBbQFuAW8BcAFxAXIBcwF0AXUBdgF3AXgBeQF6AXsBfAF9AX4BfwGAAYEBggGDAYQBhQGGAYcBiAGJAYoBiwGMAY0BjgGPAJsBkAGRAZIBkwGUAZUBlgGXAZgBmQGaAZsBnAGdAZ4BnwGgAaEBogGjAaQBpQGmAacBqAGpAaoBqwGsAa0BrgGvAbABsQGyAbMBtAG1AbYBtwG4AbkBugG7AbwBvQG+Ab8BwAHBAcIBwwHEAcUBxgHHAcgByQHKAcsBzAHNAc4BzwHQAdEB0gHTAdQB1QHWAdcB2AHZAdoB2wHcAd0B3gHfAeAB4QHiAeMB5AHlAeYB5wHoAekB6gHrAewB7QHuAe8B8AHxAfIB8wH0AfUB9gH3AfgB+QH6AfsCdAJ1AnYCdwJ4AnkCegJ7AnwCfQJ+An8CgAKBAoIAsgCzAfwB/QC2ALcAxAC0ALUAxQCCAMIAhwCrAMYB/gH/AL4AvwIAAgECAgIDAgQCBQIGAIwCBwIIAgkCCgILAgwCDQCYAKgAmgCZALwAwwClAJICDgIPAhACEQCcAKcAjwISAJQAlQITAhQCFQIWAhcCGAIZAhoCGwIcAh0CHgIfAiACIQIiAiMCJAIlAiYCJwIoAikCKgIrAiwCLQIuAi8CMAIxAjICMwI0AjUCNgI3AjgCOQI6AjsCPAI9Aj4CPwJAAkECQgJDAkQCRQJGAkcCSAJJAkoCSwJMAk0CTgJPAlACUQJSAlMCVAJVAlYCVwJYAlkCWgJbAlwCXQJeAl8CYAC5AmECYgJjAmQCZQJmAmcCaAJpAmoCawJsAm0CbgJvAnACcQKDAoQChQKGAocCiAKJAooCiwKMAo0CjgKPApACkQKSApMClAKVApYClwKYApkCmgKbApwCnQKeAp8CoAKhAqICowKkAqUCpgKnAqgCqQKqAqsCrAKtAq4CrwKwArECsgKzArQCtQpjb250cm9sREVMCXNmdGh5cGhlbgZtaWRkb3QHQW1hY3JvbgdhbWFjcm9uBkFicmV2ZQZhYnJldmUHQW9nb25lawdhb2dvbmVrBkRjYXJvbgZkY2Fyb24GRGNyb2F0B0VtYWNyb24HZW1hY3JvbgRFZG90BGVkb3QHRW9nb25lawdlb2dvbmVrBkVjYXJvbgZlY2Fyb24IR2NlZGlsbGEIZ2NlZGlsbGEHSW1hY3JvbgdpbWFjcm9uB0lvZ29uZWsHaW9nb25lawhLY2VkaWxsYQhrY2VkaWxsYQZMYWN1dGUGbGFjdXRlCExjZWRpbGxhCGxjZWRpbGxhBkxjYXJvbgZsY2Fyb24GTmFjdXRlBm5hY3V0ZQhOY2VkaWxsYQhuY2VkaWxsYQZOY2Fyb24GbmNhcm9uB09tYWNyb24Hb21hY3JvbglPZGJsYWN1dGUJb2RibGFjdXRlBlJhY3V0ZQZyYWN1dGUIUmNlZGlsbGEIcmNlZGlsbGEGUmNhcm9uBnJjYXJvbgZTYWN1dGUGc2FjdXRlCFRjZWRpbGxhCHRjZWRpbGxhBlRjYXJvbgZ0Y2Fyb24HVW1hY3Jvbgd1bWFjcm9uBVVyaW5nBXVyaW5nCVVkYmxhY3V0ZQl1ZGJsYWN1dGUHVW9nb25lawd1b2dvbmVrBlphY3V0ZQZ6YWN1dGUEWmRvdAR6ZG90BU9ob3JuBW9ob3JuBVVob3JuBXVob3JuBmFjYXJvbgZpY2Fyb24Gb2Nhcm9uBnVjYXJvbg91ZGllcmVzaXNtYWNyb24OdWRpZXJlc2lzYWN1dGUOdWRpZXJlc2lzY2Fyb24OdWRpZXJlc2lzZ3JhdmUFdGlsZGUIZ3JhdmVjbWIIYWN1dGVjbWIIdGlsZGVjbWINaG9va2Fib3ZlY29tYgtkb3RiZWxvd2NtYgV0b25vcw5kaWFseXRpa2F0b25vcwpBbHBoYXRvbm9zDEVwc2lsb250b25vcwhFdGF0b25vcwlJb3RhdG9ub3MMT21pY3JvbnRvbm9zDFVwc2lsb250b25vcwpPbWVnYXRvbm9zEWlvdGFkaWVyZXNpc3Rvbm9zBUFscGhhBEJldGEFR2FtbWEKRGVsdGFncmVlawdFcHNpbG9uBFpldGEDRXRhBVRoZXRhBElvdGEFS2FwcGEGTGFtYmRhAk11Ak51AlhpB09taWNyb24CUGkDUmhvBVNpZ21hA1RhdQdVcHNpbG9uA1BoaQNDaGkDUHNpCk9tZWdhZ3JlZWsMSW90YWRpZXJlc2lzD1Vwc2lsb25kaWVyZXNpcwphbHBoYXRvbm9zDGVwc2lsb250b25vcwhldGF0b25vcwlpb3RhdG9ub3MUdXBzaWxvbmRpZXJlc2lzdG9ub3MFYWxwaGEEYmV0YQVnYW1tYQVkZWx0YQdlcHNpbG9uBHpldGEDZXRhBXRoZXRhBGlvdGEFa2FwcGEGbGFtYmRhB211Z3JlZWsCbnUCeGkHb21pY3JvbgNyaG8Gc2lnbWExBXNpZ21hA3RhdQd1cHNpbG9uA3BoaQNjaGkDcHNpBW9tZWdhDGlvdGFkaWVyZXNpcw91cHNpbG9uZGllcmVzaXMMb21pY3JvbnRvbm9zDHVwc2lsb250b25vcwpvbWVnYXRvbm9zCWFmaWkxMDAyMwlhZmlpMTAwNTEJYWZpaTEwMDUyCWFmaWkxMDA1MwlhZmlpMTAwNTQJYWZpaTEwMDU1CWFmaWkxMDA1NglhZmlpMTAwNTcJYWZpaTEwMDU4CWFmaWkxMDA1OQlhZmlpMTAwNjAJYWZpaTEwMDYxCWFmaWkxMDA2MglhZmlpMTAxNDUJQWN5cmlsbGljCWFmaWkxMDAxOAlhZmlpMTAwMTkJYWZpaTEwMDIwCWFmaWkxMDAyMQlhZmlpMTAwMjIJYWZpaTEwMDI0CWFmaWkxMDAyNQlhZmlpMTAwMjYJYWZpaTEwMDI3CWFmaWkxMDAyOAlhZmlpMTAwMjkJYWZpaTEwMDMwCWFmaWkxMDAzMQlhZmlpMTAwMzIJYWZpaTEwMDMzCWFmaWkxMDAzNAlhZmlpMTAwMzUJYWZpaTEwMDM2CWFmaWkxMDAzNwlhZmlpMTAwMzgJYWZpaTEwMDM5CWFmaWkxMDA0MAlhZmlpMTAwNDEJYWZpaTEwMDQyCWFmaWkxMDA0MwlhZmlpMTAwNDQJYWZpaTEwMDQ1CWFmaWkxMDA0NglhZmlpMTAwNDcJYWZpaTEwMDQ4CWFmaWkxMDA0OQlhY3lyaWxsaWMJYWZpaTEwMDY2CWFmaWkxMDA2NwlhZmlpMTAwNjgJYWZpaTEwMDY5CWFmaWkxMDA3MAlhZmlpMTAwNzIJYWZpaTEwMDczCWFmaWkxMDA3NAlhZmlpMTAwNzUJYWZpaTEwMDc2CWFmaWkxMDA3NwlhZmlpMTAwNzgJYWZpaTEwMDc5CWFmaWkxMDA4MAlhZmlpMTAwODEJYWZpaTEwMDgyCWFmaWkxMDA4MwlhZmlpMTAwODQJYWZpaTEwMDg1CWFmaWkxMDA4NglhZmlpMTAwODcJYWZpaTEwMDg4CWFmaWkxMDA4OQlhZmlpMTAwOTAJYWZpaTEwMDkxCWFmaWkxMDA5MglhZmlpMTAwOTMJYWZpaTEwMDk0CWFmaWkxMDA5NQlhZmlpMTAwOTYJYWZpaTEwMDk3CWFmaWkxMDA3MQlhZmlpMTAwOTkJYWZpaTEwMTAwCWFmaWkxMDEwMQlhZmlpMTAxMDIJYWZpaTEwMTAzCWFmaWkxMDEwNAlhZmlpMTAxMDUJYWZpaTEwMTA2CWFmaWkxMDEwNwlhZmlpMTAxMDgJYWZpaTEwMTA5CWFmaWkxMDExMAlhZmlpMTAxOTMJYWZpaTEwMDUwCWFmaWkxMDA5OAlhZmlpMDAyMDgKZGJsbG93bGluZQZtaW51dGUGc2Vjb25kCWV4Y2xhbWRibAhmcmFjdGlvbgluc3VwZXJpb3IGcGVzZXRhBGRvbmcERXVybwlhZmlpNjEzNTIJYXJyb3dsZWZ0B2Fycm93dXAKYXJyb3dyaWdodAlhcnJvd2Rvd24JYXJyb3dib3RoCWFycm93dXBkbgxhcnJvd3VwZG5ic2UKb3J0aG9nb25hbAVhbmdsZQxpbnRlcnNlY3Rpb24FdW5pb24LZXF1aXZhbGVuY2USbG9naWNhbG5vdHJldmVyc2VkC2ludGVncmFsdG9wDmludGVncmFsYm90dG9tCFNGMTAwMDAwB3VuaTI1MDEIU0YxMTAwMDAHdW5pMjUwMwhTRjAxMDAwMAd1bmkyNTBGCFNGMDMwMDAwB3VuaTI1MTMIU0YwMjAwMDAHdW5pMjUxNwhTRjA0MDAwMAd1bmkyNTFCCFNGMDgwMDAwB3VuaTI1MUQHdW5pMjUyMAd1bmkyNTIzCFNGMDkwMDAwB3VuaTI1MjUHdW5pMjUyOAd1bmkyNTJCCFNGMDYwMDAwB3VuaTI1MkYHdW5pMjUzMAd1bmkyNTMzCFNGMDcwMDAwB3VuaTI1MzcHdW5pMjUzOAd1bmkyNTNCCFNGMDUwMDAwB3VuaTI1M0YHdW5pMjU0Mgd1bmkyNTRCCFNGNDMwMDAwCFNGMjQwMDAwCFNGNTEwMDAwCFNGNTIwMDAwCFNGMzkwMDAwCFNGMjIwMDAwCFNGMjEwMDAwCFNGMjUwMDAwCFNGNTAwMDAwCFNGNDkwMDAwCFNGMzgwMDAwCFNGMjgwMDAwCFNGMjcwMDAwCFNGMjYwMDAwCFNGMzYwMDAwCFNGMzcwMDAwCFNGNDIwMDAwCFNGMTkwMDAwCFNGMjAwMDAwCFNGMjMwMDAwCFNGNDcwMDAwCFNGNDgwMDAwCFNGNDEwMDAwCFNGNDUwMDAwCFNGNDYwMDAwCFNGNDAwMDAwCFNGNTQwMDAwCFNGNTMwMDAwCFNGNDQwMDAwB3VwYmxvY2sHZG5ibG9jawVibG9jawdsZmJsb2NrB3J0YmxvY2sHbHRzaGFkZQVzaGFkZQdka3NoYWRlC2JsYWNrc3F1YXJlDmJsYWNrcmVjdGFuZ2xlF2JsYWNrdXBwb2ludGluZ3RyaWFuZ2xlGWJsYWNrcmlnaHRwb2ludGluZ3BvaW50ZXIZYmxhY2tkb3ducG9pbnRpbmd0cmlhbmdsZRhibGFja2xlZnRwb2ludGluZ3BvaW50ZXIGY2lyY2xlDWJ1bGxldGludmVyc2UJaW52Y2lyY2xlCXNtaWxlZmFjZRBibGFja3NtaWxpbmdmYWNlB2NvbXBhc3MGZmVtYWxlBG1hbGUFc3BhZGUEY2x1YgVoZWFydAdkaWFtb25kC211c2ljYWxub3RlEGVpZ2h0aG5vdGViZWFtZWQHdW5pRjhGRgJmaQJmbAZnbHlwaDEGZ2x5cGgyCGdseXBoNDcxCGdseXBoNDcyCGdseXBoNDczCGdseXBoNDc0CGdseXBoNDc1CGdseXBoNDc2CGdseXBoNDc3CGdseXBoNDc4CGdseXBoNDc5CGdseXBoNDgwCGdseXBoNDgxCGdseXBoNDgyCGdseXBoNDgzCGdseXBoNDg0CGdseXBoNDg1CGdseXBoNjM0CGdseXBoNjM1CGdseXBoNjM2CGdseXBoNjM3CGdseXBoNjM4CGdseXBoNjM5CGdseXBoNjQwCGdseXBoNjQxCGdseXBoNjQyCGdseXBoNjQzCGdseXBoNjQ0CGdseXBoNjQ1CGdseXBoNjQ2CGdseXBoNjQ3CGdseXBoNjQ4CGdseXBoNjQ5CGdseXBoNjUwCGdseXBoNjUxCGdseXBoNjUyCGdseXBoNjUzCGdseXBoNjU0CGdseXBoNjU1CGdseXBoNjU2CGdseXBoNjU3CGdseXBoNjU4CGds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AAEBgP4CBAEHAAAHAAABMxEhFSERIwGAgQIA/gCBBwD8AH37fwAAAAACAX/+AQP/BwAAAwAHAAABIREhAzMDIwIBAf7+AoGBAYEDgv6ABP73AQAAAAEBAP4ABAAHAAAHAAABIREhFSERIQEAAYABgP6A/oAHAPwBgfuAAAABAQD+AQQABwAABwAAASERIREhESEBAAGAAYD+gf6ABwD8gv6A+/8AAQAB/gICAQcAAAcAAAEzESMRITUhAYCBgf6BAX8HAPcCBIF9AAAAAAEAAP4AAgEHAAAHAAABMxEjESERIQGAgYH+gAGABwD3AAQCAYAAAAABAAH+AAKABwAABwAAASERIREjNTMBAAGA/oD//wcA9wAEgIEAAAAAAQAB/gACgAcAAAcAAAEhESERIxEzAQABgP6A//8HAPcABAIBgAAAAAEAAP4CBAADAAAHAAARIRUhESMRIQQA/gCB/oEDAH37fwSBAAEAAP3+A/8DgAAHAAARIREhESMRIQP//gKB/oADgP6A+/4EAgAAAAABAAD9/wP/AwAABwAAESEVIREhESMD//6A/oD/AwCB+4AEgAABAAD9/gP/A4AABwAAESERIREhESMD//6A/oD/A4D+gPv+BAIAAAAAAQAAAoMEAAcAAAcAAAEzESEVITUhAX+BAgD8AAF/BwD8AH19AAAAAAEAAAIAA/8G/gAHAAABMxEhESERIQGAgQH+/AEBgAb+/IL+gAGAAAABAAACfwP/Bv8ABwAAEyERIRUhNTP/AYABgPwB/wb//AGBgQABAAACAAP/Bv4ABwAAEyERIREhETP/AYABgPwB/wb+/IL+gAGAAAAAAQAA/gIEAAcAAAsAAAEzESEVIREjESE1IQF/gQIA/gCB/oEBfwcA/AB9+38EgX0AAQAA/f4D/wb+AAsAAAEzESERIREjESERIQGAgQH+/gKB/oABgAb+/IL+gPv+BAIBgAAAAAEAAP3/A/8G/wALAAATIREhFSERIREjNTP/AYABgP6A/oD//wb//AGB+4AEgIEAAAEAAP3+A/8G/gALAAATIREhESERIREjETP/AYABgP6A/oD//wb+/IL+gPv+BAIBgAAAAAACAAACAQQAA4AAAwAHAAARIRUhESEVIQQA/AAEAPwAAoKBAX+BAAAAAgEA/gIChAcAAAMABwAAATMRIwEzESMCA4GB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AACC6AAAgzgAAIN4AACDxAAAg+gAAIQwAACEZAAAhPAAAIVIAACFlAAAhewAAIZkAACGqAAAhvgAAIcoAACHkAAAh7wAAIf4AACILAAAiFQAAIi0AACJBAAAiXgAAImsAACJ4AAAihQAAIpYAACKmAAAiuAAAIsYAACLaAAAi7QAAIwYAACMZAAAjLAAAIz8AACNIAAAjWgAAI24AACONAAAjowAAI7YAACPMAAAj4wAAI/IAACQGAAAkEgAAJCEAACQsAAAkPAAAJEkAACRTAAAkZQAAJHkAACSTAAAkoAAAJK0AACS8AAAkzQAAJN0AACTvAAAk/QAAJQwAACUfAAAlMwAAJUkAACVeAAAlbwAAJX4AACWOAAAlnAAAJa0AACW7AAAl0QAAJeQAACX3AAAmFgAAJi0AACY6AAAmRQAAJlAAACZjAAAmigAAJqkAACa1AAAmxwAAJuEAACb3AAAnEQAAJykAACc4AAAnRwAAJ2AAACdwAAAndwAAJ5MAACeaAAAnoQAAJ6gAACexAAAnvAAAJ8YAACfQAAAn4gAAJ/MAACgDAAAoDgAAKB4AACgpAAAoNQAAKGYAAChvAAAofAAAKI0AACicAAAorAAAKMEAACjNAAAo6QAAKQIAACkkAAApQgAAKVcAAClnAAApdgAAKYgAACmZAAApswAAKcwAACnoAAAqAQAAKhcAAColAAAqOAAAKkYAACpNAAAqZAAAKngAACqBAAAqkgAAKqMAACq0AAAqwAAAKtsAACrrAAAq+AAAKw8AACslAAArLgAAKzgAACtBAAArSAAAK08AACtWAAArXQAAK2YAACtwAAAreQAAK4IAACuLAAArlQAAK54AACunAAArsQAAK7wAACvGAAAr0AAAK9oAACvkAAAr7gAAK/gAACwBAAAsCwAALBQAACweAAAsKAAALDIAACw7AAAsRQAALFEAACxeAAAsagAALHcAACyBAAAsiwAALJYAACyiAAAssAAALLwAACzIAAAs1wAALOIAACztAAAs+wAALQYAAC0RAAAtHwAALSsAAC04AAAtSgAALVYAAC1jAAAtdAAALYEAAC2NAAAtnwAALawAAC24AAAtyQAALdoAAC3rAAAuBAAALgsAAC4SAAAuGQAALiAAAC4nAAAuYQAALs4AAC88AAAvQwAAL0oAAC9XAAAvZgAAL3MAAC+CAAAvmAAAL7IAAC/EAAAv2gAAMAEAADAgAAAwOAAAME0AADBlAAAweAAAMJAAADCiAAAwtQAAMMUAADDfAAAw8QAAMQoAADEcAAAxHAAAMRwAADEcAAAxOgAAMVEAADFRAAAxUQAAMWQAADGIAAAxiAAAMYgAADGIAAAxiAAAMYgAADGIAAAxqgAAMcAAADHLAAAxywAAMdgAADHfAAAx6AAAMe8AADH6AAAyBgAAMgYAADIiAAAyIgAAMiIAADIsAAAyNQAAMlwAADJ7AAAyjQAAMqAAADK4AAAyywAAMtcAADLpAAAzAwAAMyoAADNJAAAzWwAAM3MAADOGAAAzlwAAM7MAADPEAAAzxAAAM8QAADPLAABAAAAAQAAENs2+18PPPUCCwgAAAAAAMy6EogAAAAAzLmr1v///f4EAwcBAAAACAAAAAEAAAAABAAAAAAAAAAEAAAAAAACAAB/AH8AfgCAAH4BfwGBAQAA/wCAAQEAgAGAAH4AfgD/AIAAgACAAIAAgACAAIAAgAGAAQEBAAB/AQAAfwCAAIAAgACAAIAAgACAAIAAgAEAAH8AgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEAAIABAACAAAIBfgB/AIAAgACAAIAAgACAAIABAQCAAIAA/wCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAGAAQAAgAEAAAACAACCAIAAgACAAgAAgAB/AIAA/wCAAIIAgACAAQABAAD/AP8BAAGAAIAAgAF/AQABAAD/AIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEAAQABAAEAAIAAgACAAIAAgACAAIAA/wCAAIAAgACAAIAAgACAAIAAfwB/AH8AfwB/AH8AgACAAIAAgACAAIAA/wEBAP8A/wCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAfwCAAH8AgAB/AIAAgACAAIAAgP//AH0AgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEBAQEBAQEBAQEBAQCAAIAAgAD/AIAA/wCAAP8AfwCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAAAAAAH8BAQCAAIAAgACAAIAAgAEBAQEA/wF/AQABfwB/AIABAQGAAIABfwGBAX8AgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAQEAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIABAQCAAIAAgACAAQEAgACAAIAAgACAAIAAgACAAIABAQCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAQEAgACAAIAAgACAAIAAgACAAIABAQEBAH8AgACAAIAAgACAAIAAgACAAIAAgAB/AIAAfwCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACBAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACBAIAAgACAAIAAgACAAIAAfgCAAH8AgACAAIIAgQCBAIIAgACAAIAAgACAAIAAgACAAIABAAEAAIAAgACAAIAAgAB+AIAAgACAAH8AfwB/AQAAfwB+AH8AAAB/AQEAgQEBAQAAgAEBAIAAAAAAAAABfwF/AQEAgACAAIABAQEBAIABAQB/AX8BAQGAAYABAAB/AQEAgACAAIAAAAAAAIABAQCAAQEAAQEBAQEAgACAAIAAgAABAX8AAQCAAIAAgACAAIABAQCAAIABAAEBAQAAggH/AIAAAAAAAYABAAGAAP8AAAABAYABAAAAAAEBgAF/AQABAAABAAAAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAGAAQABAAAAAAAAAAF/AP0A/QAAAAAAAAF/AP0A/QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgD//wAA//8AgACAAIABAQCAAIEBAQB+AH8AfwB/AH8AfwEBAIAAgACAAH8AfwCAAAAAgACAAIAAAAAAAAAAgACAAAAAAACAAIAAAAAAAAAAAAAAAAAAgACAAP8AAACAAX0AAAF9AQABAAAAAIAAAAAAAX8AAQCAAIAAfwCAAIAAgAEBAIAAfwCAAIAAfwCAAIAAgAAAAIAAAAAAAgAAAQAB//8AAgAAAAEAAAAAAAEAAAAOAAAAGAAAAAAAAgABAAECrAABAAQAAAACAAAAAAAOAK4AAQAAAAAAAAA5AAAAAQAAAAAAAQAKADkAAQAAAAAAAgAHAEMAAQAAAAAAAwAUAEoAAQAAAAAABAASAF4AAQAAAAAABQANAHAAAQAAAAAABgARAH0AAwABBAkAAAByAI4AAwABBAkAAQAUAQAAAwABBAkAAgAOARQAAwABBAkAAwAoASIAAwABBAkABAAkAUoAAwABBAkABQAaAW4AAwABBAkABgAiAYipMjAxMiB3d3cuY3I4c29mdHdhcmUubmV0ICBERU1PIC0gbm90IGZvciBjb21tZXJjaWFsIHVzZS5GcmVlIFBpeGVsUmVndWxhckZyZWUgUGl4ZWwgLSBSZWd1bGFyRnJlZSBQaXhlbCBSZWd1bGFyVmVyc2lvbiAxLjAwMEZyZWVQaXhlbC1SZWd1bGFyAKkAMgAwADEAMgAgAHcAdwB3AC4AYwByADgAcwBvAGYAdAB3AGEAcgBlAC4AbgBlAHQAIAAgAEQARQBNAE8AIAAtACAAbgBvAHQAIABmAG8AcgAgAGMAbwBtAG0AZQByAGMAaQBhAGwAIAB1AHMAZQAuAEYAcgBlAGUAIABQAGkAeABlAGwAUgBlAGcAdQBsAGEAcgBGAHIAZQBlACAAUABpAHgAZQBsACAALQAgAFIAZQBnAHUAbABhAHIARgByAGUAZQAgAFAAaQB4AGUAbAAgAFIAZQBnAHUAbABhAHIAVgBlAHIAcwBpAG8AbgAgADEALgAwADAAMABGAHIAZQBlAFAAaQB4AGUAbAAtAFIAZQBnAHUAbABhAHIAAAACAAEAAAAAABYAAwABAAABHAAAAAABBgAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYQCHiIqMlJmfpKOlp6aoqqyrra6wr7GytLa1t7m4vby+vwBzZWZqAHmicWwAd2sAiZsAdAAAaHgAAAAAAG19AKm7gmRvAAAAAG5+AGODhpj3+AAAAAAAALoAwgAAZwAAAAAAAAAAAIWNhI6LkJGSj5aXAJWdnpziAAByAAAAewAAAAAEBAwAAADmAIAABgBmAH8BBwETARsBHwEjASsBMQE3AT4BSAFNAVsBZQFrAXMBfgGSAaEBsAHOAdAB0gHUAdYB2AHaAdwCxwLdAwEDAwMJAyMDhgOKA4wDoQPOBAwETwRcBF8EkSAVIBogHiAiICYgMCAzIDogPCBEIH8gpyCsIRYhIiGVIagiAiIGIg8iESIVIhoiICIrIkgiYSJlIxAjISUDJQwlECUUJRglHSUgJSUlKCUsJTAlNCU4JTwlPyVCJUslbCWAJYQliCWMJZMloCWsJbIluiW8JcQlyyXZJjwmQCZCJmAmYyZmJmv4//sC//8AAAAgAKABDAEWAR4BIgEqAS4BNgE5AUEBTAFQAV4BagFuAXgBkgGgAa8BzgHQAdIB1AHWAdgB2gHcAsYC2AMAAwMDCQMjA4QDiAOMA44DowQBBA4EUQReBJAgEyAXIBwgICAmIDAgMiA5IDwgRCB/IKcgqyEWISIhkCGoIgIiBiIPIhEiFSIZIh4iKSJIImAiZCMQIyAlACUMJQ8lEyUXJRslICUjJSglKyUvJTMlNyU7JT8lQiVLJVAlgCWEJYgljCWQJaAlrCWyJbolvCXEJcol2CY6JkAmQiZgJmMmZSZq+P/7Af///+P/w/+//73/u/+5/7P/sf+t/6z/qv+n/6X/o/+f/53/mQAA/3n/bAAAAAAAAAAAAAAAAAAAAAD+X/5P/i0AAAAAAAD9rv2tAAD9q/2q/Xj9d/12/XX9ReHT4dLh0eHQAAAAAOHD4b4AAAAAAAAAAOFSAAAAAOBxAAAAAAAAAAAAAAAA3/Tf8d/pAADftt+0AADe+90dAADdE90R3Q/dDQAA3QkAAN0F3QPdAdz/3P0AAAAAAADc7QAAAAAAAAAA3M4AAAAAAAAAAAAAAADcntyS3DIAAAAAAAAAANwO3AsAAAd3AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMQAAAAAAMAAwADAAMAAwADAAMAAwAAAAAAAAAC6ALoAugAAAAAAtgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoACgAAAAAACcAJwAnACcAAAAmgCaAAAAmACYAJgAmACYAJgAAAAAAAAAkgAAAAAAjgAAAAAAigAAAAAAAAAAAIIAAACAAAAAAAAAAAAAAAB2AHYAdgAAAHQAdAB0AHQAAAByAHIAcgByAHIAcgAAAAAAAABsAGwAbABsAAAAAABoAAAAAAEYAR0BHgEfASABIQEiASMBJAEvATABMQE4AfMB9AH5AfoB+wH8Af8CAAIHAggCCQIKAgsCDAIVAhoCIQIrAi8COgI7AjwCWgJbAlwCXQJiAmMCZAJlAmYCZwJvAnACcQJyAnc=')";
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;
constructor();
constructor(x: number, 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);
}
return new Vector2({
x: this.x + p.x,
y: this.y + p.y,
});
}
return new Vector2({
x: this.x - p.x,
y: this.y - p.y,
});
}
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,
});
}
clampY(low: number, high: number): Vector2 {
let newY = this.y;
return new Vector2({
x: this.x,
y: newY,
});
}
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({
});
}
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,
});
}
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 === 0) return this;
if (t === 1) return other;
}
lerp2D(other: Vector2, tx: number, ty: number): Vector2 {
}
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 });
}
}
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 })
);
return this.scale({ x: 0, y: 0 }, { x: 1 - t, y: 1 - t }).add(
other.scale({ x: 0, y: 0 }, { x: t, y: t })
);
if (t > 1 || t < 0) {
console.error("Lerp t must be between 0 and 1.");
}
y: this.y / length,
};
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,
scale(
about: { x: number; y: number },
amount: { x: number; y: number }
): Vector2 {
if (newY < low) {
newY = low;
}
if (newY > high) {
newY = high;
}
add(p: { x: number; y: number }): Vector2 {
subtract(p: { x: number; y: number }): Vector2 {
translate(p: { x: number; y: number }): Vector2 {
constructor(props: { x: number; y: number });
constructor(
propsOrX: { x: number; y: number } | number = { x: 0, y: 0 },
y?: number
) {
public get x(): number {
return this._x;
}
public get y(): number {
return 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 {
return new RectGroup(newRects);
}
subtract(delta: Vector2): RectGroup {
return new RectGroup(newRects);
}
}
const newRects = this._rects.map((rect) => rect.subtract(delta));
const newRects = this._rects.map((rect) => rect.add(delta));
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",
};
}
/**
* 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}]`;
}
}
hash(): string {
return this.toString();
}
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 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 = "";
let x1, x2, y1, y2;
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;
}
public get length(): number {
return Math.sqrt(
(this.x2 - this.x1) * (this.x2 - this.x1) +
);
}
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 {
return null;
}
static DeserializeLine(s: string): Line {
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 overlap
getOverlap(other: Line): Line | undefined {
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),
});
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 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 [
return [
}
}
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 {
reviver: "Line",
};
}
toString(): string {
}
equals(other: Line | null) {
return (
);
}
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") ||
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 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 (
) {
return lineIntersection;
}
return null;
}
normalize(): Line {
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 {
start: this.start.add(x),
end: this.end.add(x),
}
}
});
return new Line({
}),
});
const mag = Math.sqrt((this.x1 - this.x2) ** 2 + (this.y1 - this.y2) ** 2);
epsGreaterThan(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))
epsGreaterThan(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 other
// within us
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));
/**
!obj.hasOwnProperty("y2")
) {
(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)
if (other === null) {
return false;
}
return `Line: [(${this.x1},${this.y1}) -> (${this.x2},${this.y2})]`;
x1: this.x1,
x2: this.x2,
y1: this.y1,
y2: this.y2,
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);
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);
} /* if (orientedByY) */ else {
const summedLength = new Line(this).length + new Line(other).length;
if (!orientedByX && !orientedByY) {
return undefined;
}
const orientedByY =
this.y1 === this.y2 && this.y1 === other.y1 && this.y1 === other.y2;
const orientedByX =
this.x1 === this.x2 && this.x1 === other.x1 && this.x1 === other.x2;
} /* if (orientedByY) */ else {
const summedLength = this.length + other.length;
if (!orientedByX && !orientedByY) {
return undefined;
}
const orientedByY =
this.y1 === this.y2 && this.y1 === other.y1 && this.y1 === other.y2;
const orientedByX =
this.x1 === this.x2 && this.x1 === other.x1 && this.x1 === other.x2;
const [x1, x2, y1, y2] = s.split("|").map((x) => Number(x));
if (this.end.equals(other.start)) {
return this.end;
}
if (this.end.equals(other.end)) {
return this.end;
}
if (this.start.equals(other.start)) {
return this.start;
}
if (this.start.equals(other.end)) {
return this.start;
}
(this.y2 - this.y1) * (this.y2 - this.y1)
this.serialized = `${this.x1}|${this.x2}|${this.y1}|${this.y2}`;
if ("x1" in props) {
constructor(
props:
| { x1: number; x2: number; y1: number; y2: number }
| { start: Vector2; end: Vector2 }
) {
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 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;
}
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 {
}
public get y(): number {
}
public get w(): number {
}
public get h(): number {
}
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 {
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 {
for (const rect of intersectingRects) {
// rectToSubtract completely contains rect
if (rectToSubtract.completelyContains(rect)) {
continue;
}
// rectToSubtract partially contains rect
const subrectToRemove = rectToSubtract.getIntersection(rect)!;
// rect completely contains subtractedRect
// -------------------------
// | A |
// | |
// |-----------------------|
// | B | hole | C |
// |-----------------------|
// | |
// | D |
// -------------------------
const newRects = [
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 one
getConnectedComponents(): Rect[][] {
const components: Rect[][] = [];
for (const rect of this.cover) {
const component = this.getConnectedComponentFrom(rect);
components.push(component);
for (const seen of component) {
seenRects[seen.serialize()] = true;
}
}
return components;
}
private getConnectedComponentFrom(start: Rect): Rect[] {
let edge = [start];
while (edge.length > 0) {
let newEdge: Rect[] = [];
for (const rect of edge) {
component[rect.serialize()] = true;
newEdge = newEdge.concat(intersectingRects);
}
edge = newEdge;
}
}
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);
}
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];
for (let j = 0; j < allLines.length; j++) {
const line2 = allLines[j];
const intersection = line1.getOverlap(line2);
if (intersection) {
allLines[i] = undefined;
allLines[j] = undefined;
const newLines = line1.getNonOverlappingSections(line2);
allLines = allLines.concat(newLines);
break;
}
}
}
}
private getComponentsOfOutline(outline: Line[]): Line[][] {
// Store lookup table by start and end vertex
let 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;
lookupTable[idx1].push(line);
lookupTable[idx2].push(line);
}
let result: Line[][] = [];
let visited: { [key: string]: boolean } = {};
for (const line of outline) {
visited[line.serialized] = true;
const sequence = [line];
while (true) {
const current = sequence[sequence.length - 1];
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++) {
let squareSize = 2;
const endSquareX = x + squareSize;
const endSquareY = y + squareSize;
for (let bottomLineX = x; bottomLineX < endSquareX; bottomLineX++) {
squareSize--;
break outer;
}
}
for (let bottomLineY = y; bottomLineY < endSquareY; bottomLineY++) {
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;
}
}
}
}
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 {
}
contains(p: Vector2): boolean {
for (const r of this.cover) {
}
return false;
}
}
if (r.contains(p)) {
return true;
}
if (this.cover.length === 0) {
return true;
}
this.cover = this.cover.map((x) => x.translate(p));
this.oldOutlines = this.oldOutlines.map((l) =>
l.map((ll) => ll.translate(p))
);
rects.push(
new Rect({
x: x,
y: y,
width: squareSize,
height: squareSize,
})
);
if (
covered[(x + squareSize - 1) * MAX_SIZE + bottomLineY] ===
undefined ||
covered[(x + squareSize - 1) * MAX_SIZE + bottomLineY] === true
) {
if (
covered[bottomLineX * MAX_SIZE + (y + squareSize - 1)] ===
undefined ||
covered[bottomLineX * MAX_SIZE + (y + squareSize - 1)] === true
) {
outer: for (; squareSize < MAX_SIZE; squareSize++) {
if (covered[x * MAX_SIZE + y] !== false) {
continue;
}
if (!next) {
break;
}
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 (visited[line.serialized]) {
continue;
}
if (!lookupTable[idx1]) {
lookupTable[idx1] = [];
}
if (!lookupTable[idx2]) {
lookupTable[idx2] = [];
}
return allLines.filter((l) => l !== undefined) as Line[];
if (!line2) {
continue;
}
if (line1 === line2) {
continue;
}
if (!line1) {
continue;
}
result = result.concat(outlineComponents);
return Object.keys(component).map((r) => Rect.DeserializeRect(r));
const intersectingRects = this.cover.filter((r) =>
r.intersects(rect, { edgesOnlyIsAnIntersection: true })
);
if (component[rect.serialize()]) {
continue;
}
const component: { [key: string]: boolean } = {};
if (seenRects[rect.serialize()]) {
continue;
}
const seenRects: { [key: string]: boolean } = {};
{
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));
const intersectingRects = this.cover.filter((r) =>
r.intersects(rectToSubtract, { edgesOnlyIsAnIntersection: false })
);
const subsumingRects = this.cover.filter((r) =>
r.completelyContains(rectToAdd)
);
const intersectingRects = this.cover.filter((r) =>
r.intersects(rectToAdd, { edgesOnlyIsAnIntersection: false })
);
return (
Util.MaxBy(this.cover, (r) => r.bottom)!.bottom -
Util.MinBy(this.cover, (r) => r.y)!.y
);
if (this.cover.length === 0) {
return 0;
}
return (
Util.MaxBy(this.cover, (r) => r.right)!.right -
Util.MinBy(this.cover, (r) => r.x)!.x
);
if (this.cover.length === 0) {
return 0;
}
return Util.MinBy(this.cover, (r) => r.y)!.y;
if (this.cover.length === 0) {
return 0;
}
return Util.MinBy(this.cover, (r) => r.x)!.x;
if (this.cover.length === 0) {
return 0;
}
import { Util } from "../util";
import { Line } from "./line";
import { Rect } from "./rect";
import { Vector2 } from "./vector2";
declare module "Library" {
export interface ModeList {
Normal: never;
}
export type Mode = keyof ModeList;
type HashSet<T> = import("./data_structures/hash").HashSet<T>;
type Entity = import("./entity").Entity;
type KeyboardState = import("./keyboard").KeyboardState;
type CollisionGrid = import("./collision_grid").CollisionGrid;
type Camera = import("./camera").Camera;
export interface IGameState {
camera: Camera;
keys: KeyboardState;
lastCollisionGrid: CollisionGrid;
entities: HashSet<Entity>;
spriteToEntity: { [key: number]: Entity };
renderer: Renderer;
tick: number;
toBeDestroyed: Entity[];
stage: Entity;
mode: Mode;
}
}
const KeyInfo = () => ({
Spacebar: false,
});
export type KeyInfoType = ReturnType<typeof KeyInfo>;
interface QueuedKeyboardEvent {
isDown: boolean;
}
export class KeyboardState {
public justDown = KeyInfo();
private _queuedEvents: QueuedKeyboardEvent[] = [];
constructor() {
}
public clear() {
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) {
/* A-Z */
}
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) {
console.log("got queuedEvent", queuedEvent);
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 = [];
}
}
default:
str = String.fromCharCode(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;
this.down = KeyInfo();
this.justDown = KeyInfo();
this.justUp = KeyInfo();
document.addEventListener("keydown", (e) => this.keyDown(e), false);
document.addEventListener("keyup", (e) => this.keyUp(e), false);
window.addEventListener(
"blur",
() => {
this.clear();
},
false
);
public justUp = KeyInfo();
public down = KeyInfo();
event: KeyboardEvent;
Enter: false,
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,
import { Entity, EntityType } from "./entity";
import { Vector2 } from "./geometry/vector2";
import { Texture } from "pixi.js";
import { Rect } from "./geometry/rect";
import { BaseGame } from "./base_game";
import { BaseGameState } from "./base_state";
export class MovingEntity extends Entity {
protected _maxSpeed = 50;
constructor(props: {
game: BaseGame<{}>;
texture: Texture;
collidable: boolean;
}) {
super({
...props,
name: "MovingEntity",
});
this._collidable = props.collidable;
}
public get velocity(): Vector2 {
return this._velocity;
}
public set velocity(dir: Vector2) {
this._velocity = dir;
}
public get maxSpeed(): number {
return this._maxSpeed;
}
// Currently just stops moving.
collide = (other: Entity, intersection: Rect) => {
// if (!this._collidable) return;
// this.velocity = Vector2.Zero;
};
// It's just shy
interact = (other: Entity) => {
return;
};
}
public update = (state: BaseGameState) => {};
private _velocity = Vector2.Zero;
entityType = EntityType.MovingEntity;
type ReactWrapperProps = {
game: BaseGame<{}>;
debugFlags: DebugFlagsType;
};
type ReactWrapperState = {
selected: Entity | Container | null;
moused: Entity | Container | null;
};
static Instance: GameReactWrapper;
mounted = false;
constructor(props: ReactWrapperProps) {
super(props);
this.state = {
selected: this.props.game.stage,
moused: null,
};
setInterval(() => this.monitorHierarchyUpdates(), 500);
}
componentDidMount() {
this.mounted = true;
GameReactWrapper.Instance = this;
}
componentWillUnmount() {
console.error("This should never happen!!!! very bad?!?");
}
monitorHierarchyUpdates = () => {
if (this.mounted) {
this.forceUpdate();
}
};
setSelected = (obj: Entity | Container) => {
this.setState({
selected: obj,
});
};
setMoused = (obj: Entity | Container | null) => {
this.setState({
moused: obj,
});
};
renderSelected = () => {
const target = this.state.moused || this.state.selected;
if (target instanceof Container) {
return (
);
}
return (
<div>
<div>
x: {target.x}, y: {target.y}
</div>
<div>
</div>
<div>
width: {target.width}, height: {target.height}
</div>
<div>visible: {target.visible ? "true" : "false"}</div>
<div>
</div>
</div>
);
};
renderHierarchy() {
}
render() {
return (
</div>
<DebugFlagButtons flags={this.props.debugFlags} />
{this.renderSelected()}
{this.renderHierarchy()}
</div>
</div>
</div>
);
}
}
export const CreateGame = (game: BaseGame<any>, debugFlags: DebugFlagsType) => {
ReactDOM.render(
<React.StrictMode>
</React.StrictMode>,
);
};
document.getElementById("root")
<GameReactWrapper game={game} debugFlags={debugFlags} />
)}
<div
style={{
fontWeight: 600,
fontFamily: "arial",
paddingTop: "8px",
paddingBottom: "8px",
fontSize: "18px",
}}
>
Debug Hierarchy
</div>
<div>Draw Count: {Debug.GetDrawCount()}</div>
<div
style={{
fontWeight: 600,
fontFamily: "arial",
paddingBottom: "8px",
fontSize: "18px",
}}
>
Debug Options
</div>
<div
style={{
display: "flex",
flexDirection: "row",
borderLeft: IS_DEBUG ? "1px solid lightgray" : 0,
marginLeft: "16px",
paddingLeft: "8px",
}}
>
<div
style={{
overflow: "auto",
height: "90vh",
fontFamily: "arial",
fontSize: "14px",
}}
>
{this.props.game && this.props.game.stage && IS_DEBUG && (
<div style={{ paddingLeft: "8px" }}>
<div
style={{
fontFamily: "arial",
marginBottom: "8px",
fontSize: "14px",
width: "300px",
padding: "8px",
}}
>
Note: This debugging panel is only shown in development, or
production with ?debug=true.
<Hierarchy
selectedEntity={this.state.selected}
setMoused={this.setMoused}
setSelected={this.setSelected}
root={this.props.game.fixedCameraStage}
gameState={this.props.game.state}
/>
</div>
);
return (
<div>
<Hierarchy
selectedEntity={this.state.selected}
setMoused={this.setMoused}
setSelected={this.setSelected}
root={this.props.game.stage}
gameState={this.props.game.state}
/>
{target instanceof TextEntity ? (
<div>text: {target.html}</div>
) : (
<div>hi</div>
)}
scaleX: {target.scale.x.toFixed(2)} scaleY:{" "}
{target.scale.y.toFixed(2)}
xAbs: {target.positionAbsolute().x}, yAbs:{" "}
{target.positionAbsolute().y}
<div
style={{
fontWeight: 600,
fontFamily: "arial",
paddingTop: "8px",
paddingBottom: "8px",
fontSize: "18px",
}}
>
{target.name}
</div>
<div
style={{
fontWeight: 600,
fontFamily: "arial",
paddingTop: "8px",
paddingBottom: "8px",
fontSize: "18px",
}}
>
Stage
</div>
if (target === null) {
return null;
}
export class GameReactWrapper extends React.Component<
ReactWrapperProps,
ReactWrapperState
> {
import { BaseGame } from "../base_game";
import { Hierarchy } from "./hierarchy";
import { DebugFlagButtons, DebugFlagsType } from "./debug_flag_buttons";
import { IS_DEBUG } from "../environment";
import { Entity } from "../entity";
import { Container } from "pixi.js";
import { TextEntity } from "../text_entity";
import { Debug } from "../debug";
import React from "react";
import ReactDOM from "react-dom";
// import { DebugFlags } from '../../game/debug';
type HierarchyProps = {
root: Entity | Container;
setSelected: (obj: Entity | Container) => void;
setMoused: (obj: Entity | Container | null) => void;
gameState: IGameState;
selectedEntity: Entity | Container | null;
};
constructor(props: HierarchyProps) {
super(props);
this.state = {
hover: false,
collapsed: true,
};
setInterval(() => {
this.updateDebugGraphics();
}, 200);
}
oldTint: { [key: number]: number } = {};
hoverGraphics: Graphics[] = [];
hoverTarget: Entity | Container | null = null;
updateDebugGraphics = () => {
// clear debug graphics
for (const graphic of this.hoverGraphics) {
graphic.parent.removeChild(graphic);
graphic.destroy();
}
this.hoverGraphics = [];
if (this.hoverTarget !== null) {
if (this.props.root instanceof Entity) {
const point = Debug.DrawPoint(this.props.root.position, 0xff0000, true);
}
}
if (this.props.selectedEntity === this.props.root) {
if (this.props.root instanceof Entity) {
}
}
};
mouseOver = () => {
if (this.props.root instanceof Entity) {
this.oldTint[this.props.root.id] = this.props.root.sprite.tint;
this.props.root.sprite.tint = 0x000099;
}
this.hoverTarget = this.props.root;
this.props.setMoused(this.props.root);
};
mouseOut = () => {
if (this.props.root instanceof Entity) {
this.props.root.sprite.tint = this.oldTint[this.props.root.id];
}
this.hoverTarget = null;
this.props.setMoused(null);
};
click = () => {
this.props.setSelected(this.props.root);
console.log(this.props.root);
};
renderLeaf(root: any) {
}
render() {
const root = this.props.root;
let children = allChildren;
let canCollapse = children.length > 20;
let didCollapse = false;
if (canCollapse) {
if (this.state.collapsed) {
children = children.slice(0, 20);
didCollapse = true;
}
}
if (children)
return (
<div
style={{
paddingLeft: "10px",
}}
>
<div
onMouseEnter={this.mouseOver}
onMouseLeave={this.mouseOut}
onClick={this.click}
>
{this.renderLeaf(root)}
</div>
</div>
}
);
}
{children.map((child) => {
return (
<Hierarchy
selectedEntity={this.props.selectedEntity}
setMoused={this.props.setMoused}
setSelected={this.props.setSelected}
root={child}
gameState={this.props.gameState}
/>
);
})}
{canCollapse ? (
<div
onClick={() =>
this.setState({ collapsed: !this.state.collapsed })
}
style={{ padding: "8px 0" }}
>
{didCollapse ? (
<span>[see {allChildren.length - 20} more]</span>
) : (
<span>[collapse]</span>
)}
</div>
) : null}
fontFamily: "Arial",
fontSize: "14px",
backgroundColor: this.state.hover ? "darkgray" : "black",
let allChildren = root instanceof Entity ? root.children() : [];
return (
<div>
{this.props.selectedEntity === this.props.root ? (
<strong>{root.name}</strong>
) : (
root.name
)}{" "}
(depth: {root.zIndex}){" "}
{root instanceof Entity &&
(root.activeModes.includes(this.props.gameState.mode)
? "Active"
: "Inactive")}
</div>
);
this.setState({ hover: false });
this.setState({ hover: true });
this.hoverGraphics = [...this.hoverGraphics, point];
const point = Debug.DrawPoint(
this.props.selectedEntity.position,
0xff0000,
true
);
this.hoverGraphics = [
...this.hoverGraphics,
...Debug.DrawBounds(this.props.selectedEntity, 0xff0000, true, "stage"),
];
this.hoverGraphics = [...this.hoverGraphics, point];
this.hoverGraphics = [
...Debug.DrawBounds(this.props.root, 0xff0000, true, "stage"),
];
export class Hierarchy extends React.Component<
HierarchyProps,
{
hover: boolean;
collapsed: boolean;
}
> {
import { Container, Graphics } from "pixi.js";
import { Entity } from "../entity";
import { Debug } from "../debug";
import { IGameState } from "Library";
import React from "react";
export type DebugFlagsType = {
[key: string]: boolean;
};
const LOCAL_STORAGE_KEY = "debug flags";
if (IS_DEBUG) {
// delete flags that don't exist
for (const flagName of Object.keys(prevStoredFlags)) {
if (!defaultFlags.hasOwnProperty(flagName)) {
delete prevStoredFlags[flagName];
}
}
return {
...defaultFlags,
...prevStoredFlags,
};
} else {
return defaultFlags;
}
const SaveDebugFlagsToLocalStorage = (flags: DebugFlagsType) => {
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(flags));
};
type DebugFlagButtonsProps = {
flags: DebugFlagsType;
};
render() {
const flagNames = Object.keys(this.props.flags);
return (
<div>
</div>
}
}
);
SaveDebugFlagsToLocalStorage(this.props.flags);
}}
/>{" "}
{flagName}
</div>
);
})}
// The point being that the value gets synced back into the
// game with no one being the wiser. MAGIC!
this.props.flags[flagName] = !this.props.flags[flagName];
// The reason this works at all is because GameReactWrapper
// does a forceUpdate() on a setInterval to keep everything
// in sync. DebugFlagButtons will be captured in the
// setInterval and forced to update.
return (
<div key={flagName}>
<input
type="checkbox"
checked={flag}
onChange={() => {
// NOTE: This is TERRIBLE React code. DO NOT LEARN FROM
// THIS. DO NOT IMITATE THIS. IN FACT, RUN FAR AWAY FROM
// THIS!!!
{flagNames.map((flagName) => {
const flag = this.props.flags[flagName];
export class DebugFlagButtons extends React.Component<
DebugFlagButtonsProps,
{}
> {
};
const prevStoredFlags = JSON.parse(
window.localStorage.getItem(LOCAL_STORAGE_KEY) || "{}"
) as DebugFlagsType;
export const ReadDebugFlagsFromLocalStorage = <T extends DebugFlagsType>(
defaultFlags: T
): T => {
import React from "react";
import { IS_DEBUG } from "../environment";
{
"compilerOptions": {
/* Basic Options */
// "node"
// ],
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}
}
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"strict": true /* Enable all strict type-checking options. */,
// "types": [
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
import { Util } from "../util";
const configPath = process.argv[2];
const configDirectory = path.dirname(configPath);
const assetDirectory = path.join(configDirectory, config.assets.assetsPath);
function walkDir(dir: string, callback: (path: string) => void) {
fs.readdirSync(dir).forEach((f: string) => {
const isDirectory = fs.statSync(dirPath).isDirectory();
isDirectory ? walkDir(dirPath, callback) : callback(path.join(dir, f));
});
function allNestedFiles(dir: string): string[] {
let files: string[] = [];
files.push(path.slice(dir.length));
});
return files;
}
const isPathTiledTileMap = (path: string) => {
try {
} catch (e) {
return false;
}
};
const isPathTiledWorldMap = (path: string) => {
try {
} catch (e) {
return false;
}
};
const buildAssetsFile = () => {
const normalFiles: string[] = [];
const animationBundles: { [key: string]: string[] } = {};
for (const file of allFiles) {
const match = /(.+)_(\d+)\.(png|gif)$/.exec(file);
if (match !== null) {
const [fullString, prefix, frame, extension] = match;
animationBundles[prefix][Number(frame)] = file;
continue;
}
const match2 = /(.+) \((\d+)\)\.(png|gif)$/.exec(file);
if (match2 !== null) {
const [fullString, prefix, frame, extension] = match2;
animationBundles[prefix][Number(frame)] = file;
continue;
}
normalFiles.push(file);
continue;
}
let output = `// THIS FILE IS AUTOGENERATED from the parameters in config.json. Do not edit it.
// If you want to change something about how it's generated, look at library/asset_builder.ts.
import { TypesafeLoader } from "../library/typesafe_loader";
export type AssetType =
| "Image"
| "TileMap"
| "TileWorld"
| "Audio"
| "Spritesheet"
| "Animation"
export type AssetName = keyof typeof AssetsToLoad
export type AssetPath =
export const AssetsToLoad = {
if (allFiles.length === 0) {
return output;
}
const longestAssetType = "'TileWorld'".length;
for (const file of normalFiles) {
if (file.endsWith(".png") || file.endsWith(".gif")) {
resourceType = "Image";
} else if (file.endsWith(".json")) {
if (isPathTiledTileMap(path.join(assetDirectory, file))) {
resourceType = "TileMap";
} else if (isPathTiledWorldMap(path.join(assetDirectory, file))) {
resourceType = "TileWorld";
} else {
continue;
}
} else if (file.endsWith(".mp3")) {
resourceType = "Audio";
}
const fileNameWithoutExtension = file.slice(0, file.lastIndexOf("."));
}
if (Object.keys(animationBundles).length > 0) {
for (const animationName of Object.keys(animationBundles)) {
output += ` type: "Animation" as const,\n`;
output += ` paths: [\n`;
for (const frame of animationBundles[animationName]) {
}
output += ` ],\n`;
output += ` },\n`;
}
}
output += "};\n";
output += "export const Assets = new TypesafeLoader(AssetsToLoad);\n";
return output;
function writeAssetsFile() {
fs.writeFileSync(assetFilePath, buildAssetsFile());
}
fs.watch(
Util.Debounce(() => {
writeAssetsFile();
})
);
writeAssetsFile();
assetDirectory,
{ recursive: true },
console.log(`[${Util.FormatDate(new Date())}] Recompiling...`);
};
output += "\n";
output += ` "${frame}",\n`;
if (frame === undefined) {
continue;
}
output += ` "${animationName}": {\n`;
output += `\n`;
output += ` /* Animations */\n`;
output += `\n`;
output += ` "${Util.PadString(
fileNameWithoutExtension,
longestTruncatedFileLength,
'"'
)}: { type: "${Util.PadString(
resourceType,
longestAssetType,
'"'
)} as const, path: "${Util.PadString(file, longestFileLength, '"')} },\n`;
let resourceType = "";
const longestTruncatedFileLength = Util.MaxBy(allFiles, (x) =>
x.lastIndexOf(".")
)!.lastIndexOf(".");
const longestFileLength = Util.MaxBy(allFiles, (x) => x.length)!.length;
output += " // No files found!";
output += "}";
`;
${allKeys.map((key) => ` | "${key}"\n`).join("")}
${allKeys.length === 0 ? " | void\n" : ""}
const allKeys = normalFiles.concat(
Util.FlattenByOne(
Object.keys(animationBundles).map((key) => animationBundles[key])
)
);
animationBundles[prefix] = animationBundles[prefix] || [];
animationBundles[prefix] = animationBundles[prefix] || [];
const allFiles = allNestedFiles(assetDirectory).filter((file) =>
assetExtensions.find((ext) => file.endsWith(ext))
);
const assetExtensions = [".png", ".gif", ".mp3", ".json"];
return json.maps && json.type === "world";
const json = JSON.parse(fs.readFileSync(path, "utf8"));
return json.version && json.tilewidth && json.type === "map";
const json = JSON.parse(fs.readFileSync(path, "utf8"));
walkDir(dir, (path) => {
}
const dirPath = path.join(dir, f);
const assetFilePath = path.join(
configDirectory,
config.assets.compiledAssetsFile
);
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
import * as fs from "fs";
import * as path from "path";
import { BaseGame } from "./base_game";
import { AllResourcesType } from "./typesafe_loader";
import { Entity, AugmentedSprite } from "./entity";
import { HashSet } from "./data_structures/hash";
const serializedClasses: { [key: string]: Function } = {};
export function serialized(constructor: Function) {
serializedClasses[constructor.name] = constructor;
}
type GenericJSON = {
};
type SerializeJSON = {
entities: GenericJSON[];
stage: number;
fixedStage: number;
parallaxStage: number;
};
const run: { [key: string]: boolean } = {};
export const once = (fn: () => void) => {
if (!run[fn.toString()]) {
fn();
run[fn.toString()] = true;
}
export class Serializer<T extends AllResourcesType> {
game: BaseGame<T>;
constructor(game: BaseGame<T>) {
this.game = game;
}
getAllEntities(thing: HashSet<Entity> | Entity[]): Entity[] {
let list: Entity[] = [];
if (Array.isArray(thing)) {
list = thing;
} else {
list = thing.values();
}
let result: Entity[] = [];
for (const e of list) {
}
return result;
}
public serializeEntity(e: Entity): string {
const result: { [key: string]: any } = {};
for (const key in this) {
const val = this[key];
if (val instanceof AugmentedSprite) {
result[key] = {
__type: "AugmentedSprite",
} else if (val instanceof Entity) {
result[key] = {
__type: "NestedEntity",
__subtype: val.constructor.name,
__id: val.id,
};
result[key] = (val as any).toJSON();
} else {
result[key] = val;
}
}
// .map(([key]) => key)
for (const [name, descriptor] of getters) {
result["get:" + name] = descriptor.get!.bind(e)();
}
console.log(result);
return JSON.stringify({
__type: "Entity",
__subtype: this.constructor.name,
...result,
});
}
serialize(): string {
const idToEntity: { [key: string]: Entity } = {};
for (const e of allEntities.values()) idToEntity[e.id] = e;
const entities = this.game.state.entities.values();
const e = entities[0];
once(() => {
console.log(allEntities);
const result = JSON.stringify(entities, (key, value) => {
if (value instanceof Entity) {
return this.serializeEntity(value);
} else if (
typeof value === "number" ||
typeof value === "string" ||
typeof value === "boolean" ||
return JSON.stringify(value);
} else {
console.log("Unhandled:", value);
throw new Error("Unhandled type!");
}
});
console.log(result);
});
return "";
}
}
typeof value === "undefined"
) {
const allEntities = new HashSet(
this.getAllEntities(this.game.state.entities)
);
const getters = Object.entries(
Object.getOwnPropertyDescriptors(Object.getPrototypeOf(this))
).filter(([key, descriptor]) => typeof descriptor.get === "function");
} else if (typeof val === "object" && val !== null && "toJSON" in val) {
};
result = [...result, e, ...this.getAllEntities(e.children())];
};
[key: string]:
| string
| number
| boolean
| null
| undefined
| GenericJSON[]
| GenericJSON;
export const SetAudioToLoop = (audio: HTMLAudioElement) => {
audio.currentTime = 0;
audio.play();
});
return audio;
};
audio.addEventListener("ended", () => {
import { BaseTextEntity } from "./base_text_entity";
import { BaseGameState } from "./base_state";
export type TextAlignType = "left" | "right" | "center";
export type TextEntityStyle = {
color: string;
fontSize: number;
align?: TextAlignType;
export type TextStyles = {
[key: string]: TextEntityStyle;
export type TextSegment = {
text: string;
style: TextEntityStyle;
export enum TextSegmentState {
NormalText,
IdText,
StyledText,
}
if (currentState === TextSegmentState.NormalText) {
return TextSegmentState.IdText;
} else if (currentState === TextSegmentState.IdText) {
return TextSegmentState.StyledText;
} else if (currentState === TextSegmentState.StyledText) {
return TextSegmentState.NormalText;
}
return undefined as any; // stupid typechecker
/**
* "%1%This is some red text% normal text %2%blue text!%".
*/
export class TextEntity extends BaseTextEntity<BaseGameState> {
customStyles: TextStyles;
defaultStyle: TextEntityStyle;
public static StandardStyles: TextStyles = {
1: { color: "white", fontSize: 32, align: "left" },
2: { color: "cyan", fontSize: 40, align: "left" },
};
/**
* "%1%This is some red text% normal text %2%blue text!%".
*/
constructor({
text,
styles = TextEntity.StandardStyles,
width = 500,
height = 300,
color = "white",
fontSize = 32,
align = "left",
super("", width, height);
this.defaultStyle = { color, fontSize, align };
this.customStyles = styles;
this.setText(text);
}
setText(text: string): void {
if (text === "") {
this.html = "";
return;
}
const textSegments = this.buildTextSegments(text);
style="
font-family: FreePixel;
this.html = html;
}
set color(color: string) {
}
// TODO: This is a hard function to write properly.
calculateTextWidth = (text: string) => {
const canvas = document.getElementById("canvas2d")! as HTMLCanvasElement;
const context = canvas.getContext("2d")!;
context.font = `${this.defaultStyle.fontSize}px FreePixel`;
const calculatedWidth = context.measureText(text).width;
return calculatedWidth;
};
buildTextSegments(text: string): TextSegment[] {
let i = 0;
const readChar = () => text[i++];
let state = TextSegmentState.NormalText;
let id = "";
while (i < text.length) {
const ch = readChar();
if (ch === "%") {
if (state === TextSegmentState.NormalText) {
id = "";
} else if (state === TextSegmentState.IdText) {
segments.push({
text: "",
style: this.customStyles[id],
});
} else if (state === TextSegmentState.StyledText) {
segments.push({
text: "",
style: this.defaultStyle,
});
}
state = AdvanceState(state);
continue;
} else {
if (state === TextSegmentState.NormalText) {
segments[segments.length - 1].text += ch;
} else if (state === TextSegmentState.IdText) {
id += ch;
} else if (state === TextSegmentState.StyledText) {
segments[segments.length - 1].text += ch;
}
}
}
}
// public set width(value: number) {
// this.sprite.width = value;
// // this.buildTextGraphic();
// }
// public set height(value: number) {
// this.sprite.width = value;
// // this.buildTextGraphic();
// }
}
return segments.filter((segment) => segment.text.trim() !== "");
const segments: TextSegment[] = [
{
text: "",
style: this.defaultStyle,
},
];
// This only works after the CSS has loaded
this.defaultStyle = { ...this.defaultStyle, color: color };
text-align: ${segment.style.align || "left"};
font-size: ${segment.style.fontSize}px;"
>${segment.text}</span>`;
})
.join("")
.replace(/\n/g, "");
color: ${segment.style.color};
const html = textSegments
.map((segment) => {
return `<span
}: {
text: string;
styles?: TextStyles;
width?: number;
height?: number;
color?: string;
fontSize?: number;
align?: TextAlignType;
}) {
* Format:
*
* Format:
*
};
export const AdvanceState = (
currentState: TextSegmentState
): TextSegmentState => {
};
};
};
import { Rectangle, Texture } from "pixi.js";
// import { AssetName } from '../game/assets';
import { Tile } from "./tilemap/tilemap_types";
import { TypesafeLoader } from "./typesafe_loader";
// import { C } from '../game/constants';
export class TextureCache {
static Cache: { [key: string]: Texture } = {};
public static GetTextureFromSpritesheet({
resourceName: textureName,
x,
y,
tilewidth,
tileheight,
assets,
}: {
resourceName: any; // AssetName;
x: number;
y: number;
tilewidth: number;
tileheight: number;
assets: TypesafeLoader<{}>;
}): Texture {
const key = `${textureName}-${x}-${y}`;
if (!TextureCache.Cache[key]) {
const texture = (assets.getResource(textureName) as Texture).clone();
texture.frame = new Rectangle(
x * tilewidth,
y * tileheight,
tilewidth,
tileheight
);
this.Cache[key] = texture;
}
return this.Cache[key];
}
public static GetTextureForTile({
assets,
tile,
}: {
assets: TypesafeLoader<{}>;
tile: Tile;
}): Texture {
const {
tile: { imageUrlRelativeToGame, spritesheetx, spritesheety },
} = tile;
return TextureCache.GetTextureFromSpritesheet({
// TODO: Is there any way to improve this cast?
// Once I add a loader for tilemaps, probably yes!
resourceName: imageUrlRelativeToGame.slice(
0,
imageUrlRelativeToGame.lastIndexOf(".")
) as any, // as AssetName
x: spritesheetx,
y: spritesheety,
tilewidth: tile.tile.tilewidth,
tileheight: tile.tile.tileheight,
assets: assets,
});
}
}
export interface TiledTileLayerChunkJSON {
data: number[];
height: number;
width: number;
x: number;
y: number;
}
export interface TiledTileLayerJSON {
chunks: TiledTileLayerChunkJSON[];
height: number;
id: number;
name: string;
opacity: number;
startx: number;
starty: number;
offsetx: number;
offsety: number;
type: "tilelayer";
visible: boolean;
width: number;
x: number;
y: number;
}
export interface TiledGroupLayerJSON {
id: number;
layers: TiledLayerTypes[];
name: string;
opacity: string;
type: "group";
visible: boolean;
x: number;
y: number;
}[];
export interface TiledObjectJSON {
gid?: number;
properties?: any;
propertytypes?: { [key: string]: "int" | "string" };
height: number;
id: number;
name: string;
rotation: number;
type: any;
visible: boolean;
width: number;
x: number;
y: number;
}
export interface TiledObjectLayerJSON {
draworder: "topdown" | "index";
type: "objectgroup";
}
export interface TilesetTilesJSON {
objectgroup?: TiledObjectLayerJSON;
value: string;
}[];
}
export interface TilesetJSON {
imageheight: number;
}
| TiledTileLayerJSON
| TiledObjectLayerJSON
export interface TiledJSON {
height: number;
nextobjectid: number;
orientation: "orthogonal";
renderorder: "right-down";
tileheight: number;
tilewidth: number;
version: number;
layers: TiledLayerTypes[];
tilesets: TilesetJSON[];
}
export interface Tile {
}
export interface Tileset {
gidStart: number;
gidEnd: number;
name: string;
imageUrlRelativeToTilemap: string;
imageUrlRelativeToGame: string;
imageheight: number;
}
export interface TiledObject {
tile: SpritesheetTile;
height: number;
width: number;
x: number;
y: number;
}
export interface SpritesheetTile {
imageUrlRelativeToGame: string;
spritesheetx: number;
spritesheety: number;
tilewidth: number;
tileProperties: TilesetTilesJSON[] | undefined;
}
tileheight: number;
properties?: { [key: string]: string };
tilewidth: number;
tileheight: number;
tiles: TilesetTilesJSON[] | undefined;
imagewidth: number;
x: number;
y: number;
gid: number;
tile: SpritesheetTile;
isCollider: boolean;
tileProperties: { [key: string]: unknown };
width: number;
| TiledGroupLayerJSON;
export type TiledLayerTypes =
tiles?: TilesetTilesJSON[];
imagewidth: number;
margin: number;
name: string;
spacing: number;
tilecount: number;
tileheight: number;
tilewidth: number;
columns: number;
firstgid: number;
image: string;
properties?: {
name: string;
type: "string"; // TODO: There are probably others. And yes, the literal string "string".
id: number;
height: number;
name: string;
objects: TiledObjectJSON[];
opacity: number;
visible: boolean;
width: number;
x: number;
y: number;
export type TiledPropertiesType = {
name: string;
type: string;
value: string;
}
import { Entity } from "../entity";
import { Rect } from "../geometry/rect";
import { TiledObjectLayerJSON, Tile } from "./tilemap_types";
import { TextureCache } from "../texture_cache";
import { Grid } from "../data_structures/grid";
import { Texture } from "pixi.js";
import { TiledTilemap, MapLayer } from "./tilemap";
import { TilemapRegion } from "./tilemap_data";
import { TypesafeLoader } from "../typesafe_loader";
export type GetInstanceTypeProps = {
type TilemapCustomObjectSingle = {
};
type TilemapCustomObjectGroup = {
};
type TilemapCustomObjectRect = {
layerName: string;
};
| TilemapCustomObjectGroup
| TilemapCustomObjectSingle
export type ObjectInfo = { entity: Entity; layerName: string };
export class TiledTilemapObjects {
private _layers: TiledObjectLayerJSON[];
private _customObjects: TilemapCustomObjects[];
private _map: TiledTilemap;
/**
* Every custom object in the game.
*/
private _allObjects: ObjectInfo[] = [];
private _assets: TypesafeLoader<any>;
constructor(props: {
customObjects: TilemapCustomObjects[];
}) {
const { layers, customObjects, map } = props;
this._customObjects = customObjects;
for (const layer of this._layers) {
const objectsInLayer = this.loadLayer(layer);
this._allObjects = [...this._allObjects, ...objectsInLayer];
}
this.turnOffAllObjects();
}
turnOffAllObjects() {
for (const customObject of this._allObjects) {
customObject.entity.stopUpdating();
}
}
loadObjectLayers(): MapLayer[] {
this.turnOffAllObjects();
let result: MapLayer[] = [];
for (const layer of this._layers) {
result.push({
objectLayer: true,
});
}
for (const object of this._allObjects) {
associatedLayer.entity.addChild(object.entity);
object.entity.startUpdating();
}
return result;
}
private loadLayer(layer: TiledObjectLayerJSON): ObjectInfo[] {
const results: ObjectInfo[] = [];
type ObjectInGroup = {
gridX: number;
gridY: number;
};
const objectsToGroup: ObjectInGroup[] = [];
// Add all single objects
if (!obj.gid) {
// this is probably a region, so see if we have one of those.
for (const customObject of this._customObjects) {
customObject.process({
rect: new Rect({
properties: TiledTilemap.ParseTiledProperties(obj.properties),
});
continue processNextObject;
}
}
}
const { spritesheet, tileProperties } = this._map._data.gidInfo(obj.gid);
const objProperties: { [key: string]: unknown } = {};
tileProperties[name] = value;
}
const allProperties = {
...tileProperties,
...objProperties,
};
let newObj: Entity | null = null;
let x = obj.x;
const tile = {
tileProperties: allProperties,
};
const tileName = allProperties.name as string;
if (tileName === undefined) {
throw new Error("Custom object needs a tile type");
}
if (obj.type === "single") {
return obj.name === tileName;
}
if (obj.type === "group") {
return obj.names.includes(tileName);
}
return false;
});
if (associatedObject === undefined) {
}
if (associatedObject.type === "single") {
if (associatedObject.name === tileName) {
newObj = associatedObject.getInstanceType(spriteTex, allProperties, {
layerName: layer.name,
x: tile.x,
y: tile.y,
});
}
} else if (associatedObject.type === "group") {
// add to the list of grouped objects, which we will process later.
if (associatedObject.names.includes(tileName)) {
objectsToGroup.push({
name: tileName,
tile: tile,
// TODO: We're making an assumption that the size of the objects
// are all the same. I think this is safe tho?
gridX: tile.x / obj.width,
gridY: tile.y / obj.height,
});
}
}
if (newObj) {
newObj.x = tile.x;
newObj.y = tile.y;
results.push({
layerName: layer.name,
});
}
}
// Find all groups and add them
// Step 1: Load all objects into grid
for (const objectToGroup of objectsToGroup) {
grid.set(objectToGroup.gridX, objectToGroup.gridY, {
grouped: false,
});
}
// Step 2: BFS from each object to find all neighbors which are part of the
// group.
for (const obj of objectsToGroup) {
const result = grid.get(obj.gridX, obj.gridY);
const { grouped } = result;
if (grouped) {
continue;
}
// Step 2a: Find all names of objects in that group
let customObject: TilemapCustomObjectGroup | null = null;
for (const candidate of this._customObjects) {
if (candidate.type === "group") {
if (candidate.names.includes(obj.name)) {
customObject = candidate;
break;
}
}
}
if (customObject === null) {
throw new Error("HUH!?!?");
}
// Step 2: Actually run BFS
const group: ObjectInGroup[] = [obj];
const groupEdge: ObjectInGroup[] = [obj];
while (groupEdge.length > 0) {
const current = groupEdge.pop()!;
const dxdy = [
];
for (const [dx, dy] of dxdy) {
const result = grid.get(current.gridX + dx, current.gridY + dy);
const { obj: neighbor, grouped } = result;
if (customObject.names.includes(neighbor.name)) {
group.push(neighbor);
groupEdge.push(neighbor);
}
}
}
// BFS complete; `group` contains entire group.
for (const obj of group) {
grid.get(obj.gridX, obj.gridY)!.grouped = true;
}
// Find (x, y) of group
let minTileX = Number.POSITIVE_INFINITY;
let minTileY = Number.POSITIVE_INFINITY;
for (const obj of group) {
minTileX = Math.min(minTileX, obj.tile.x);
minTileY = Math.min(minTileY, obj.tile.y);
}
const groupEntity = customObject.getGroupInstanceType({
layerName: layer.name,
});
groupEntity.x = minTileX;
groupEntity.y = minTileY;
for (const obj of group) {
const objEntity = customObject.getInstanceType(spriteTex);
groupEntity.addChild(objEntity);
objEntity.x = obj.tile.x - groupEntity.x;
objEntity.y = obj.tile.y - groupEntity.y;
}
results.push({
layerName: layer.name,
});
}
return results;
}
getAllObjects(): ObjectInfo[] {
return this._allObjects;
}
}
entity: groupEntity,
const spriteTex = TextureCache.GetTextureForTile({
assets: this._assets,
tile: obj.tile,
});
x: minTileX,
y: minTileY,
if (grouped) {
continue;
}
if (group.includes(neighbor)) {
continue;
}
if (!result) {
continue;
}
[1, 0],
[-1, 0],
[0, 1],
[0, -1],
if (!result) {
throw new Error("Wat");
}
obj: objectToGroup,
const grid = new Grid<{ obj: ObjectInGroup; grouped: boolean }>();
entity: newObj,
const spriteTex = TextureCache.GetTextureForTile({
assets: this._assets,
tile,
});
throw new Error(`Unhandled tile type: ${tileName}`);
const associatedObject = this._customObjects.find((obj) => {
x: x,
y: y,
tile: spritesheet,
isCollider: this._map._data._gidHasCollision[obj.gid] || false,
gid: obj.gid,
let y = obj.y - spritesheet.tileheight; // Tiled pivot point is (0, 1) so we need to subtract by tile height.
for (const { name, value } of obj.properties || []) {
throw new Error(
`on layer ${layer.name} at position x: ${obj.x} and y: ${obj.y} you have a rect region that's not being processed`
);
x: obj.x,
y: obj.y,
width: obj.width,
height: obj.height,
}),
if (
customObject.type === "rect" &&
customObject.layerName === layer.name
) {
processNextObject: for (const obj of layer.objects) {
// Step 0:
name: string;
tile: Tile;
const associatedLayer = result.find(
(obj) => obj.layerName === object.layerName
)!;
entity: new Entity({ name: layer.name }),
layerName: layer.name,
this._map = map;
this._assets = props.assets;
this._layers = layers;
map: TiledTilemap;
assets: TypesafeLoader<any>;
layers: TiledObjectLayerJSON[];
| TilemapCustomObjectRect;
export type TilemapCustomObjects =
process: (rect: TilemapRegion) => void;
type: "rect";
type: "group";
names: string[];
getInstanceType: (tex: Texture) => Entity;
getGroupInstanceType: (props: GetInstanceTypeProps) => Entity;
type: "single";
name: string;
getInstanceType: (
tex: Texture,
tileProperties: { [key: string]: unknown },
props: GetInstanceTypeProps
) => Entity | null;
layerName: string;
x: number;
y: number;
};
import { Grid } from "../data_structures/grid";
import { Rect } from "../geometry/rect";
import { RectGroup } from "../geometry/rect_group";
import { Vector2 } from "../geometry/vector2";
import { TiledTilemap } from "./tilemap";
import { Util } from "../util";
export type TilemapRegion = {
properties: { [key: string]: string };
| {
export class TilemapData {
private _tileHeight: number;
// (should be private, but cant be for organization reasons)
_gidHasCollision: { [id: number]: boolean } = {};
const { data, pathToTilemap } = props;
this._data = data;
}
isGidCollider(gid: number): boolean {
return this._gidHasCollision[gid] || false;
}
getTileWidth(): number {
return this._tileWidth;
}
getTileHeight(): number {
return this._tileHeight;
}
getTilesets(): Tileset[] {
return this._tilesets;
}
private loadTilesets(pathToTilemap: string, json: TiledJSON): Tileset[] {
const tilesets: Tileset[] = [];
tilesets.push({
name,
imageUrlRelativeToTilemap,
imageUrlRelativeToGame,
imagewidth,
imageheight,
tilewidth,
tileheight,
tiles,
gidStart: firstgid,
});
}
return tilesets;
}
private buildCollisionInfoForTiles(): { [key: number]: boolean } {
// Build a dumb (for now) object of collision ids by just checking if the
// tile literally has any collision object at all and takes that to mean the
// entire thing is covered.
// We could improve this if we want!
const gidHasCollision: { [id: number]: boolean } = {};
for (const tileset of this._data.tilesets) {
if (tileset.tiles) {
for (const tileAndCollisionObjects of tileset.tiles) {
if (!tileAndCollisionObjects.objectgroup) {
continue;
}
if (tileAndCollisionObjects.objectgroup.objects.length > 0) {
gidHasCollision[
tileAndCollisionObjects.id + tileset.firstgid
] = true;
}
}
}
}
return gidHasCollision;
}
getLayerNames(): string[] {
return Object.keys(this._layers);
}
private getAllLayers(): (TiledTileLayerJSON | TiledObjectLayerJSON)[] {
return this._getAllLayersHelper(this._data.layers);
}
getLayer(layerName: string) {
return this._layers[layerName];
}
/**
* Returns all layers as a flat array - most notably flattens
* layer groups, which are nested.
*/
let result: (TiledTileLayerJSON | TiledObjectLayerJSON)[] = [];
for (const layer of root) {
if (layer.type === "group") {
result = [...result, ...this._getAllLayersHelper(layer.layers)];
} else {
result.push(layer);
}
}
return result;
}
getAllObjectLayers(): TiledObjectLayerJSON[] {
const allLayers = this.getAllLayers();
const objectLayers: TiledObjectLayerJSON[] = [];
for (const layer of allLayers) {
if (layer.type === "objectgroup") {
objectLayers.push(layer);
}
}
return objectLayers;
}
private loadTileLayers(): { [layerName: string]: TilemapLayer } {
const result: { [layerName: string]: TilemapLayer } = {};
const layers = this.getAllLayers();
for (const layer of layers) {
if (layer.type === "tilelayer") {
const grid = this.loadTiles(layer);
type: "tiles",
grid,
offset: new Vector2(layer.offsetx, layer.offsety),
};
} else if (layer.type === "objectgroup") {
result[layer.name] = this.loadRectLayer(layer);
}
}
return result;
}
loadRectLayer(layer: TiledObjectLayerJSON): TilemapLayer {
const objects = layer.objects;
const rects: TilemapRegion[] = [];
for (const obj of objects) {
if (!obj.gid) {
rects.push({
rect: new Rect({
x: obj.x,
y: obj.y,
width: obj.width,
height: obj.height,
}),
properties: TiledTilemap.ParseTiledProperties(obj.properties) || {},
});
}
}
return {
rects: rects,
offset: Vector2.Zero,
};
}
private loadTiles(layer: TiledTileLayerJSON): Grid<Tile> {
const result = new Grid<Tile>();
const { chunks } = layer;
// TODO: If the world gets very large, loading in all chunks like this might
// not be the best idea - lazy loading could be better.
for (const chunk of chunks) {
for (let i = 0; i < chunk.data.length; i++) {
const gid = chunk.data[i];
const relTileY = Math.floor(i / chunk.width);
if (isNaN(layer.offsetx)) layer.offsetx = 0; // TODO this is indicative of a tmx tileset embed, which we dont support yet
if (isNaN(layer.offsety)) layer.offsety = 0;
const offsetX = layer.offsetx / this._tileWidth;
const offsetY = layer.offsety / this._tileHeight;
throw new Error("AAAAAAAAAAAAAAAAAAAAAAAAA");
}
const absTileX = relTileX + chunk.x + offsetX;
const absTileY = relTileY + chunk.y + offsetY;
const { spritesheet, tileProperties } = this.gidInfo(gid);
// TODO: Merge instance properties and tileset properties...
result.set(absTileX, absTileY, {
tileProperties: tileProperties,
});
}
}
return result;
}
tileProperties: { [key: string]: unknown };
} {
if (gid >= gidStart && gid < gidEnd) {
const normalizedGid = gid - gidStart;
const y = Math.floor(normalizedGid / tilesWide);
const spritesheet = {
imageUrlRelativeToGame,
spritesheetx: x,
spritesheety: y,
tilewidth,
tileheight,
tileProperties: tiles,
};
let tileProperties: { [key: string]: unknown } = {};
if (tiles) {
if (matchedTileInfo && matchedTileInfo.properties) {
for (const { name, value } of matchedTileInfo.properties) {
tileProperties[name] = value;
}
}
}
return {
spritesheet,
tileProperties,
};
}
}
throw new Error("gid out of range. ask gabby what to do?!?");
}
public getTilesAtAbsolutePosition(x: number, y: number): Tile[] {
return this.getLayerNames()
}
const tileHeight = this._tileHeight;
const layer = this._layers[layerName];
if (layer.type === "tiles") {
return layer.grid.get(
Math.floor(x / tileWidth),
Math.floor(y / tileHeight)
);
}
return null;
}
getCollidersInRegion(region: Rect): Rect[] {
}
getCollidersInRegionForLayer(region: Rect, layerName: string): RectGroup {
const lowX = Math.floor(region.x / this._tileWidth);
const lowY = Math.floor(region.y / this._tileHeight);
const highY = Math.ceil(region.bottom / this._tileHeight);
let colliders: Rect[] = [];
for (let x = lowX; x <= highX; x++) {
for (let y = lowY; y <= highY; y++) {
const tile = this.getTileAtAbsolutePositionForLayer(
y * this._tileHeight,
layerName
);
if (tile && tile.isCollider) {
}
}
}
return new RectGroup(colliders);
}
}
colliders.push(
new Rect({
x: x * this._tileWidth,
y: y * this._tileHeight,
width: this._tileWidth,
height: this._tileHeight,
})
);
x * this._tileWidth,
const highX = Math.ceil(region.right / this._tileWidth);
return Util.FlattenByOne(
this.getLayerNames().map((layerName) =>
this.getCollidersInRegionForLayer(region, layerName).getRects()
)
);
public getTileAtAbsolutePositionForLayer(
x: number,
y: number,
layerName: string
): Tile | null {
const tileWidth = this._tileWidth;
.map((layerName) =>
this.getTileAtAbsolutePositionForLayer(x, y, layerName)
)
.filter((x) => x) as Tile[];
const matchedTileInfo = tiles.find(
(tile) => gid === gidStart + tile.id
);
const x = normalizedGid % tilesWide;
const tilesWide = imagewidth / tilewidth;
for (const {
gidStart,
gidEnd,
imageUrlRelativeToGame,
imagewidth,
tilewidth,
tileheight,
tiles,
} of this._tilesets) {
gidInfo(
gid: number
): {
spritesheet: SpritesheetTile;
gid: gid,
x: absTileX * this._tileWidth + layer.offsetx,
y: absTileY * this._tileHeight + layer.offsety,
tile: spritesheet,
isCollider: this.isGidCollider(gid),
if (
offsetX !== Math.floor(offsetX) ||
offsetY !== Math.floor(offsetY)
) {
const relTileX = i % chunk.width;
if (gid === 0) {
continue;
} // empty
if (gid > 200000) {
throw new Error("???");
} // tiled bug? (TODO: does this actually happen?)
type: "rects",
result[layer.name] = {
private _getAllLayersHelper(
root: TiledLayerTypes[]
): (TiledTileLayerJSON | TiledObjectLayerJSON)[] {
gidEnd: firstgid + tileCountInTileset,
for (const {
image: imageUrlRelativeToTilemap,
name,
firstgid,
imageheight,
imagewidth,
tileheight,
tilewidth,
tiles,
} of json.tilesets) {
const tileCountInTileset =
(imageheight * imagewidth) / (tileheight * tilewidth);
const imageUrlRelativeToGame = new URL(
pathToTilemap + "/" + imageUrlRelativeToTilemap,
"http://a"
).href.slice("http://a".length + 1); // slice off the initial / too
this._tileWidth = this._data.tilewidth;
this._tileHeight = this._data.tileheight;
this._gidHasCollision = this.buildCollisionInfoForTiles();
this._tilesets = this.loadTilesets(pathToTilemap, this._data);
this._layers = this.loadTileLayers();
constructor(props: { data: TiledJSON; pathToTilemap: string }) {
private _layers: { [tilesetName: string]: TilemapLayer };
private _tilesets: Tileset[];
private _data: TiledJSON;
private _tileWidth: number;
type: "rects";
rects: TilemapRegion[];
offset: Vector2;
};
export type TilemapLayer =
| {
type: "tiles";
grid: Grid<Tile>;
offset: Vector2;
}
};
rect: Rect;
import {
TiledJSON,
Tileset,
Tile,
TiledLayerTypes,
TiledTileLayerJSON,
TiledObjectLayerJSON,
SpritesheetTile,
} from "./tilemap_types";
// import { Assets } from '../../game/assets';
export type MapLayer = {
layerName: string;
entity: Entity;
objectLayer: boolean;
};
// TODO: Handle the weird new file format where tilesets link to ANOTHER json file
export class TiledTilemap {
private _tileWidth: number;
private _tileHeight: number;
private _renderer: Renderer;
private _objects: TiledTilemapObjects;
private _assets: TypesafeLoader<any>;
_data: TilemapData;
// this is required to calculate the relative paths of the tileset images.
json: TiledJSON;
renderer: Renderer;
pathToTilemap: string;
customObjects: TilemapCustomObjects[];
assets: TypesafeLoader<any>;
}) {
this._data = new TilemapData({ data, pathToTilemap });
this._renderer = renderer;
this._tileWidth = this._data.getTileWidth();
this._tileHeight = this._data.getTileHeight();
this._assets = assets;
this._objects = new TiledTilemapObjects({
layers: this._data.getAllObjectLayers(),
customObjects: customObjects,
map: this,
assets: null as any,
});
}
/**
* Load all the regions on a specified layer.
*/
loadRegionLayer(layerName: string): TilemapRegion[] {
const layer = this._data.getLayer(layerName);
if (layer.type === "rects") {
return layer.rects;
}
throw new Error("Not a rect layer");
}
private cache: { [key: string]: MapLayer[] } = {};
public loadLayersInRectCached(region: Rect): MapLayer[] {
// for (const k of Object.keys(this.cache)) {
// const obj = this.cache[k]
// for (const l of obj) {
// if (l.entity.texture) {
// l.entity.texture.destroy();
// }
// l.entity.parent?.removeChild(l.entity);
// }
// }
// this.cache = {};
const hash = region.toString();
if (!this.cache[hash]) {
this.cache[hash] = this.loadLayersInRect(region);
}
return this.cache[hash];
}
private loadLayersInRect(region: Rect): MapLayer[] {
let tileLayers: MapLayer[] = [];
// Load tile layers
for (const layerName of this._data.getLayerNames()) {
const layer = this._data.getLayer(layerName);
const renderTexture = RenderTexture.create({
width: Math.ceil(region.width),
height: Math.ceil(region.height),
});
const tileWidth = this._tileWidth;
const tileHeight = this._tileHeight;
const iStart = region.x / tileWidth;
const jStart = region.y / tileHeight;
if (iStart !== Math.floor(iStart) || jStart !== Math.floor(jStart)) {
}
for (let i = region.x / tileWidth; i < region.right / tileWidth; i++) {
const tile = layer.grid.get(i, j);
const sprite = new Sprite(tex);
// We have to offset here because we'd be drawing outside of the
// bounds of the RenderTexture otherwise.
this._renderer.render(sprite, renderTexture, false);
}
}
const layerEntity = new Entity({
texture: renderTexture,
name: layerName,
});
layerEntity.x = region.x;
layerEntity.y = region.y;
layerEntity.width = region.width;
layerEntity.height = region.height;
tileLayers.push({
entity: layerEntity,
layerName,
objectLayer: false,
});
}
// Load object layers
// TODO: only load objects in this region - not the entire layer!!!
const objectLayers = this._objects.loadObjectLayers();
for (const objectLayer of objectLayers) {
objectLayer.entity.zIndex = 5; // TODO
}
for (const tileLayer of tileLayers) {
tileLayer.entity.zIndex = 0;
}
tileLayers = [...tileLayers, ...objectLayers];
return tileLayers;
}
turnOffAllObjects() {
this._objects.turnOffAllObjects();
}
getAllObjects(): ObjectInfo[] {
return this._objects.getAllObjects();
}
const result: { [key: string]: string } = {};
if (properties === undefined) {
return {};
}
for (const obj of properties) {
result[obj.name] = obj.value;
}
return result;
}
}
public static ParseTiledProperties(
properties: { name: string; type: string; value: string }[] | undefined
): { [key: string]: string } {
sprite.x = tile.x - region.x - layer.offset.x;
sprite.y = tile.y - region.y - layer.offset.y;
const tex = TextureCache.GetTextureForTile({
assets: this._assets,
tile,
});
if (!tile) {
continue;
}
for (
let j = region.y / tileHeight;
j < region.bottom / tileHeight;
j++
) {
throw new Error(
"x and y of passed in region aren't divisible by tileWidth/height"
);
if (layer.type !== "tiles") {
continue;
}
constructor({
json: data,
renderer,
pathToTilemap,
customObjects,
assets,
}: {
import { TypesafeLoader } from "../typesafe_loader";
import { Sprite, Renderer, RenderTexture } from "pixi.js";
import { Rect } from "../geometry/rect";
import { TiledJSON } from "./tilemap_types";
import { TextureCache } from "../texture_cache";
import { Entity } from "../entity";
import {
TiledTilemapObjects,
TilemapCustomObjects,
ObjectInfo,
} from "./tilemap_objects";
import { TilemapData, TilemapRegion } from "./tilemap_data";
// import { AssetsToLoad } from '../game/assets';
const AssetsToLoad = {} as { never: any };
type AnimationResource = {
paths: string[];
};
type NormalResource = {
type: "Image" | "Audio" | "TileMap" | "TileWorld" | "Spritesheet";
path: string;
};
type IndividualResourceObj = AnimationResource | NormalResource;
* TypeSafe loader is intended to be a wrapper around PIXI.Loader which gives a
* type-checked getResource() check.
*/
export class TypesafeLoader<Resources extends AllResourcesType> {
loader: Loader;
loadComplete: boolean;
loadCompleteCallbacks: (() => void)[];
constructor(resourceNames: Resources) {
this.loadCompleteCallbacks = [];
this.loader = new Loader();
this.loadComplete = false;
this.startStageOneLoading(resourceNames);
}
// Stage 1: Load all assets in resources.ts
private startStageOneLoading = (resources: Resources) => {
for (const key of Object.keys(resources)) {
const resource = resources[key];
if (resource.type === "Animation") {
for (const path of resource.paths) {
this.loader.add(path);
}
} else {
this.loader.add(resource.path);
}
}
this.loader.load(this.startStageTwoLoading);
// Stage 2: Load all assets required by tilemaps - mostly tilesets, I hope!.
private startStageTwoLoading = () => {
let allTilemapDependencyPaths: string[] = [];
for (const resource of Object.keys(AssetsToLoad)) {
const castedResource = resource as keyof typeof AssetsToLoad;
if (AssetsToLoad[castedResource].type === "TileMap") {
});
allTilemapDependencyPaths = allTilemapDependencyPaths.concat(
);
}
}
for (const tilemapDependencyPath of allTilemapDependencyPaths) {
if (!this.loader.resources[tilemapDependencyPath]) {
this.loader.add(tilemapDependencyPath);
}
}
this.loader.load(this.finishLoading);
const resource = AssetsToLoad[resourceName] as IndividualResourceObj;
if (resource.type === "Audio") {
return new Audio(resource.path) as any;
} else if (resource.type === "Animation") {
} else if (resource.type === "Image") {
return this.loader.resources[resource.path].texture as any;
} else if (resource.type === "Spritesheet") {
throw new Error("Unhandled");
} else if (resource.type === "TileMap") {
return this.loader.resources[resource.path].data;
} else if (resource.type === "TileWorld") {
return this.loader.resources[resource.path].data;
}
throw new Error("AAAAAA");
}
private finishLoading = () => {
this.loadComplete = true;
for (const callback of this.loadCompleteCallbacks) {
callback();
}
this.loadCompleteCallbacks = [];
onLoadComplete(callback: () => void) {
if (this.loadComplete) {
setTimeout(() => {
callback();
}, 0);
} else {
this.loadCompleteCallbacks.push(callback);
}
}
}
};
return resource.paths.map(
(path) => this.loader.resources[path].texture
) as any;
getResource<T extends keyof typeof AssetsToLoad>(
resourceName: T
): ResourceReturn<typeof AssetsToLoad[T]["type"]> {
};
tilemapData
.getTilesets()
.map((tileset) => tileset.imageUrlRelativeToGame)
const tilemapData = new TilemapData({
data: this.getResource(castedResource) as TiledJSON,
pathToTilemap,
const pathToTilemap = resource.substring(0, resource.lastIndexOf("/"));
};
/**
export type AllResourcesType = { [key: string]: IndividualResourceObj };
type ResourceReturn<T extends string> = T extends "Image"
? Texture
: T extends "Audio"
? HTMLAudioElement
: T extends "TileMap"
? TiledJSON
: T extends "TileWorld"
? object
: T extends "Spritesheet"
? unknown
: T extends "Animation"
? Texture[]
: never;
type: "Animation";
import { TilemapData } from "./tilemap/tilemap_data";
import { TiledJSON } from "./tilemap/tilemap_types";
import { Loader, Texture } from "pixi.js";
let lastUsedId = 0;
export const getUniqueID = () => {
return lastUsedId++;
};
export class Util {
static MinBy<T>(list: T[], fn: (T: T) => number): T | undefined {
let lowestValue: number | undefined = undefined;
for (const item of list) {
const value = fn(item);
if (lowestValue === undefined || value < lowestValue) {
lowestT = item;
lowestValue = value;
}
}
return lowestT;
}
static MaxBy<T>(list: T[], fn: (T: T) => number): T | undefined {
let highestValue: number | undefined = undefined;
for (const item of list) {
const value = fn(item);
if (highestValue === undefined || 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) => {
});
}
public static ReplaceAll(
mapObj: { [key: string]: string }
): string {
});
}
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 = [
}
public static FlattenByOne<T>(arr: T[][]): T[] {
for (const obj of arr) {
}
}
return string + intersperse + character.repeat(length - string.length);
}
}
public static PadString(
string: string,
length: number,
intersperse = "",
character = " "
) {
return result;
result = result.concat(obj);
let result: T[] = [];
return `${monthName} ${d.getDate()}, ${("00" + d.getHours()).substr(-2)}:${(
"00" + d.getMinutes()
).substr(-2)}:${("00" + d.getSeconds()).substr(-2)}`;
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
][d.getMonth()];
};
};
return str.replace(re, (matched) => {
return mapObj[matched.toLowerCase()];
const re = new RegExp(Object.keys(mapObj).join("|"), "gi");
str: string,
return key(a) - key(b);
let highestT: T | undefined = undefined;
let lowestT: T | undefined = undefined;