Files
fledgling/src/engine/datatypes.ts
2025-02-22 22:51:21 -08:00

388 lines
8.6 KiB
TypeScript

export interface IGame {
update(): void;
draw(): void;
}
export class Color {
readonly r: number;
readonly g: number;
readonly b: number;
readonly a: number;
constructor(r: number, g: number, b: number, a?: number) {
this.r = r;
this.g = g;
this.b = b;
this.a = a ?? 255;
}
static parseHexCode(hexCode: string) {
const regex1 =
/#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?/;
const regex2 =
/#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})?/;
let result = regex1.exec(hexCode) ?? regex2.exec(hexCode);
if (result == null) {
throw `could not parse color: ${hexCode}`;
}
let parseGroup = (s: string | undefined): number => {
if (s === undefined) {
return 255;
}
if (s.length == 1) {
return 17 * parseInt(s, 16);
}
return parseInt(s, 16);
};
return new Color(
parseGroup(result[1]),
parseGroup(result[2]),
parseGroup(result[3]),
parseGroup(result[4]),
);
}
toStyle(): string {
return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a / 255.0})`;
}
}
export class Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
toString(): string {
return `${this.x},${this.y}`;
}
offset(other: Point | Size): Point {
if (other instanceof Point) {
return new Point(this.x + other.x, this.y + other.y);
}
return new Point(this.x + other.w, this.y + other.h);
}
negate() {
return new Point(-this.x, -this.y);
}
equals(other: Point): boolean {
return this.x == other.x && this.y == other.y;
}
scale(other: Point | Size) {
if (other instanceof Point) {
return new Point(this.x * other.x, this.y * other.y);
}
return new Point(this.x * other.w, this.y * other.h);
}
unscale(other: Point | Size) {
if (other instanceof Point) {
return new Point(this.x / other.x, this.y / other.y);
}
return new Point(this.x / other.w, this.y / other.h);
}
subtract(top: Point): Size {
return new Size(this.x - top.x, this.y - top.y);
}
manhattan(other: Point) {
return Math.abs(this.x - other.x) + Math.abs(this.y - other.y);
}
snap(x: number, y: number) {
return new Point(
lerp(x, Math.floor(this.x), Math.ceil(this.x)),
lerp(y, Math.floor(this.y), Math.ceil(this.y)),
);
}
distance(other: Point) {
let dx = other.x - this.x;
let dy = other.y - this.y;
return Math.sqrt(dx * dx + dy * dy);
}
neighbors(): Point[] {
return [
new Point(this.x, this.y - 1),
new Point(this.x - 1, this.y),
new Point(this.x, this.y + 1),
new Point(this.x + 1, this.y),
];
}
}
export class Size {
readonly w: number;
readonly h: number;
constructor(w: number, h: number) {
this.w = w;
this.h = h;
}
add(other: Size) {
return new Size(this.w + other.w, this.h + other.h);
}
equals(other: Size) {
return this.w == other.w && this.h == other.h;
}
toString(): string {
return `${this.w}x${this.h}`;
}
}
export class Circle {
readonly center: Point;
readonly radius: number;
constructor(center: Point, radius: number) {
this.center = center;
this.radius = radius;
}
getContactWithRect(rect: Rect): Point | null {
// port: https://www.jeffreythompson.org/collision-detection/circle-rect.php
let cx = this.center.x;
let cy = this.center.y;
let testX = this.center.x;
let testY = this.center.y;
let rx = rect.top.x;
let ry = rect.top.y;
let rw = rect.size.w;
let rh = rect.size.h;
if (cx < rx) {
testX = rx;
} else if (cx > rx + rw) {
testX = rx + rw;
}
if (cy < ry) {
testY = ry;
} else if (cy > ry + rh) {
testY = ry + rh;
}
let distX = cx - testX;
let distY = cy - testY;
let sqDistance = distX * distX + distY * distY;
if (sqDistance <= this.radius * this.radius) {
return new Point(testX, testY);
}
return null;
}
overlappedCells(size: Size): Rect[] {
let meAsRect = new Rect(
this.center.offset(new Point(-this.radius, -this.radius)),
new Size(this.radius * 2, this.radius * 2),
);
let all: Rect[] = [];
for (let cell of meAsRect.overlappedCells(size).values()) {
if (this.getContactWithRect(cell) != null) {
all.push(cell);
}
}
return all;
}
}
export class Rect {
readonly top: Point;
readonly size: Size;
constructor(top: Point, size: Size) {
this.top = top;
this.size = size;
}
toString(): string {
return `Rect(${this.top},${this.size})`;
}
offset(offset: Point) {
return new Rect(this.top.offset(offset), this.size);
}
contains(other: Point) {
return (
other.x >= this.top.x &&
other.y >= this.top.y &&
other.x < this.top.x + this.size.w &&
other.y < this.top.y + this.size.h
);
}
overlappedCells(size: Size) {
let x0 = this.top.x;
let y0 = this.top.y;
let x1 = x0 + this.size.w;
let y1 = y0 + this.size.w;
let cx0 = Math.floor(x0 / size.w);
let cy0 = Math.floor(y0 / size.h);
let cx1 = Math.ceil(x1 / size.w);
let cy1 = Math.ceil(y1 / size.h);
let cells = [];
for (let cy = cy0; cy < cy1; cy++) {
for (let cx = cx0; cx < cx1; cx++) {
let px0 = cx * size.w;
let py0 = cy * size.h;
cells.push(new Rect(new Point(px0, py0), size));
}
}
return cells;
}
overlaps(other: Rect) {
let ax0 = this.top.x;
let ay0 = this.top.y;
let ax1 = ax0 + this.size.w;
let ay1 = ay0 + this.size.h;
let bx0 = other.top.x;
let by0 = other.top.y;
let bx1 = bx0 + other.size.w;
let by1 = by0 + other.size.h;
let noOverlap = ax0 > bx1 || bx0 > ax1 || ay0 > by1 || by0 > ay1;
return !noOverlap;
}
}
export class Grid<T> {
readonly size: Size;
#data: T[][];
constructor(size: Size, cbDefault: (xy: Point) => T) {
this.size = size;
this.#data = [];
for (let y = 0; y < size.h; y++) {
let row = [];
for (let x = 0; x < size.w; x++) {
row.push(cbDefault(new Point(x, y)));
}
this.#data.push(row);
}
}
static createGridFromMultilineString(multiline: string): Grid<string> {
let lines = [];
for (let line of multiline.split("\n")) {
let trimmedLine = line.trim();
if (trimmedLine == "") {
continue;
}
lines.push(trimmedLine);
}
return this.createGridFromStringArray(lines);
}
static createGridFromStringArray(ary: Array<string>): Grid<string> {
let w = 0;
let h = ary.length;
for (let i = 0; i < h - 1; i++) {
let w1 = ary[i].length;
let w2 = ary[i + 1].length;
if (w1 != w2) {
throw `createGridFromStringArray: must be grid-shaped, got ${ary}`;
}
w = w1;
}
return new Grid(new Size(w, h), (xy) => {
return ary[xy.y].charAt(xy.x);
});
}
static createGridFromJaggedArray<T>(ary: Array<Array<T>>): Grid<T> {
let w = 0;
let h = ary.length;
for (let i = 0; i < h - 1; i++) {
let w1 = ary[i].length;
let w2 = ary[i + 1].length;
if (w1 != w2) {
throw `createGridFromJaggedArray: must be grid-shaped, got ${ary}`;
}
w = w1;
}
return new Grid(new Size(w, h), (xy) => {
return ary[xy.y][xy.x];
});
}
map<T2>(cbCell: (content: T, position: Point) => T2) {
return new Grid(this.size, (xy) => cbCell(this.get(xy), xy));
}
#invalidPosition(position: Point): boolean {
return (
position.x < 0 ||
position.x >= this.size.w ||
Math.floor(position.x) != position.x ||
position.y < 0 ||
position.y >= this.size.h ||
Math.floor(position.y) != position.y
);
}
#checkPosition(position: Point) {
if (this.#invalidPosition(position)) {
throw new Error(`invalid position for ${this.size}: ${position}`);
}
}
maybeGet(position: Point): T | null {
if (this.#invalidPosition(position)) {
return null;
}
return this.#data[position.y][position.x];
}
get(position: Point): T {
this.#checkPosition(position);
return this.#data[position.y][position.x];
}
set(position: Point, value: T) {
this.#checkPosition(position);
this.#data[position.y][position.x] = value;
}
}
export enum AlignX {
Left = 0,
Center = 1,
Right = 2,
}
export enum AlignY {
Top = 0,
Middle = 1,
Bottom = 2,
}
export function lerp(amt: number, lo: number, hi: number) {
if (amt <= 0) {
return lo;
}
if (amt >= 1) {
return hi;
}
return lo + (hi - lo) * amt;
}