Files
pixel.horse/src/ts/common/utils.ts
T
2019-08-28 21:15:02 -07:00

542 lines
14 KiB
TypeScript

import { HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Point, Rect, Entity, Dict } from './interfaces';
import { tileWidth, tileHeight, SECOND, MINUTE, HOUR, DAY } from './constants';
import { ACCESS_ERROR, NOT_FOUND_ERROR, OFFLINE_ERROR, PROTECTION_ERROR } from './errors';
// enum
export function invalidEnum(value: never) {
if (DEVELOPMENT) {
throw new Error(`Invalid enum value: ${value}`);
}
}
export function invalidEnumReturn<T>(value: never, ret: T): T {
if (DEVELOPMENT && !TESTS) {
throw new Error(`Invalid enum value: ${value}`);
}
return ret;
}
// date
export function fromDate(date: Date, duration: number): Date {
date.setTime(date.getTime() + duration);
return date;
}
export function fromNow(duration: number): Date {
return fromDate(new Date(), duration);
}
export function compareDates(a?: Date, b?: Date) {
return a ? (b ? a.getTime() - b.getTime() : 1) : (b ? -1 : 0);
}
export function maxDate(a?: Date, b?: Date) {
return (compareDates(a, b) > 0 ? a : b) || a || b;
}
export function minDate(a?: Date, b?: Date) {
return (compareDates(a, b) < 0 ? a : b) || a || b;
}
export function formatDuration(duration: number) {
const s = Math.floor(duration / SECOND) % 60;
const m = Math.floor(duration / MINUTE) % 60;
const h = Math.floor(duration / HOUR) % 24;
const d = Math.floor(duration / DAY);
if (d > 0) {
return h ? `${d}d ${h}h` : `${d}d`;
} else if (h > 0) {
return m ? `${h}h ${m}m` : `${h}h`;
} else if (m > 0) {
return s ? `${m}m ${s}s` : `${m}m`;
} else {
return `${s}s`;
}
}
export function formatISODate(date: Date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
}
export function parseISODate(value: string) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
let day = 0;
let month = 0;
let year = 0;
if (match) {
year = parseInt(match[1], 10);
month = parseInt(match[2], 10);
day = parseInt(match[3], 10);
}
return { day, month, year };
}
export function createValidBirthDate(day: number, month: number, year: number) {
const date = new Date(0);
const currentYear = (new Date()).getFullYear();
date.setFullYear(year, month - 1, day);
if (
date.getFullYear() === year && date.getMonth() === (month - 1) && date.getDate() === day &&
year >= (currentYear - 120) && year < currentYear
) {
return date;
} else {
return undefined;
}
}
// color
export function parseSpriteColor(str: string): number {
return str === '0' ? 0 : (str.length === 6 ? (((parseInt(str, 16) << 8) | 0xff) >>> 0) : (parseInt(str, 16) >>> 0));
}
// numbers
export function clamp(value: number, min: number, max: number): number {
return value > min ? (value < max ? value : max) : min;
}
export function lerp(a: number, b: number, t: number) {
return a + t * (b - a);
}
export function normalize(x: number, y: number): Point {
const d = Math.sqrt(x * x + y * y);
return { x: x / d, y: y / d };
}
export function computeCRC(colors: Uint32Array): number {
let crc = 0;
for (let i = 0; i < colors.length; i++) {
crc ^= colors[i];
for (let j = 0; j < 8; j++) {
crc = (crc & 1) ? ((crc >>> 1) ^ 0x82f63b78) : (crc >>> 1);
}
}
return crc >>> 0;
}
export function computeFriendsCRC(friends: string[]) {
if (!friends.length) {
return 0;
}
friends.sort();
const data = new Uint32Array(friends.length * 3);
for (let i = 0; i < friends.length; i++) {
const id = friends[i];
data[i * 3] = parseInt(id.substr(0, 8), 16);
data[i * 3 + 1] = parseInt(id.substr(8, 8), 16);
data[i * 3 + 2] = parseInt(id.substr(16, 8), 16);
}
return computeCRC(data);
}
export function lerpColor(a: number[] | Float32Array, b: number[] | Float32Array, t: number) {
a[0] = t * b[0] + (1 - t) * a[0];
a[1] = t * b[1] + (1 - t) * a[1];
a[2] = t * b[2] + (1 - t) * a[2];
a[3] = t * b[3] + (1 - t) * a[3];
}
// common
export function toInt(value: any): number {
return value | 0;
}
export function dispose<T extends { dispose(): void; }>(obj: T | undefined): undefined {
obj && obj.dispose();
return undefined;
}
export function cloneDeep<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
// enums
export function hasFlag(value: number | undefined, flag: number): boolean {
return (value! & flag) === flag;
}
export function setFlag(value: number | undefined, flag: number, on: boolean): number {
return (value! & ~flag) | (on ? flag : 0);
}
export function flagsToString(value: number, flags: { value: number; name: string; }[], none = 'None') {
return flags
.filter(flag => hasFlag(value, flag.value))
.map(flag => flag.name).join(' | ') || none;
}
// collections
export function includes<T>(array: T[] | undefined, item: T): boolean {
return array !== undefined && array.indexOf(item) !== -1;
}
export function array<T>(size: number, defaultValue: T) {
const result: T[] = [];
for (let i = 0; i < size; i++) {
result.push(defaultValue);
}
return result;
}
export function repeat<T>(count: number, ...values: T[]): T[] {
const result: T[] = [];
for (let i = 0; i < count; i++) {
result.push(...values);
}
return result;
}
export function times<T>(count: number, action: (index: number) => T) {
const result: T[] = [];
for (let i = 0; i < count; i++) {
result.push(action(i));
}
return result;
}
export function last<T>(array: T[]): T | undefined {
return array.length > 0 ? array[array.length - 1] : undefined;
}
export function flatten<T>(arrays: T[][]): T[] {
return ([] as T[]).concat(...arrays);
}
export function at<T>(items: T[], index: any): T | undefined {
return items[clamp(index | 0, 0, items.length - 1)];
}
export function att<T>(items: T[] | null | undefined, index: any): T | undefined {
return items ? items[clamp(index | 0, 0, items.length - 1)] : undefined;
}
export function findById<U, T extends { id: U }>(items: T[], id: U): T | undefined {
for (let i = 0; i < items.length; i++) {
if (items[i].id === id) {
return items[i];
}
}
return undefined;
}
export function findIndexById<U, T extends { id: U }>(items: T[], id: U): number {
for (let i = 0; i < items.length; i++) {
if (items[i].id === id) {
return i;
}
}
return -1;
}
export function removeItem<T>(items: T[], item: T): boolean {
const index = items.indexOf(item);
if (index !== -1) {
items.splice(index, 1);
return true;
} else {
return false;
}
}
export function removeItemFast<T>(items: T[], item: T): boolean {
const index = items.indexOf(item);
if (index !== -1) {
items[index] = items[items.length - 1];
items.pop();
return true;
} else {
return false;
}
}
export function removeById<U, T extends { id: U }>(items: T[], id: U): T | undefined {
const index = findIndexById(items, id);
if (index !== -1) {
const item = items[index];
items.splice(index, 1);
return item;
} else {
return undefined;
}
}
export function arraysEqual<T>(a: T[], b: T[]): boolean {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
export function pushUniq<T>(array: T[], item: T) {
const index = array.indexOf(item);
if (index === -1) {
array.push(item);
return array.length;
} else {
return index + 1;
}
}
export function createPlainMap<T>(values: Dict<T>): Dict<T> {
return Object.keys(values).reduce((obj: Dict<T>, key: string) => (obj[key] = values[key], obj), Object.create(null));
}
// rects / points
export function point(x: number, y: number): Point {
return { x, y };
}
export function contains(x: number, y: number, bounds: Rect, point: Point): boolean {
const bx = bounds.x / tileWidth + x;
const by = bounds.y / tileHeight + y;
const bw = bounds.w / tileWidth;
const bh = bounds.h / tileHeight;
return point.x > bx && point.x < bx + bw && point.y > by && point.y < by + bh;
}
export function containsPoint(dx: number, dy: number, rect: Rect, px: number, py: number): boolean {
return pointInXYWH(px, py, rect.x + dx, rect.y + dy, rect.w, rect.h);
}
export function containsPointWitBorder(dx: number, dy: number, rect: Rect, px: number, py: number, border: number): boolean {
return pointInXYWH(px, py, rect.x + dx - border, rect.y + dy - border, rect.w + border * 2, rect.h + border * 2);
}
export function pointInRect(x: number, y: number, rect: Rect) {
return x > rect.x && x < rect.x + rect.w && y > rect.y && y < rect.y + rect.h;
}
export function pointInXYWH(px: number, py: number, rx: number, ry: number, rw: number, rh: number) {
return px > rx && px < rx + rw && py > ry && py < ry + rh;
}
export function randomPoint({ x, y, w, h }: Rect): Point {
return {
x: x + w * Math.random(),
y: y + h * Math.random(),
};
}
export function lengthOfXY(dx: number, dy: number): number {
return Math.sqrt(dx * dx + dy * dy);
}
export function distanceXY(ax: number, ay: number, bx: number, by: number): number {
return lengthOfXY(ax - bx, ay - by);
}
export function distanceSquaredXY(ax: number, ay: number, bx: number, by: number): number {
const dx = ax - bx;
const dy = ay - by;
return dx * dx + dy * dy;
}
export function distance(a: Point, b: Point): number {
return distanceXY(a.x, a.y, b.x, b.y);
}
export function entitiesIntersect(a: Entity, b: Entity): boolean {
const aBounds = a.bounds;
const bBounds = b.bounds;
if (!aBounds || !bBounds) {
return false;
}
const ax = a.x * tileWidth + aBounds.x;
const ay = a.y * tileHeight + aBounds.y;
const bx = b.x * tileWidth + bBounds.x;
const by = b.y * tileHeight + bBounds.y;
return intersect(ax, ay, aBounds.w, aBounds.h, bx, by, bBounds.w, bBounds.h);
}
export function collidersIntersect(ax: number, ay: number, a: Rect, bx: number, by: number, b: Rect): boolean {
const axmin = Math.floor((ax + a.x) * tileWidth) | 0;
const axmax = Math.ceil((ax + a.x + a.w) * tileWidth) | 0;
const aymin = Math.floor((ay + a.y) * tileHeight) | 0;
const aymax = Math.ceil((ay + a.y + a.h) * tileHeight) | 0;
const bxmin = Math.floor((bx + b.x) * tileWidth) | 0;
const bxmax = Math.ceil((bx + b.x + b.w) * tileWidth) | 0;
const bymin = Math.floor((by + b.y) * tileHeight) | 0;
const bymax = Math.ceil((by + b.y + b.h) * tileHeight) | 0;
return axmin < bxmax && axmax > bxmin && aymin < bymax && aymax > bymin;
}
export function boundsIntersect(
ax: number, ay: number, a: Rect | undefined, bx: number, by: number, b: Rect | undefined
): boolean {
return !!(a && b && intersect(
ax * tileWidth + a.x, ay * tileHeight + a.y, a.w, a.h,
bx * tileWidth + b.x, by * tileHeight + b.y, b.w, b.h));
}
export function intersect(
ax: number, ay: number, aw: number, ah: number, bx: number, by: number, bw: number, bh: number
): boolean {
return ax <= (bx + bw) && (ax + aw) >= bx && ay <= (by + bh) && (ay + ah) >= by;
}
// requests
export type RequestError = Error & { status?: number; text?: string; };
export function createError(status: number, data: string | { error: string; }): Error {
if (status > 500 && status < 600) {
return new Error(PROTECTION_ERROR);
// } else if (status === 400) {
// return new Error('Bad Request');
} else if (status === 403) {
return new Error(ACCESS_ERROR);
} else if (status === 404) {
return new Error(NOT_FOUND_ERROR);
} else if (typeof data === 'string') {
return new Error(data || OFFLINE_ERROR);
} else {
return new Error((data && data.error) || OFFLINE_ERROR);
}
}
export function delay(timeout: number) {
return new Promise<void>(resolve => setTimeout(resolve, timeout));
}
export function observableToPromise<T>(observable: Observable<T>) {
return observable.toPromise()
.catch(({ status, error }: HttpErrorResponse) => {
const text = error && error.text;
try {
error = JSON.parse(error);
} catch { }
const e: RequestError = createError(status || 0, error);
e.status = status;
e.text = text;
throw e;
});
}
// other
function setTransformDefault(element: HTMLElement | undefined, transform: string) {
if (element) {
element.style.transform = transform;
}
}
function setTransformSafari(element: HTMLElement | undefined, transform: string) {
if (element) {
(element.style as any).webkitTransform = transform;
}
}
export const setTransform = (typeof document !== 'undefined' && 'transform' in document.body.style) ?
setTransformDefault : setTransformSafari;
export class ObjectCache<T> {
private cache: T[] = [];
constructor(private limit: number, private ctor: () => T) {
}
get(): T {
return this.cache.pop() || this.ctor();
}
put(item: T) {
if (this.cache.length < this.limit) {
this.cache.push(item);
}
}
}
export function bitmask(data: Uint8Array, key: number) {
if (key) {
for (let i = 0; i < data.length; i++) {
data[i] = data[i] ^ key;
}
}
return data;
}
export function isCommand(text: string) {
return /^\//.test(text);
}
export function processCommand(text: string) {
text = text.substr(1);
const space = text.indexOf(' ');
const command = (space === -1 ? text : text.substr(0, space)).trim() as string | undefined;
const args = space === -1 ? '' : text.substr(space + 1).trim();
return { command, args };
}
// events
export type AnyEvent = MouseEvent | PointerEvent | TouchEvent;
export function isTouch(e: AnyEvent): e is TouchEvent {
return /^touch/i.test(e.type);
}
export function getButton(e: AnyEvent): number {
return ('button' in e) ? (e.button || 0) : 0;
}
export function getX(e: AnyEvent): number {
return ('touches' in e && e.touches.length > 0) ? e.touches[0].pageX : (e as any).pageX;
}
export function getY(e: AnyEvent): number {
return ('touches' in e && e.touches.length > 0) ? e.touches[0].pageY : (e as any).pageY;
}
export function isKeyEventInvalid(e: KeyboardEvent) {
return e.target && /^(input|textarea|select)$/i.test((<any>e.target).tagName);
}