mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-06-13 13:04:03 +00:00
Cleaned up /core
This commit is contained in:
parent
4dd57c1605
commit
c399e0d7f1
@ -1,16 +1,21 @@
|
||||
import { Signal } from "./signal";
|
||||
|
||||
// @ts-ignore
|
||||
import BackgroundAnimationFrameEmitterWorker from "../webworkers/background_animation_frame_emittter.worker";
|
||||
import { createLogger } from "./logging";
|
||||
|
||||
const logger = createLogger("animation_frame");
|
||||
const maxDtMs = 1000;
|
||||
const resetDtMs = 16;
|
||||
export class AnimationFrame {
|
||||
public frameEmitted = new Signal();
|
||||
public bgFrameEmitted = new Signal();
|
||||
public frameEmitted = new Signal<[dt: number]>();
|
||||
public bgFrameEmitted = new Signal<[dt: number]>();
|
||||
|
||||
public lastTime = performance.now();
|
||||
public bgLastTime = performance.now();
|
||||
|
||||
public boundMethod = this.handleAnimationFrame.bind(this);
|
||||
|
||||
public backgroundWorker = new BackgroundAnimationFrameEmitterWorker();
|
||||
|
||||
constructor() {
|
||||
@ -19,28 +24,35 @@ export class AnimationFrame {
|
||||
});
|
||||
this.backgroundWorker.addEventListener("message", this.handleBackgroundTick.bind(this));
|
||||
}
|
||||
|
||||
handleBackgroundTick() {
|
||||
const time = performance.now();
|
||||
|
||||
let dt = time - this.bgLastTime;
|
||||
|
||||
if (dt > maxDtMs) {
|
||||
dt = resetDtMs;
|
||||
}
|
||||
|
||||
this.bgFrameEmitted.dispatch(dt);
|
||||
this.bgLastTime = time;
|
||||
}
|
||||
|
||||
start() {
|
||||
assertAlways(window.requestAnimationFrame, "requestAnimationFrame is not supported!");
|
||||
this.handleAnimationFrame();
|
||||
this.handleAnimationFrame(0);
|
||||
}
|
||||
handleAnimationFrame(time) {
|
||||
|
||||
handleAnimationFrame(time: number) {
|
||||
let dt = time - this.lastTime;
|
||||
|
||||
if (dt > maxDtMs) {
|
||||
dt = resetDtMs;
|
||||
}
|
||||
|
||||
try {
|
||||
this.frameEmitted.dispatch(dt);
|
||||
}
|
||||
catch (ex) {
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
this.lastTime = time;
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { createLogger } from "./logging";
|
||||
|
||||
const logger = createLogger("assert");
|
||||
|
||||
let assertionErrorShown = false;
|
||||
|
||||
function initAssert() {
|
||||
/**
|
||||
* Expects a given condition to be true
|
||||
* @param {} failureMessage
|
||||
*/
|
||||
// @ts-ignore
|
||||
window.assert = function (condition: Boolean, ...failureMessage: ...String) {
|
||||
window.assert = function (condition: boolean, ...failureMessage: string[]) {
|
||||
if (!condition) {
|
||||
logger.error("assertion failed:", ...failureMessage);
|
||||
if (!assertionErrorShown) {
|
||||
@ -18,4 +20,5 @@ function initAssert() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
initAssert();
|
||||
|
@ -1,20 +1,24 @@
|
||||
// @ts-ignore
|
||||
import CompressionWorker from "../webworkers/compression.worker";
|
||||
|
||||
import { createLogger } from "./logging";
|
||||
import { round2Digits } from "./utils";
|
||||
|
||||
const logger = createLogger("async_compression");
|
||||
|
||||
export let compressionPrefix = String.fromCodePoint(1);
|
||||
function checkCryptPrefix(prefix) {
|
||||
|
||||
function checkCryptPrefix(prefix: string) {
|
||||
try {
|
||||
window.localStorage.setItem("prefix_test", prefix);
|
||||
window.localStorage.removeItem("prefix_test");
|
||||
return true;
|
||||
}
|
||||
catch (ex) {
|
||||
} catch (ex) {
|
||||
logger.warn("Prefix '" + prefix + "' not available");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!checkCryptPrefix(compressionPrefix)) {
|
||||
logger.warn("Switching to basic prefix");
|
||||
compressionPrefix = " ";
|
||||
@ -22,15 +26,18 @@ if (!checkCryptPrefix(compressionPrefix)) {
|
||||
logger.warn("Prefix not available, ls seems to be unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
export type JobEntry = {
|
||||
errorHandler: function(: void):void;
|
||||
resolver: function(: void):void;
|
||||
errorHandler: (err: any) => void;
|
||||
resolver: (res: any) => void;
|
||||
startTime: number;
|
||||
};
|
||||
|
||||
class AsynCompression {
|
||||
public worker = new CompressionWorker();
|
||||
|
||||
public currentJobId = 1000;
|
||||
|
||||
public currentJobs: {
|
||||
[idx: number]: JobEntry;
|
||||
} = {};
|
||||
@ -43,12 +50,22 @@ class AsynCompression {
|
||||
logger.error("Failed to resolve job result, job id", jobId, "is not known");
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = performance.now() - jobData.startTime;
|
||||
logger.log("Got job", jobId, "response within", round2Digits(duration), "ms: ", result.length, "bytes");
|
||||
logger.log(
|
||||
"Got job",
|
||||
jobId,
|
||||
"response within",
|
||||
round2Digits(duration),
|
||||
"ms: ",
|
||||
result.length,
|
||||
"bytes"
|
||||
);
|
||||
const resolver = jobData.resolver;
|
||||
delete this.currentJobs[jobId];
|
||||
resolver(result);
|
||||
});
|
||||
|
||||
this.worker.addEventListener("error", err => {
|
||||
logger.error("Got error from webworker:", err, "aborting all jobs");
|
||||
const failureCalls = [];
|
||||
@ -61,6 +78,7 @@ class AsynCompression {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compresses any object
|
||||
*/
|
||||
@ -71,9 +89,9 @@ class AsynCompression {
|
||||
compressionPrefix,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a new job
|
||||
* {}
|
||||
*/
|
||||
internalQueueJob(job: string, data: any): Promise<any> {
|
||||
const jobId = ++this.currentJobId;
|
||||
@ -87,9 +105,11 @@ class AsynCompression {
|
||||
resolver: resolve,
|
||||
startTime: performance.now(),
|
||||
};
|
||||
|
||||
logger.log("Posting job", job, "/", jobId);
|
||||
this.worker.postMessage({ jobId, job, data });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const asyncCompressor = new AsynCompression();
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
export type Size = {
|
||||
w: number;
|
||||
h: number;
|
||||
@ -24,20 +23,29 @@ export type AtlasMeta = {
|
||||
smartupdate: string;
|
||||
};
|
||||
export type SourceData = {
|
||||
frames: Object<string, SpriteDefinition>;
|
||||
frames: {
|
||||
[idx: string]: SpriteDefinition;
|
||||
};
|
||||
meta: AtlasMeta;
|
||||
};
|
||||
export class AtlasDefinition {
|
||||
public meta = meta;
|
||||
public sourceData = frames;
|
||||
public sourceFileName = meta.image;
|
||||
public meta: AtlasMeta;
|
||||
public sourceData: {
|
||||
[idx: string]: SpriteDefinition;
|
||||
};
|
||||
public sourceFileName: string;
|
||||
|
||||
constructor({ frames, meta }) {
|
||||
constructor({ frames, meta }: SourceData) {
|
||||
this.meta = meta;
|
||||
this.sourceData = frames;
|
||||
this.sourceFileName = meta.image;
|
||||
}
|
||||
|
||||
getFullSourcePath() {
|
||||
return this.sourceFileName;
|
||||
}
|
||||
}
|
||||
|
||||
export const atlasFiles: AtlasDefinition[] = require
|
||||
// @ts-ignore
|
||||
.context("../../../res_built/atlas/", false, /.*\.json/i)
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end */
|
||||
import type { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
||||
|
||||
import { initSpriteCache } from "../game/meta_building_registry";
|
||||
import { MUSIC, SOUNDS } from "../platform/sound";
|
||||
import { T } from "../translations";
|
||||
@ -10,14 +10,24 @@ import { Loader } from "./loader";
|
||||
import { createLogger } from "./logging";
|
||||
import { Signal } from "./signal";
|
||||
import { clamp, getLogoSprite, timeoutPromise } from "./utils";
|
||||
|
||||
const logger = createLogger("background_loader");
|
||||
const MAIN_MENU_ASSETS = {
|
||||
|
||||
type Assets = {
|
||||
sprites: string[];
|
||||
sounds: string[];
|
||||
atlas: AtlasDefinition[];
|
||||
css: string[];
|
||||
};
|
||||
|
||||
const MAIN_MENU_ASSETS: Assets = {
|
||||
sprites: [getLogoSprite()],
|
||||
sounds: [SOUNDS.uiClick, SOUNDS.uiError, SOUNDS.dialogError, SOUNDS.dialogOk],
|
||||
atlas: [],
|
||||
css: [],
|
||||
};
|
||||
const INGAME_ASSETS = {
|
||||
|
||||
const INGAME_ASSETS: Assets = {
|
||||
sprites: [],
|
||||
sounds: [
|
||||
...Array.from(Object.values(MUSIC)),
|
||||
@ -26,32 +36,36 @@ const INGAME_ASSETS = {
|
||||
atlas: atlasFiles,
|
||||
css: ["async-resources.css"],
|
||||
};
|
||||
|
||||
if (G_IS_STANDALONE) {
|
||||
MAIN_MENU_ASSETS.sounds = [...Array.from(Object.values(MUSIC)), ...Array.from(Object.values(SOUNDS))];
|
||||
INGAME_ASSETS.sounds = [];
|
||||
}
|
||||
|
||||
const LOADER_TIMEOUT_PER_RESOURCE = 180000;
|
||||
|
||||
// Cloudflare does not send content-length headers with brotli compression,
|
||||
// so store the actual (compressed) file sizes so we can show a progress bar.
|
||||
const HARDCODED_FILE_SIZES = {
|
||||
"async-resources.css": 2216145,
|
||||
};
|
||||
|
||||
export class BackgroundResourcesLoader {
|
||||
public app = app;
|
||||
public mainMenuPromise = null;
|
||||
public ingamePromise = null;
|
||||
public resourceStateChangedSignal = new Signal();
|
||||
public resourceStateChangedSignal = new Signal<[{ progress: number }]>();
|
||||
|
||||
constructor(app) {
|
||||
}
|
||||
getMainMenuPromise() {
|
||||
constructor(public app) {}
|
||||
|
||||
getMainMenuPromise(): Promise<void> {
|
||||
if (this.mainMenuPromise) {
|
||||
return this.mainMenuPromise;
|
||||
}
|
||||
logger.log("⏰ Loading main menu assets");
|
||||
return (this.mainMenuPromise = this.loadAssets(MAIN_MENU_ASSETS));
|
||||
}
|
||||
getIngamePromise() {
|
||||
|
||||
getIngamePromise(): Promise<void> {
|
||||
if (this.ingamePromise) {
|
||||
return this.ingamePromise;
|
||||
}
|
||||
@ -59,46 +73,77 @@ export class BackgroundResourcesLoader {
|
||||
const promise = this.loadAssets(INGAME_ASSETS).then(() => initSpriteCache());
|
||||
return (this.ingamePromise = promise);
|
||||
}
|
||||
async loadAssets({ sprites, sounds, atlas, css }: {
|
||||
|
||||
async loadAssets({
|
||||
sprites,
|
||||
sounds,
|
||||
atlas,
|
||||
css,
|
||||
}: {
|
||||
sprites: string[];
|
||||
sounds: string[];
|
||||
atlas: AtlasDefinition[];
|
||||
css: string[];
|
||||
}) {
|
||||
let promiseFunctions: ((progressHandler: (progress: number) => void) => Promise<void>)[] = [];
|
||||
let promiseFunctions: ((progressHandler: (progress: number) => void) => Promise<void>)[] = [];
|
||||
|
||||
// CSS
|
||||
for (let i = 0; i < css.length; ++i) {
|
||||
promiseFunctions.push(progress => timeoutPromise(this.internalPreloadCss(css[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch(err => {
|
||||
logger.error("Failed to load css:", css[i], err);
|
||||
throw new Error("HUD Stylesheet " + css[i] + " failed to load: " + err);
|
||||
}));
|
||||
promiseFunctions.push(progress =>
|
||||
timeoutPromise(this.internalPreloadCss(css[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch(
|
||||
err => {
|
||||
logger.error("Failed to load css:", css[i], err);
|
||||
throw new Error("HUD Stylesheet " + css[i] + " failed to load: " + err);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ATLAS FILES
|
||||
for (let i = 0; i < atlas.length; ++i) {
|
||||
promiseFunctions.push(progress => timeoutPromise(Loader.preloadAtlas(atlas[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch(err => {
|
||||
logger.error("Failed to load atlas:", atlas[i].sourceFileName, err);
|
||||
throw new Error("Atlas " + atlas[i].sourceFileName + " failed to load: " + err);
|
||||
}));
|
||||
promiseFunctions.push(progress =>
|
||||
timeoutPromise(Loader.preloadAtlas(atlas[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch(
|
||||
err => {
|
||||
logger.error("Failed to load atlas:", atlas[i].sourceFileName, err);
|
||||
throw new Error("Atlas " + atlas[i].sourceFileName + " failed to load: " + err);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// HUD Sprites
|
||||
for (let i = 0; i < sprites.length; ++i) {
|
||||
promiseFunctions.push(progress => timeoutPromise(Loader.preloadCSSSprite(sprites[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch(err => {
|
||||
logger.error("Failed to load css sprite:", sprites[i], err);
|
||||
throw new Error("HUD Sprite " + sprites[i] + " failed to load: " + err);
|
||||
}));
|
||||
promiseFunctions.push(progress =>
|
||||
timeoutPromise(
|
||||
Loader.preloadCSSSprite(sprites[i], progress),
|
||||
LOADER_TIMEOUT_PER_RESOURCE
|
||||
).catch(err => {
|
||||
logger.error("Failed to load css sprite:", sprites[i], err);
|
||||
throw new Error("HUD Sprite " + sprites[i] + " failed to load: " + err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// SFX & Music
|
||||
for (let i = 0; i < sounds.length; ++i) {
|
||||
promiseFunctions.push(progress => timeoutPromise(this.app.sound.loadSound(sounds[i]), LOADER_TIMEOUT_PER_RESOURCE).catch(err => {
|
||||
logger.warn("Failed to load sound, will not be available:", sounds[i], err);
|
||||
}));
|
||||
promiseFunctions.push(progress =>
|
||||
timeoutPromise(this.app.sound.loadSound(sounds[i]), LOADER_TIMEOUT_PER_RESOURCE).catch(
|
||||
err => {
|
||||
logger.warn("Failed to load sound, will not be available:", sounds[i], err);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const originalAmount = promiseFunctions.length;
|
||||
const start = performance.now();
|
||||
|
||||
logger.log("⏰ Preloading", originalAmount, "assets");
|
||||
|
||||
let progress = 0;
|
||||
this.resourceStateChangedSignal.dispatch({ progress });
|
||||
let promises = [];
|
||||
|
||||
for (let i = 0; i < promiseFunctions.length; i++) {
|
||||
let lastIndividualProgress = 0;
|
||||
const progressHandler = individualProgress => {
|
||||
@ -107,49 +152,64 @@ export class BackgroundResourcesLoader {
|
||||
progress += delta / originalAmount;
|
||||
this.resourceStateChangedSignal.dispatch({ progress });
|
||||
};
|
||||
promises.push(promiseFunctions[i](progressHandler).then(() => {
|
||||
progressHandler(1);
|
||||
}));
|
||||
promises.push(
|
||||
promiseFunctions[i](progressHandler).then(() => {
|
||||
progressHandler(1);
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
logger.log("⏰ Preloaded assets in", Math.round(performance.now() - start), "ms");
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an error when a resource failed to load and allows to reload the game
|
||||
*/
|
||||
showLoaderError(dialogs, err) {
|
||||
showLoaderError(dialogs: HUDModalDialogs, err: string) {
|
||||
if (G_IS_STANDALONE) {
|
||||
dialogs
|
||||
.showWarning(T.dialogs.resourceLoadFailed.title, T.dialogs.resourceLoadFailed.descSteamDemo + "<br>" + err, ["retry"])
|
||||
.showWarning(
|
||||
T.dialogs.resourceLoadFailed.title,
|
||||
T.dialogs.resourceLoadFailed.descSteamDemo + "<br>" + err,
|
||||
["retry"]
|
||||
)
|
||||
.retry.add(() => window.location.reload());
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
dialogs
|
||||
.showWarning(T.dialogs.resourceLoadFailed.title, T.dialogs.resourceLoadFailed.descWeb.replace("<demoOnSteamLinkText>", `<a href="https://get.shapez.io/resource_timeout" target="_blank">${T.dialogs.resourceLoadFailed.demoLinkText}</a>`) +
|
||||
"<br>" +
|
||||
err, ["retry"])
|
||||
.showWarning(
|
||||
T.dialogs.resourceLoadFailed.title,
|
||||
T.dialogs.resourceLoadFailed.descWeb.replace(
|
||||
"<demoOnSteamLinkText>",
|
||||
`<a href="https://get.shapez.io/resource_timeout" target="_blank">${T.dialogs.resourceLoadFailed.demoLinkText}</a>`
|
||||
) +
|
||||
"<br>" +
|
||||
err,
|
||||
["retry"]
|
||||
)
|
||||
.retry.add(() => window.location.reload());
|
||||
}
|
||||
}
|
||||
preloadWithProgress(src, progressHandler) {
|
||||
|
||||
preloadWithProgress(src: string, progressHandler: (percent: number) => void): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
let notifiedNotComputable = false;
|
||||
|
||||
const fullUrl = cachebust(src);
|
||||
xhr.open("GET", fullUrl, true);
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.onprogress = function (ev) {
|
||||
if (ev.lengthComputable) {
|
||||
progressHandler(ev.loaded / ev.total);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (window.location.search.includes("alwaysLogFileSize")) {
|
||||
console.warn("Progress:", src, ev.loaded);
|
||||
}
|
||||
|
||||
if (HARDCODED_FILE_SIZES[src]) {
|
||||
progressHandler(clamp(ev.loaded / HARDCODED_FILE_SIZES[src]));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (!notifiedNotComputable) {
|
||||
notifiedNotComputable = true;
|
||||
console.warn("Progress not computable:", src, ev.loaded);
|
||||
@ -158,14 +218,15 @@ export class BackgroundResourcesLoader {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onloadend = function () {
|
||||
if (!xhr.status.toString().match(/^2/)) {
|
||||
reject(fullUrl + ": " + xhr.status + " " + xhr.statusText);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (!notifiedNotComputable) {
|
||||
progressHandler(1);
|
||||
}
|
||||
|
||||
const options = {};
|
||||
const headers = xhr.getAllResponseHeaders();
|
||||
const contentType = headers.match(/^Content-Type:\s*(.*?)$/im);
|
||||
@ -179,7 +240,8 @@ export class BackgroundResourcesLoader {
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
internalPreloadCss(src, progressHandler) {
|
||||
|
||||
internalPreloadCss(src: string, progressHandler: (percent: number) => void) {
|
||||
return this.preloadWithProgress(src, progressHandler).then(blobSrc => {
|
||||
var styleElement = document.createElement("link");
|
||||
styleElement.href = blobSrc;
|
||||
|
@ -2,6 +2,7 @@ import { GameRoot } from "../game/root";
|
||||
import { clearBufferBacklog, freeCanvas, getBufferStats, makeOffscreenBuffer } from "./buffer_utils";
|
||||
import { createLogger } from "./logging";
|
||||
import { round1Digit } from "./utils";
|
||||
|
||||
export type CacheEntry = {
|
||||
canvas: HTMLCanvasElement;
|
||||
context: CanvasRenderingContext2D;
|
||||
@ -9,16 +10,18 @@ export type CacheEntry = {
|
||||
};
|
||||
|
||||
const logger = createLogger("buffers");
|
||||
|
||||
const bufferGcDurationSeconds = 0.5;
|
||||
|
||||
export class BufferMaintainer {
|
||||
public root = root;
|
||||
public cache: Map<string, Map<string, CacheEntry>> = new Map();
|
||||
public iterationIndex = 1;
|
||||
public lastIteration = 0;
|
||||
|
||||
constructor(root) {
|
||||
constructor(public root) {
|
||||
this.root.signals.gameFrameStarted.add(this.update, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the buffer stats
|
||||
*/
|
||||
@ -30,14 +33,18 @@ export class BufferMaintainer {
|
||||
};
|
||||
this.cache.forEach((subCache, key) => {
|
||||
++stats.rootKeys;
|
||||
|
||||
subCache.forEach((cacheEntry, subKey) => {
|
||||
++stats.subKeys;
|
||||
|
||||
const canvas = cacheEntry.canvas;
|
||||
stats.vramBytes += canvas.width * canvas.height * 4;
|
||||
});
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes to the next buffer iteration, clearing all buffers which were not used
|
||||
* for a few iterations
|
||||
@ -46,28 +53,34 @@ export class BufferMaintainer {
|
||||
let totalKeys = 0;
|
||||
let deletedKeys = 0;
|
||||
const minIteration = this.iterationIndex;
|
||||
|
||||
this.cache.forEach((subCache, key) => {
|
||||
let unusedSubKeys = [];
|
||||
|
||||
// Filter sub cache
|
||||
subCache.forEach((cacheEntry, subKey) => {
|
||||
if (cacheEntry.lastUse < minIteration ||
|
||||
if (
|
||||
cacheEntry.lastUse < minIteration ||
|
||||
// @ts-ignore
|
||||
cacheEntry.canvas._contextLost) {
|
||||
cacheEntry.canvas._contextLost
|
||||
) {
|
||||
unusedSubKeys.push(subKey);
|
||||
freeCanvas(cacheEntry.canvas);
|
||||
++deletedKeys;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
++totalKeys;
|
||||
}
|
||||
});
|
||||
|
||||
// Delete unused sub keys
|
||||
for (let i = 0; i < unusedSubKeys.length; ++i) {
|
||||
subCache.delete(unusedSubKeys[i]);
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure our backlog never gets too big
|
||||
clearBufferBacklog();
|
||||
|
||||
// if (G_IS_DEV) {
|
||||
// const bufferStats = getBufferStats();
|
||||
// const mbUsed = round1Digit(bufferStats.vramUsage / (1024 * 1024));
|
||||
@ -89,8 +102,10 @@ export class BufferMaintainer {
|
||||
// "MB"
|
||||
// );
|
||||
// }
|
||||
|
||||
++this.iterationIndex;
|
||||
}
|
||||
|
||||
update() {
|
||||
const now = this.root.time.realtimeNow();
|
||||
if (now - this.lastIteration > bufferGcDurationSeconds) {
|
||||
@ -98,18 +113,30 @@ export class BufferMaintainer {
|
||||
this.garbargeCollect();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*
|
||||
*/
|
||||
getForKey({ key, subKey, w, h, dpi, redrawMethod, additionalParams }: {
|
||||
|
||||
getForKey({
|
||||
key,
|
||||
subKey,
|
||||
w,
|
||||
h,
|
||||
dpi,
|
||||
redrawMethod,
|
||||
additionalParams = {},
|
||||
}: {
|
||||
key: string;
|
||||
subKey: string;
|
||||
w: number;
|
||||
h: number;
|
||||
dpi: number;
|
||||
redrawMethod: function(: void, : void, : void, : void, : void, : void):void;
|
||||
additionalParams: object=;
|
||||
redrawMethod: (
|
||||
canvas: HTMLCanvasElement,
|
||||
context: CanvasRenderingContext2D,
|
||||
w: number,
|
||||
h: number,
|
||||
dpi: number,
|
||||
addParams?: object
|
||||
) => void;
|
||||
additionalParams: object;
|
||||
}): HTMLCanvasElement {
|
||||
// First, create parent key
|
||||
let parent = this.cache.get(key);
|
||||
@ -117,21 +144,26 @@ export class BufferMaintainer {
|
||||
parent = new Map();
|
||||
this.cache.set(key, parent);
|
||||
}
|
||||
|
||||
// Now search for sub key
|
||||
const cacheHit = parent.get(subKey);
|
||||
if (cacheHit) {
|
||||
cacheHit.lastUse = this.iterationIndex;
|
||||
return cacheHit.canvas;
|
||||
}
|
||||
|
||||
// Need to generate new buffer
|
||||
const effectiveWidth = w * dpi;
|
||||
const effectiveHeight = h * dpi;
|
||||
|
||||
const [canvas, context] = makeOffscreenBuffer(effectiveWidth, effectiveHeight, {
|
||||
reusable: true,
|
||||
label: "buffer-" + key + "/" + subKey,
|
||||
smooth: true,
|
||||
});
|
||||
|
||||
redrawMethod(canvas, context, w, h, dpi, additionalParams);
|
||||
|
||||
parent.set(subKey, {
|
||||
canvas,
|
||||
context,
|
||||
@ -139,14 +171,8 @@ export class BufferMaintainer {
|
||||
});
|
||||
return canvas;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*
|
||||
*/
|
||||
getForKeyOrNullNoUpdate({ key, subKey }: {
|
||||
key: string;
|
||||
subKey: string;
|
||||
}): ?HTMLCanvasElement {
|
||||
|
||||
getForKeyOrNullNoUpdate({ key, subKey }: { key: string; subKey: string }): HTMLCanvasElement | null {
|
||||
let parent = this.cache.get(key);
|
||||
if (!parent) {
|
||||
return null;
|
||||
|
@ -1,16 +1,20 @@
|
||||
import { globalConfig } from "./config";
|
||||
import { fastArrayDelete } from "./utils";
|
||||
import { createLogger } from "./logging";
|
||||
|
||||
const logger = createLogger("buffer_utils");
|
||||
|
||||
/**
|
||||
* Enables images smoothing on a context
|
||||
*/
|
||||
export function enableImageSmoothing(context: CanvasRenderingContext2D) {
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.webkitImageSmoothingEnabled = true;
|
||||
|
||||
// @ts-ignore
|
||||
context.imageSmoothingQuality = globalConfig.smoothing.quality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables image smoothing on a context
|
||||
*/
|
||||
@ -18,16 +22,19 @@ export function disableImageSmoothing(context: CanvasRenderingContext2D) {
|
||||
context.imageSmoothingEnabled = false;
|
||||
context.webkitImageSmoothingEnabled = false;
|
||||
}
|
||||
|
||||
export type CanvasCacheEntry = {
|
||||
canvas: HTMLCanvasElement;
|
||||
context: CanvasRenderingContext2D;
|
||||
};
|
||||
|
||||
const registeredCanvas: Array<CanvasCacheEntry> = [];
|
||||
|
||||
/**
|
||||
* Buckets for each width * height combination
|
||||
*/
|
||||
const freeCanvasBuckets: Map<number, Array<CanvasCacheEntry>> = new Map();
|
||||
|
||||
/**
|
||||
* Track statistics
|
||||
*/
|
||||
@ -38,12 +45,14 @@ const stats = {
|
||||
numReused: 0,
|
||||
numCreated: 0,
|
||||
};
|
||||
|
||||
export function getBufferVramUsageBytes(canvas: HTMLCanvasElement) {
|
||||
assert(canvas, "no canvas given");
|
||||
assert(Number.isFinite(canvas.width), "bad canvas width: " + canvas.width);
|
||||
assert(Number.isFinite(canvas.height), "bad canvas height" + canvas.height);
|
||||
return canvas.width * canvas.height * 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stats on the allocated buffers
|
||||
*/
|
||||
@ -52,12 +61,14 @@ export function getBufferStats() {
|
||||
freeCanvasBuckets.forEach(bucket => {
|
||||
numBuffersFree += bucket.length;
|
||||
});
|
||||
|
||||
return {
|
||||
...stats,
|
||||
backlogKeys: freeCanvasBuckets.size,
|
||||
backlogSize: numBuffersFree,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the backlog buffers if they grew too much
|
||||
*/
|
||||
@ -72,14 +83,15 @@ export function clearBufferBacklog() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new offscreen buffer
|
||||
* {}
|
||||
*/
|
||||
export function makeOffscreenBuffer(w: Number, h: Number, { smooth = true, reusable = true, label = "buffer" }): [
|
||||
HTMLCanvasElement,
|
||||
CanvasRenderingContext2D
|
||||
] {
|
||||
export function makeOffscreenBuffer(
|
||||
w: number,
|
||||
h: number,
|
||||
{ smooth = true, reusable = true, label = "buffer" }
|
||||
): [HTMLCanvasElement, CanvasRenderingContext2D] {
|
||||
assert(w > 0 && h > 0, "W or H < 0");
|
||||
if (w % 1 !== 0 || h % 1 !== 0) {
|
||||
// console.warn("Subpixel offscreen buffer size:", w, h);
|
||||
@ -89,43 +101,56 @@ export function makeOffscreenBuffer(w: Number, h: Number, { smooth = true, reusa
|
||||
w = Math.max(1, w);
|
||||
h = Math.max(1, h);
|
||||
}
|
||||
|
||||
const recommendedSize = 1024 * 1024;
|
||||
if (w * h > recommendedSize) {
|
||||
logger.warn("Creating huge buffer:", w, "x", h, "with label", label);
|
||||
}
|
||||
|
||||
w = Math.floor(w);
|
||||
h = Math.floor(h);
|
||||
|
||||
let canvas = null;
|
||||
let context = null;
|
||||
|
||||
// Ok, search in cache first
|
||||
const bucket = freeCanvasBuckets.get(w * h) || [];
|
||||
|
||||
for (let i = 0; i < bucket.length; ++i) {
|
||||
const { canvas: useableCanvas, context: useableContext } = bucket[i];
|
||||
if (useableCanvas.width === w && useableCanvas.height === h) {
|
||||
// Ok we found one
|
||||
canvas = useableCanvas;
|
||||
context = useableContext;
|
||||
|
||||
// Restore past state
|
||||
context.restore();
|
||||
context.save();
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
delete canvas.style.width;
|
||||
delete canvas.style.height;
|
||||
|
||||
stats.numReused++;
|
||||
stats.backlogVramUsage -= getBufferVramUsageBytes(canvas);
|
||||
fastArrayDelete(bucket, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// None found , create new one
|
||||
if (!canvas) {
|
||||
canvas = document.createElement("canvas");
|
||||
context = canvas.getContext("2d" /*, { alpha } */);
|
||||
|
||||
stats.numCreated++;
|
||||
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
|
||||
// Initial state
|
||||
context.save();
|
||||
|
||||
canvas.addEventListener("webglcontextlost", () => {
|
||||
console.warn("canvas::webglcontextlost", canvas);
|
||||
// @ts-ignore
|
||||
@ -139,35 +164,42 @@ export function makeOffscreenBuffer(w: Number, h: Number, { smooth = true, reusa
|
||||
}
|
||||
// @ts-ignore
|
||||
canvas._contextLost = false;
|
||||
|
||||
// @ts-ignore
|
||||
canvas.label = label;
|
||||
if (smooth) {
|
||||
enableImageSmoothing(context);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
disableImageSmoothing(context);
|
||||
}
|
||||
|
||||
if (reusable) {
|
||||
registerCanvas(canvas, context);
|
||||
}
|
||||
|
||||
return [canvas, context];
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees a canvas
|
||||
*/
|
||||
export function registerCanvas(canvas: HTMLCanvasElement, context) {
|
||||
export function registerCanvas(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D) {
|
||||
registeredCanvas.push({ canvas, context });
|
||||
|
||||
stats.bufferCount += 1;
|
||||
const bytesUsed = getBufferVramUsageBytes(canvas);
|
||||
stats.vramUsage += bytesUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees a canvas
|
||||
*/
|
||||
export function freeCanvas(canvas: HTMLCanvasElement) {
|
||||
assert(canvas, "Canvas is empty");
|
||||
|
||||
let index = -1;
|
||||
let data = null;
|
||||
|
||||
for (let i = 0; i < registeredCanvas.length; ++i) {
|
||||
if (registeredCanvas[i].canvas === canvas) {
|
||||
index = i;
|
||||
@ -175,20 +207,23 @@ export function freeCanvas(canvas: HTMLCanvasElement) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index < 0) {
|
||||
logger.error("Tried to free unregistered canvas of size", canvas.width, canvas.height);
|
||||
return;
|
||||
}
|
||||
fastArrayDelete(registeredCanvas, index);
|
||||
|
||||
const key = canvas.width * canvas.height;
|
||||
const bucket = freeCanvasBuckets.get(key);
|
||||
if (bucket) {
|
||||
bucket.push(data);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
freeCanvasBuckets.set(key, [data]);
|
||||
}
|
||||
|
||||
stats.bufferCount -= 1;
|
||||
|
||||
const bytesUsed = getBufferVramUsageBytes(canvas);
|
||||
stats.vramUsage -= bytesUsed;
|
||||
stats.backlogVramUsage += bytesUsed;
|
||||
|
@ -5,19 +5,26 @@ import { Vector } from "./vector";
|
||||
import { IS_MOBILE, SUPPORT_TOUCH } from "./config";
|
||||
import { SOUNDS } from "../platform/sound";
|
||||
import { GLOBAL_APP } from "./globals";
|
||||
|
||||
const logger = createLogger("click_detector");
|
||||
|
||||
export const MAX_MOVE_DISTANCE_PX = IS_MOBILE ? 20 : 80;
|
||||
|
||||
// For debugging
|
||||
const registerClickDetectors = G_IS_DEV && true;
|
||||
if (registerClickDetectors) {
|
||||
window.activeClickDetectors = [];
|
||||
window.activeClickDetectors = [];
|
||||
}
|
||||
|
||||
// Store active click detectors so we can cancel them
|
||||
const ongoingClickDetectors: Array<ClickDetector> = [];
|
||||
|
||||
// Store when the last touch event was registered, to avoid accepting a touch *and* a click event
|
||||
|
||||
export let clickDetectorGlobals = {
|
||||
lastTouchTime: -1000,
|
||||
};
|
||||
|
||||
export type ClickDetectorConstructorArgs = {
|
||||
consumeEvents?: boolean;
|
||||
preventDefault?: boolean;
|
||||
@ -32,30 +39,67 @@ export type ClickDetectorConstructorArgs = {
|
||||
// Detects clicks
|
||||
export class ClickDetector {
|
||||
public clickDownPosition = null;
|
||||
public consumeEvents = consumeEvents;
|
||||
public preventDefault = preventDefault;
|
||||
public applyCssClass = applyCssClass;
|
||||
public captureTouchmove = captureTouchmove;
|
||||
public targetOnly = targetOnly;
|
||||
public clickSound = clickSound;
|
||||
public maxDistance = maxDistance;
|
||||
public preventClick = preventClick;
|
||||
public click = new Signal();
|
||||
public rightClick = new Signal();
|
||||
public touchstart = new Signal();
|
||||
public touchmove = new Signal();
|
||||
public touchend = new Signal();
|
||||
public touchcancel = new Signal();
|
||||
public touchstartSimple = new Signal();
|
||||
public touchmoveSimple = new Signal();
|
||||
public touchendSimple = new Signal();
|
||||
public clickStartTime = null;
|
||||
public consumeEvents: boolean;
|
||||
public preventDefault: boolean;
|
||||
public applyCssClass: string;
|
||||
public captureTouchmove: boolean;
|
||||
public targetOnly: boolean;
|
||||
public clickSound: string;
|
||||
public maxDistance: number;
|
||||
public preventClick: boolean;
|
||||
|
||||
// Bound Methods
|
||||
public handlerTouchStart = this.internalOnPointerDown.bind(this);
|
||||
public handlerTouchEnd = this.internalOnPointerEnd.bind(this);
|
||||
public handlerTouchMove = this.internalOnPointerMove.bind(this);
|
||||
public handlerTouchCancel = this.internalOnTouchCancel.bind(this);
|
||||
public handlerPreventClick = this.internalPreventClick.bind(this);
|
||||
|
||||
// Signals
|
||||
public click = new Signal<[pos: Vector, ev?: TouchEvent | MouseEvent]>();
|
||||
public rightClick = new Signal<[pos: Vector, ev: MouseEvent]>();
|
||||
public touchstart = new Signal<[ev: TouchEvent | MouseEvent]>();
|
||||
public touchmove = new Signal<[ev: TouchEvent | MouseEvent]>();
|
||||
public touchend = new Signal<[ev: TouchEvent | MouseEvent]>();
|
||||
public touchcancel = new Signal<[ev: TouchEvent | MouseEvent]>();
|
||||
|
||||
public touchstartSimple = new Signal<[x: number, y: number]>();
|
||||
public touchmoveSimple = new Signal<[x: number, y: number]>();
|
||||
public touchendSimple = new Signal<[ev?: TouchEvent | MouseEvent]>();
|
||||
|
||||
public clickStartTime: number = null;
|
||||
|
||||
public cancelled = false;
|
||||
|
||||
constructor(element, { consumeEvents = false, preventDefault = true, applyCssClass = "pressed", captureTouchmove = false, targetOnly = false, maxDistance = MAX_MOVE_DISTANCE_PX, clickSound = SOUNDS.uiClick, preventClick = false, }) {
|
||||
public element?: HTMLElement;
|
||||
|
||||
constructor(
|
||||
element: Element,
|
||||
{
|
||||
consumeEvents = false,
|
||||
preventDefault = true,
|
||||
applyCssClass = "pressed",
|
||||
captureTouchmove = false,
|
||||
targetOnly = false,
|
||||
maxDistance = MAX_MOVE_DISTANCE_PX,
|
||||
clickSound = SOUNDS.uiClick,
|
||||
preventClick = false,
|
||||
}
|
||||
) {
|
||||
assert(element, "No element given!");
|
||||
this.internalBindTo(element as HTMLElement));
|
||||
|
||||
this.consumeEvents = consumeEvents;
|
||||
this.preventDefault = preventDefault;
|
||||
this.applyCssClass = applyCssClass;
|
||||
this.captureTouchmove = captureTouchmove;
|
||||
this.targetOnly = targetOnly;
|
||||
this.clickSound = clickSound;
|
||||
this.maxDistance = maxDistance;
|
||||
this.preventClick = preventClick;
|
||||
|
||||
this.internalBindTo(element as HTMLElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up all event listeners of this detector
|
||||
*/
|
||||
@ -65,20 +109,22 @@ export class ClickDetector {
|
||||
const index = window.activeClickDetectors.indexOf(this);
|
||||
if (index < 0) {
|
||||
logger.error("Click detector cleanup but is not active");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
window.activeClickDetectors.splice(index, 1);
|
||||
}
|
||||
}
|
||||
const options = this.internalGetEventListenerOptions();
|
||||
|
||||
if (SUPPORT_TOUCH) {
|
||||
this.element.removeEventListener("touchstart", this.handlerTouchStart, options);
|
||||
this.element.removeEventListener("touchend", this.handlerTouchEnd, options);
|
||||
this.element.removeEventListener("touchcancel", this.handlerTouchCancel, options);
|
||||
}
|
||||
|
||||
this.element.removeEventListener("mouseup", this.handlerTouchStart, options);
|
||||
this.element.removeEventListener("mousedown", this.handlerTouchEnd, options);
|
||||
this.element.removeEventListener("mouseout", this.handlerTouchCancel, options);
|
||||
|
||||
if (this.captureTouchmove) {
|
||||
if (SUPPORT_TOUCH) {
|
||||
this.element.removeEventListener("touchmove", this.handlerTouchMove, options);
|
||||
@ -88,19 +134,23 @@ export class ClickDetector {
|
||||
if (this.preventClick) {
|
||||
this.element.removeEventListener("click", this.handlerPreventClick, options);
|
||||
}
|
||||
|
||||
this.click.removeAll();
|
||||
this.touchstart.removeAll();
|
||||
this.touchmove.removeAll();
|
||||
this.touchend.removeAll();
|
||||
this.touchcancel.removeAll();
|
||||
|
||||
this.element = null;
|
||||
}
|
||||
}
|
||||
// INTERNAL METHODS
|
||||
internalPreventClick(event: Event) {
|
||||
|
||||
internalPreventClick(event: Event) {
|
||||
window.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to get the options to pass to an event listener
|
||||
*/
|
||||
@ -110,17 +160,14 @@ export class ClickDetector {
|
||||
passive: !this.preventDefault,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the click detector to an element
|
||||
*/
|
||||
internalBindTo(element: HTMLElement) {
|
||||
const options = this.internalGetEventListenerOptions();
|
||||
this.handlerTouchStart = this.internalOnPointerDown.bind(this);
|
||||
this.handlerTouchEnd = this.internalOnPointerEnd.bind(this);
|
||||
this.handlerTouchMove = this.internalOnPointerMove.bind(this);
|
||||
this.handlerTouchCancel = this.internalOnTouchCancel.bind(this);
|
||||
|
||||
if (this.preventClick) {
|
||||
this.handlerPreventClick = this.internalPreventClick.bind(this);
|
||||
element.addEventListener("click", this.handlerPreventClick, options);
|
||||
}
|
||||
if (SUPPORT_TOUCH) {
|
||||
@ -142,12 +189,14 @@ export class ClickDetector {
|
||||
}
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the bound element is currently in the DOM.
|
||||
*/
|
||||
internalIsDomElementAttached() {
|
||||
return this.element && document.documentElement.contains(this.element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given event is relevant for this detector
|
||||
*/
|
||||
@ -156,52 +205,70 @@ export class ClickDetector {
|
||||
// Already cleaned up
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.targetOnly && event.target !== this.element) {
|
||||
// Clicked a child element
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stop any propagation and defaults if configured
|
||||
if (this.consumeEvents && event.cancelable) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (this.preventDefault && event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (window.TouchEvent && event instanceof TouchEvent) {
|
||||
clickDetectorGlobals.lastTouchTime = performance.now();
|
||||
|
||||
// console.log("Got touches", event.targetTouches.length, "vs", expectedRemainingTouches);
|
||||
if (event.targetTouches.length !== expectedRemainingTouches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the mous position from an event
|
||||
* {} The client space position
|
||||
* Extracts the mouse position from an event
|
||||
* @param event The client space position
|
||||
*/
|
||||
static extractPointerPosition(event: TouchEvent | MouseEvent): Vector {
|
||||
if (window.TouchEvent && event instanceof TouchEvent) {
|
||||
if (event.changedTouches.length !== 1) {
|
||||
logger.warn("Got unexpected target touches:", event.targetTouches.length, "->", event.targetTouches);
|
||||
logger.warn(
|
||||
"Got unexpected target touches:",
|
||||
event.targetTouches.length,
|
||||
"->",
|
||||
event.targetTouches
|
||||
);
|
||||
return new Vector(0, 0);
|
||||
}
|
||||
|
||||
const touch = event.changedTouches[0];
|
||||
return new Vector(touch.clientX, touch.clientY);
|
||||
}
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
return new Vector(event.clientX, event.clientY);
|
||||
}
|
||||
|
||||
assertAlways(false, "Got unknown event: " + event);
|
||||
|
||||
return new Vector(0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cacnels all ongoing events on this detector
|
||||
* Cancels all ongoing events on this detector
|
||||
*/
|
||||
cancelOngoingEvents() {
|
||||
if (this.applyCssClass && this.element) {
|
||||
@ -212,16 +279,19 @@ export class ClickDetector {
|
||||
this.cancelled = true;
|
||||
fastArrayDeleteValueIfContained(ongoingClickDetectors, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal pointer down handler
|
||||
*/
|
||||
internalOnPointerDown(event: TouchEvent | MouseEvent) {
|
||||
window.focus();
|
||||
|
||||
if (!this.internalEventPreHandler(event, 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const position = this.constructor as typeof ClickDetector).extractPointerPosition(event);
|
||||
const position = (this.constructor as typeof ClickDetector).extractPointerPosition(event);
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
const isRightClick = event.button === 2;
|
||||
if (isRightClick) {
|
||||
@ -232,33 +302,40 @@ export class ClickDetector {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.clickDownPosition) {
|
||||
logger.warn("Ignoring double click");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.cancelled = false;
|
||||
this.touchstart.dispatch(event);
|
||||
|
||||
// Store where the touch started
|
||||
this.clickDownPosition = position;
|
||||
this.clickStartTime = performance.now();
|
||||
this.touchstartSimple.dispatch(this.clickDownPosition.x, this.clickDownPosition.y);
|
||||
|
||||
// If we are not currently within a click, register it
|
||||
if (ongoingClickDetectors.indexOf(this) < 0) {
|
||||
ongoingClickDetectors.push(this);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
logger.warn("Click detector got pointer down of active pointer twice");
|
||||
}
|
||||
|
||||
// If we should apply any classes, do this now
|
||||
if (this.applyCssClass) {
|
||||
this.element.classList.add(this.applyCssClass);
|
||||
}
|
||||
|
||||
// If we should play any sound, do this
|
||||
if (this.clickSound) {
|
||||
GLOBAL_APP.sound.playUiSound(this.clickSound);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal pointer move handler
|
||||
*/
|
||||
@ -267,61 +344,68 @@ export class ClickDetector {
|
||||
return false;
|
||||
}
|
||||
this.touchmove.dispatch(event);
|
||||
|
||||
const pos = this.constructor as typeof ClickDetector).extractPointerPosition(event);
|
||||
const pos = (this.constructor as typeof ClickDetector).extractPointerPosition(event);
|
||||
this.touchmoveSimple.dispatch(pos.x, pos.y);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal pointer end handler
|
||||
*/
|
||||
internalOnPointerEnd(event: TouchEvent | MouseEvent) {
|
||||
window.focus();
|
||||
|
||||
if (!this.internalEventPreHandler(event, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cancelled) {
|
||||
// warn(this, "Not dispatching touchend on cancelled listener");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
const isRightClick = event.button === 2;
|
||||
if (isRightClick) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const index = ongoingClickDetectors.indexOf(this);
|
||||
if (index < 0) {
|
||||
logger.warn("Got pointer end but click detector is not in pressed state");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
fastArrayDelete(ongoingClickDetectors, index);
|
||||
}
|
||||
|
||||
let dispatchClick = false;
|
||||
let dispatchClickPos = null;
|
||||
|
||||
// Check for correct down position, otherwise must have pinched or so
|
||||
if (this.clickDownPosition) {
|
||||
|
||||
const pos = this.constructor as typeof ClickDetector).extractPointerPosition(event);
|
||||
const pos = (this.constructor as typeof ClickDetector).extractPointerPosition(event);
|
||||
const distance = pos.distance(this.clickDownPosition);
|
||||
if (!IS_MOBILE || distance <= this.maxDistance) {
|
||||
dispatchClick = true;
|
||||
dispatchClickPos = pos;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
console.warn("[ClickDetector] Touch does not count as click:", "(was", distance, ")");
|
||||
}
|
||||
}
|
||||
|
||||
this.clickDownPosition = null;
|
||||
this.clickStartTime = null;
|
||||
|
||||
if (this.applyCssClass) {
|
||||
this.element.classList.remove(this.applyCssClass);
|
||||
}
|
||||
|
||||
// Dispatch in the end to avoid the element getting invalidated
|
||||
// Also make sure that the element is still in the dom
|
||||
if (this.internalIsDomElementAttached()) {
|
||||
this.touchend.dispatch(event);
|
||||
this.touchendSimple.dispatch();
|
||||
|
||||
if (dispatchClick) {
|
||||
const detectors = ongoingClickDetectors.slice();
|
||||
for (let i = 0; i < detectors.length; ++i) {
|
||||
@ -332,6 +416,7 @@ export class ClickDetector {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal touch cancel handler
|
||||
*/
|
||||
@ -339,10 +424,12 @@ export class ClickDetector {
|
||||
if (!this.internalEventPreHandler(event, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cancelled) {
|
||||
// warn(this, "Not dispatching touchcancel on cancelled listener");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.cancelOngoingEvents();
|
||||
this.touchcancel.dispatch(event);
|
||||
this.touchendSimple.dispatch(event);
|
||||
|
@ -1,126 +1,126 @@
|
||||
export default {
|
||||
// You can set any debug options here!
|
||||
/* dev:start */
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Quickly enters the game and skips the main menu - good for fast iterating
|
||||
// fastGameEnter: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Skips any delays like transitions between states and such
|
||||
// noArtificialDelays: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables writing of savegames, useful for testing the same savegame over and over
|
||||
// disableSavegameWrite: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Shows bounds of all entities
|
||||
// showEntityBounds: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Shows arrows for every ejector / acceptor
|
||||
// showAcceptorEjectors: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the music (Overrides any setting, can cause weird behaviour)
|
||||
// disableMusic: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Do not render static map entities (=most buildings)
|
||||
// doNotRenderStatics: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allow to zoom freely without limits
|
||||
// disableZoomLimits: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// All rewards can be unlocked by passing just 1 of any shape
|
||||
// rewardsInstant: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Unlocks all buildings
|
||||
// allBuildingsUnlocked: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables cost of blueprints
|
||||
// blueprintsNoCost: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables cost of upgrades
|
||||
// upgradesNoCost: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the dialog when completing a level
|
||||
// disableUnlockDialog: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the simulation - This effectively pauses the game.
|
||||
// disableLogicTicks: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Test the rendering if everything is clipped out properly
|
||||
// testClipping: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allows to render slower, useful for recording at half speed to avoid stuttering
|
||||
// framePausesBetweenTicks: 250,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Replace all translations with emojis to see which texts are translateable
|
||||
// testTranslations: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Enables an inspector which shows information about the entity below the cursor
|
||||
// enableEntityInspector: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Enables ads in the local build (normally they are deactivated there)
|
||||
// testAds: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allows unlocked achievements to be logged to console in the local build
|
||||
// testAchievements: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Enables use of (some) existing flags within the puzzle mode context
|
||||
// testPuzzleMode: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the automatic switch to an overview when zooming out
|
||||
// disableMapOverview: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the notification when there are new entries in the changelog since last played
|
||||
// disableUpgradeNotification: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Makes belts almost infinitely fast
|
||||
// instantBelts: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Makes item processors almost infinitely fast
|
||||
// instantProcessors: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Makes miners almost infinitely fast
|
||||
// instantMiners: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// When using fastGameEnter, controls whether a new game is started or the last one is resumed
|
||||
// resumeGameOnFastEnter: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Special option used to render the trailer
|
||||
// renderForTrailer: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Whether to render changes
|
||||
// renderChanges: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Whether to render belt paths
|
||||
// renderBeltPaths: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Whether to check belt paths
|
||||
// checkBeltPaths: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Whether to items / s instead of items / m in stats
|
||||
// detailedStatistics: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Shows detailed information about which atlas is used
|
||||
// showAtlasInfo: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Renders the rotation of all wires
|
||||
// renderWireRotations: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Renders information about wire networks
|
||||
// renderWireNetworkInfos: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables ejector animations and processing
|
||||
// disableEjectorProcessing: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allows manual ticking
|
||||
// manualTickOnly: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables slow asserts, useful for debugging performance
|
||||
// disableSlowAsserts: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allows to load a mod from an external source for developing it
|
||||
// externalModUrl: "http://localhost:3005/combined.js",
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Visualizes the shape grouping on belts
|
||||
// showShapeGrouping: true
|
||||
// -----------------------------------------------------------------------------------
|
||||
/* dev:end */
|
||||
// You can set any debug options here!
|
||||
/* dev:start */
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Quickly enters the game and skips the main menu - good for fast iterating
|
||||
// fastGameEnter: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Skips any delays like transitions between states and such
|
||||
// noArtificialDelays: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables writing of savegames, useful for testing the same savegame over and over
|
||||
// disableSavegameWrite: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Shows bounds of all entities
|
||||
// showEntityBounds: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Shows arrows for every ejector / acceptor
|
||||
// showAcceptorEjectors: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the music (Overrides any setting, can cause weird behaviour)
|
||||
// disableMusic: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Do not render static map entities (=most buildings)
|
||||
// doNotRenderStatics: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allow to zoom freely without limits
|
||||
// disableZoomLimits: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// All rewards can be unlocked by passing just 1 of any shape
|
||||
// rewardsInstant: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Unlocks all buildings
|
||||
// allBuildingsUnlocked: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables cost of blueprints
|
||||
// blueprintsNoCost: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables cost of upgrades
|
||||
// upgradesNoCost: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the dialog when completing a level
|
||||
// disableUnlockDialog: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the simulation - This effectively pauses the game.
|
||||
// disableLogicTicks: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Test the rendering if everything is clipped out properly
|
||||
// testClipping: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allows to render slower, useful for recording at half speed to avoid stuttering
|
||||
// framePausesBetweenTicks: 250,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Replace all translations with emojis to see which texts are translateable
|
||||
// testTranslations: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Enables an inspector which shows information about the entity below the cursor
|
||||
// enableEntityInspector: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Enables ads in the local build (normally they are deactivated there)
|
||||
// testAds: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allows unlocked achievements to be logged to console in the local build
|
||||
// testAchievements: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Enables use of (some) existing flags within the puzzle mode context
|
||||
// testPuzzleMode: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the automatic switch to an overview when zooming out
|
||||
// disableMapOverview: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the notification when there are new entries in the changelog since last played
|
||||
// disableUpgradeNotification: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Makes belts almost infinitely fast
|
||||
// instantBelts: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Makes item processors almost infinitely fast
|
||||
// instantProcessors: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Makes miners almost infinitely fast
|
||||
// instantMiners: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// When using fastGameEnter, controls whether a new game is started or the last one is resumed
|
||||
// resumeGameOnFastEnter: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Special option used to render the trailer
|
||||
// renderForTrailer: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Whether to render changes
|
||||
// renderChanges: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Whether to render belt paths
|
||||
// renderBeltPaths: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Whether to check belt paths
|
||||
// checkBeltPaths: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Whether to items / s instead of items / m in stats
|
||||
// detailedStatistics: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Shows detailed information about which atlas is used
|
||||
// showAtlasInfo: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Renders the rotation of all wires
|
||||
// renderWireRotations: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Renders information about wire networks
|
||||
// renderWireNetworkInfos: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables ejector animations and processing
|
||||
// disableEjectorProcessing: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allows manual ticking
|
||||
// manualTickOnly: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables slow asserts, useful for debugging performance
|
||||
// disableSlowAsserts: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allows to load a mod from an external source for developing it
|
||||
// externalModUrl: "http://localhost:3005/combined.js",
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Visualizes the shape grouping on belts
|
||||
// showShapeGrouping: true
|
||||
// -----------------------------------------------------------------------------------
|
||||
/* dev:end */
|
||||
};
|
||||
|
@ -1,21 +1,26 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end */
|
||||
export const IS_DEBUG = G_IS_DEV &&
|
||||
|
||||
export const IS_DEBUG =
|
||||
G_IS_DEV &&
|
||||
typeof window !== "undefined" &&
|
||||
window.location.port === "3005" &&
|
||||
(window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) &&
|
||||
window.location.search.indexOf("nodebug") < 0;
|
||||
|
||||
export const SUPPORT_TOUCH = false;
|
||||
|
||||
const smoothCanvas = true;
|
||||
|
||||
export const THIRDPARTY_URLS = {
|
||||
discord: "https://discord.gg/HN7EVzV",
|
||||
github: "https://github.com/tobspr-games/shapez.io",
|
||||
reddit: "https://www.reddit.com/r/shapezio",
|
||||
shapeViewer: "https://viewer.shapez.io",
|
||||
|
||||
twitter: "https://twitter.com/tobspr",
|
||||
patreon: "https://www.patreon.com/tobsprgames",
|
||||
privacyPolicy: "https://tobspr.io/privacy.html",
|
||||
|
||||
standaloneCampaignLink: "https://get.shapez.io/bundle/$campaign",
|
||||
puzzleDlcStorePage: "https://get.shapez.io/mm_puzzle_dlc?target=dlc",
|
||||
levelTutorialVideos: {
|
||||
@ -23,52 +28,69 @@ export const THIRDPARTY_URLS = {
|
||||
25: "https://www.youtube.com/watch?v=7OCV1g40Iew&",
|
||||
26: "https://www.youtube.com/watch?v=gfm6dS1dCoY",
|
||||
},
|
||||
|
||||
modBrowser: "https://shapez.mod.io/",
|
||||
};
|
||||
|
||||
export function openStandaloneLink(app: Application, campaign: string) {
|
||||
const discount = globalConfig.currentDiscount > 0 ? "_discount" + globalConfig.currentDiscount : "";
|
||||
const event = campaign + discount;
|
||||
app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneCampaignLink.replace("$campaign", event));
|
||||
app.gameAnalytics.noteMinor("g.stdlink." + event);
|
||||
}
|
||||
|
||||
export const globalConfig = {
|
||||
// Size of a single tile in Pixels.
|
||||
// NOTICE: Update webpack.production.config too!
|
||||
tileSize: 32,
|
||||
halfTileSize: 16,
|
||||
|
||||
// Which dpi the assets have
|
||||
assetsDpi: 192 / 32,
|
||||
assetsSharpness: 1.5,
|
||||
shapesSharpness: 1.3,
|
||||
|
||||
// Achievements
|
||||
achievementSliceDuration: 10,
|
||||
|
||||
// Production analytics
|
||||
statisticsGraphDpi: 2.5,
|
||||
statisticsGraphSlices: 100,
|
||||
analyticsSliceDurationSeconds: G_IS_DEV ? 1 : 10,
|
||||
|
||||
minimumTickRate: 25,
|
||||
maximumTickRate: 500,
|
||||
|
||||
// Map
|
||||
mapChunkSize: 16,
|
||||
chunkAggregateSize: 4,
|
||||
mapChunkOverviewMinZoom: 0.9,
|
||||
mapChunkWorldSize: null,
|
||||
|
||||
maxBeltShapeBundleSize: 20,
|
||||
|
||||
// Belt speeds
|
||||
// NOTICE: Update webpack.production.config too!
|
||||
beltSpeedItemsPerSecond: 2,
|
||||
minerSpeedItemsPerSecond: 0,
|
||||
|
||||
defaultItemDiameter: 20,
|
||||
|
||||
itemSpacingOnBelts: 0.63,
|
||||
|
||||
wiresSpeedItemsPerSecond: 6,
|
||||
|
||||
undergroundBeltMaxTilesByTier: [5, 9],
|
||||
|
||||
readerAnalyzeIntervalSeconds: 10,
|
||||
|
||||
goalAcceptorItemsRequired: 12,
|
||||
goalAcceptorsPerProducer: 5,
|
||||
puzzleModeSpeed: 3,
|
||||
puzzleMinBoundsSize: 2,
|
||||
puzzleMaxBoundsSize: 20,
|
||||
puzzleValidationDurationSeconds: 30,
|
||||
|
||||
buildingSpeeds: {
|
||||
cutter: 1 / 4,
|
||||
cutterQuad: 1 / 4,
|
||||
@ -81,39 +103,53 @@ export const globalConfig = {
|
||||
mixer: 1 / 5,
|
||||
stacker: 1 / 8,
|
||||
},
|
||||
|
||||
// Zooming
|
||||
initialZoom: 1.9,
|
||||
minZoomLevel: 0.1,
|
||||
maxZoomLevel: 3,
|
||||
|
||||
// Global game speed
|
||||
gameSpeed: 1,
|
||||
|
||||
warmupTimeSecondsFast: 0.25,
|
||||
warmupTimeSecondsRegular: 0.25,
|
||||
|
||||
smoothing: {
|
||||
smoothMainCanvas: smoothCanvas && true,
|
||||
quality: "low", // Low is CRUCIAL for mobile performance!
|
||||
},
|
||||
|
||||
rendering: {},
|
||||
debug: require("./config.local").default,
|
||||
|
||||
currentDiscount: 0,
|
||||
|
||||
// Secret vars
|
||||
info: {
|
||||
// Binary file salt
|
||||
file: "Ec'])@^+*9zMevK3uMV4432x9%iK'=",
|
||||
|
||||
// Savegame salt
|
||||
sgSalt: "}95Q3%8/.837Lqym_BJx%q7)pAHJbF",
|
||||
|
||||
// Analytics key
|
||||
analyticsApiKey: "baf6a50f0cc7dfdec5a0e21c88a1c69a4b34bc4a",
|
||||
},
|
||||
};
|
||||
|
||||
export const IS_MOBILE = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
|
||||
// Automatic calculations
|
||||
globalConfig.minerSpeedItemsPerSecond = globalConfig.beltSpeedItemsPerSecond / 5;
|
||||
|
||||
globalConfig.mapChunkWorldSize = globalConfig.mapChunkSize * globalConfig.tileSize;
|
||||
|
||||
// Dynamic calculations
|
||||
if (globalConfig.debug.disableMapOverview) {
|
||||
globalConfig.mapChunkOverviewMinZoom = 0;
|
||||
}
|
||||
|
||||
// Stuff for making the trailer
|
||||
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
|
||||
globalConfig.debug.framePausesBetweenTicks = 32;
|
||||
@ -124,9 +160,11 @@ if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
|
||||
globalConfig.debug.disableSavegameWrite = true;
|
||||
// globalConfig.beltSpeedItemsPerSecond *= 2;
|
||||
}
|
||||
|
||||
if (globalConfig.debug.fastGameEnter) {
|
||||
globalConfig.debug.noArtificialDelays = true;
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
|
||||
globalConfig.warmupTimeSecondsFast = 0;
|
||||
globalConfig.warmupTimeSecondsRegular = 0;
|
||||
|
@ -1,61 +1,64 @@
|
||||
import { globalConfig } from "./config";
|
||||
import { round1Digit, round2Digits } from "./utils";
|
||||
|
||||
/**
|
||||
* Returns the current dpi
|
||||
* {}
|
||||
*/
|
||||
export function getDeviceDPI(): number {
|
||||
return window.devicePixelRatio || 1;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {} Smoothed dpi
|
||||
* @param dpi Smoothed dpi
|
||||
*/
|
||||
export function smoothenDpi(dpi: number): number {
|
||||
if (dpi < 0.05) {
|
||||
return 0.05;
|
||||
}
|
||||
else if (dpi < 0.2) {
|
||||
} else if (dpi < 0.2) {
|
||||
return round2Digits(Math.round(dpi / 0.04) * 0.04);
|
||||
}
|
||||
else if (dpi < 1) {
|
||||
} else if (dpi < 1) {
|
||||
return round1Digit(Math.round(dpi / 0.1) * 0.1);
|
||||
}
|
||||
else if (dpi < 4) {
|
||||
} else if (dpi < 4) {
|
||||
return round1Digit(Math.round(dpi / 0.5) * 0.5);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial dpi
|
||||
// setDPIMultiplicator(1);
|
||||
|
||||
/**
|
||||
* Prepares a context for hihg dpi rendering
|
||||
* Prepares a context for high dpi rendering
|
||||
*/
|
||||
export function prepareHighDPIContext(context: CanvasRenderingContext2D, smooth = true) {
|
||||
const dpi = getDeviceDPI();
|
||||
context.scale(dpi, dpi);
|
||||
|
||||
if (smooth) {
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.webkitImageSmoothingEnabled = true;
|
||||
|
||||
// @ts-ignore
|
||||
context.imageSmoothingQuality = globalConfig.smoothing.quality;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context.imageSmoothingEnabled = false;
|
||||
context.webkitImageSmoothingEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes a high dpi canvas
|
||||
*/
|
||||
export function resizeHighDPICanvas(canvas: HTMLCanvasElement, w: number, h: number, smooth = true) {
|
||||
const dpi = getDeviceDPI();
|
||||
|
||||
const wNumber = Math.floor(w);
|
||||
const hNumber = Math.floor(h);
|
||||
|
||||
const targetW = Math.floor(wNumber * dpi);
|
||||
const targetH = Math.floor(hNumber * dpi);
|
||||
|
||||
if (targetW !== canvas.width || targetH !== canvas.height) {
|
||||
// console.log("Resize Canvas from", canvas.width, canvas.height, "to", targetW, targetH)
|
||||
canvas.width = targetW;
|
||||
@ -65,6 +68,7 @@ export function resizeHighDPICanvas(canvas: HTMLCanvasElement, w: number, h: num
|
||||
prepareHighDPIContext(canvas.getContext("2d"), smooth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes a canvas
|
||||
*/
|
||||
@ -81,10 +85,16 @@ export function resizeCanvas(canvas: HTMLCanvasElement, w: number, h: number, se
|
||||
// console.log("Resizing canvas to", actualW, "x", actualH);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes a canvas and makes sure its cleared
|
||||
*/
|
||||
export function resizeCanvasAndClear(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, w: number, h: number) {
|
||||
export function resizeCanvasAndClear(
|
||||
canvas: HTMLCanvasElement,
|
||||
context: CanvasRenderingContext2D,
|
||||
w: number,
|
||||
h: number
|
||||
) {
|
||||
const actualW = Math.ceil(w);
|
||||
const actualH = Math.ceil(h);
|
||||
if (actualW !== canvas.width || actualH !== canvas.height) {
|
||||
@ -93,8 +103,7 @@ export function resizeCanvasAndClear(canvas: HTMLCanvasElement, context: CanvasR
|
||||
canvas.style.width = actualW + "px";
|
||||
canvas.style.height = actualH + "px";
|
||||
// console.log("Resizing canvas to", actualW, "x", actualH);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context.clearRect(0, 0, actualW, actualH);
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,20 @@
|
||||
import { globalConfig } from "./config";
|
||||
|
||||
export type GameRoot = import("../game/root").GameRoot;
|
||||
export type Rectangle = import("./rectangle").Rectangle;
|
||||
|
||||
export class DrawParameters {
|
||||
public context: CanvasRenderingContext2D = context;
|
||||
public visibleRect: Rectangle = visibleRect;
|
||||
public desiredAtlasScale: string = desiredAtlasScale;
|
||||
public zoomLevel: number = zoomLevel;
|
||||
public root: GameRoot = root;
|
||||
public context: CanvasRenderingContext2D;
|
||||
public visibleRect: Rectangle;
|
||||
public desiredAtlasScale: string;
|
||||
public zoomLevel: number;
|
||||
public root: GameRoot;
|
||||
|
||||
constructor({ context, visibleRect, desiredAtlasScale, zoomLevel, root }) {
|
||||
this.context = context;
|
||||
this.visibleRect = visibleRect;
|
||||
this.desiredAtlasScale = desiredAtlasScale;
|
||||
this.zoomLevel = zoomLevel;
|
||||
this.root = root;
|
||||
}
|
||||
}
|
||||
|
@ -1,63 +1,94 @@
|
||||
import type { AtlasSprite } from "./sprites";
|
||||
import type { DrawParameters } from "./draw_parameters";
|
||||
|
||||
export type AtlasSprite = import("./sprites").AtlasSprite;
|
||||
export type DrawParameters = import("./draw_parameters").DrawParameters;
|
||||
import { globalConfig } from "./config";
|
||||
import { createLogger } from "./logging";
|
||||
import { Rectangle } from "./rectangle";
|
||||
|
||||
const logger = createLogger("draw_utils");
|
||||
|
||||
export function initDrawUtils() {
|
||||
CanvasRenderingContext2D.prototype.beginRoundedRect = function (x, y, w, h, r) {
|
||||
this.beginPath();
|
||||
|
||||
if (r < 0.05) {
|
||||
this.rect(x, y, w, h);
|
||||
return;
|
||||
}
|
||||
|
||||
if (w < 2 * r) {
|
||||
r = w / 2;
|
||||
}
|
||||
|
||||
if (h < 2 * r) {
|
||||
r = h / 2;
|
||||
}
|
||||
|
||||
this.moveTo(x + r, y);
|
||||
this.arcTo(x + w, y, x + w, y + h, r);
|
||||
this.arcTo(x + w, y + h, x, y + h, r);
|
||||
this.arcTo(x, y + h, x, y, r);
|
||||
this.arcTo(x, y, x + w, y, r);
|
||||
};
|
||||
|
||||
CanvasRenderingContext2D.prototype.beginCircle = function (x, y, r) {
|
||||
this.beginPath();
|
||||
|
||||
if (r < 0.05) {
|
||||
this.rect(x, y, 1, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
this.arc(x, y, r, 0, 2.0 * Math.PI);
|
||||
};
|
||||
}
|
||||
export function drawRotatedSprite({ parameters, sprite, x, y, angle, size, offsetX = 0, offsetY = 0 }: {
|
||||
|
||||
export function drawRotatedSprite({
|
||||
parameters,
|
||||
sprite,
|
||||
x,
|
||||
y,
|
||||
angle,
|
||||
size,
|
||||
offsetX = 0,
|
||||
offsetY = 0,
|
||||
}: {
|
||||
parameters: DrawParameters;
|
||||
sprite: AtlasSprite;
|
||||
x: number;
|
||||
y: number;
|
||||
angle: number;
|
||||
size: number;
|
||||
offsetX: number=;
|
||||
offsetY: number=;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}) {
|
||||
if (angle === 0) {
|
||||
sprite.drawCachedCentered(parameters, x + offsetX, y + offsetY, size);
|
||||
return;
|
||||
}
|
||||
|
||||
parameters.context.translate(x, y);
|
||||
parameters.context.rotate(angle);
|
||||
sprite.drawCachedCentered(parameters, offsetX, offsetY, size, false);
|
||||
parameters.context.rotate(-angle);
|
||||
parameters.context.translate(-x, -y);
|
||||
}
|
||||
|
||||
let warningsShown = 0;
|
||||
|
||||
/**
|
||||
* Draws a sprite with clipping
|
||||
*/
|
||||
export function drawSpriteClipped({ parameters, sprite, x, y, w, h, originalW, originalH }: {
|
||||
export function drawSpriteClipped({
|
||||
parameters,
|
||||
sprite,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
originalW,
|
||||
originalH,
|
||||
}: {
|
||||
parameters: DrawParameters;
|
||||
sprite: HTMLCanvasElement;
|
||||
x: number;
|
||||
@ -72,7 +103,11 @@ export function drawSpriteClipped({ parameters, sprite, x, y, w, h, originalW, o
|
||||
if (!intersection) {
|
||||
// Clipped
|
||||
if (++warningsShown % 200 === 1) {
|
||||
logger.warn("Sprite drawn clipped but it's not on screen - perform culling before (", warningsShown, "warnings)");
|
||||
logger.warn(
|
||||
"Sprite drawn clipped but it's not on screen - perform culling before (",
|
||||
warningsShown,
|
||||
"warnings)"
|
||||
);
|
||||
}
|
||||
if (G_IS_DEV && globalConfig.debug.testClipping) {
|
||||
parameters.context.fillStyle = "yellow";
|
||||
@ -80,9 +115,19 @@ export function drawSpriteClipped({ parameters, sprite, x, y, w, h, originalW, o
|
||||
}
|
||||
return;
|
||||
}
|
||||
parameters.context.drawImage(sprite,
|
||||
// src pos and size
|
||||
((intersection.x - x) / w) * originalW, ((intersection.y - y) / h) * originalH, (originalW * intersection.w) / w, (originalH * intersection.h) / h,
|
||||
// dest pos and size
|
||||
intersection.x, intersection.y, intersection.w, intersection.h);
|
||||
|
||||
parameters.context.drawImage(
|
||||
sprite,
|
||||
// src pos and size
|
||||
((intersection.x - x) / w) * originalW,
|
||||
((intersection.y - y) / h) * originalH,
|
||||
(originalW * intersection.w) / w,
|
||||
(originalH * intersection.h) / h,
|
||||
|
||||
// dest pos and size
|
||||
intersection.x,
|
||||
intersection.y,
|
||||
intersection.w,
|
||||
intersection.h
|
||||
);
|
||||
}
|
||||
|
@ -1,28 +1,36 @@
|
||||
export class ExplainedResult {
|
||||
public result: boolean = result;
|
||||
public reason: string = reason;
|
||||
public result: boolean;
|
||||
public reason: string;
|
||||
|
||||
constructor(result = true, reason: string = null, additionalProps = {}) {
|
||||
this.result = result;
|
||||
this.reason = reason;
|
||||
|
||||
constructor(result = true, reason = null, additionalProps = {}) {
|
||||
// Copy additional props
|
||||
for (const key in additionalProps) {
|
||||
this[key] = additionalProps[key];
|
||||
}
|
||||
}
|
||||
|
||||
isGood() {
|
||||
return !!this.result;
|
||||
}
|
||||
|
||||
isBad() {
|
||||
return !this.result;
|
||||
}
|
||||
|
||||
static good() {
|
||||
return new ExplainedResult(true);
|
||||
}
|
||||
static bad(reason, additionalProps) {
|
||||
|
||||
static bad(reason?: string, additionalProps?: object) {
|
||||
return new ExplainedResult(false, reason, additionalProps);
|
||||
}
|
||||
static requireAll(...args) {
|
||||
|
||||
static requireAll(...args: (() => ExplainedResult)[]) {
|
||||
for (let i = 0; i < args.length; ++i) {
|
||||
const subResult = args[i].call();
|
||||
const subResult = args[i].call(undefined);
|
||||
if (!subResult.isGood()) {
|
||||
return subResult;
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
import { createLogger } from "./logging";
|
||||
|
||||
const logger = createLogger("factory");
|
||||
|
||||
// simple factory pattern
|
||||
export class Factory {
|
||||
public id = id;
|
||||
public entries = [];
|
||||
public entryIds = [];
|
||||
export class Factory<T extends { getId: () => string }> {
|
||||
public entries: T[] = [];
|
||||
public entryIds: string[] = [];
|
||||
public idToEntry = {};
|
||||
|
||||
constructor(id) {
|
||||
}
|
||||
constructor(public id?: string) {}
|
||||
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
register(entry) {
|
||||
|
||||
register(entry: T) {
|
||||
// Extract id
|
||||
const id = entry.getId();
|
||||
assert(id, "Factory: Invalid id for class: " + entry);
|
||||
@ -23,18 +25,18 @@ export class Factory {
|
||||
this.entryIds.push(id);
|
||||
this.idToEntry[id] = entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given id is registered
|
||||
* {}
|
||||
*/
|
||||
hasId(id: string): boolean {
|
||||
return !!this.idToEntry[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an instance by a given id
|
||||
* {}
|
||||
*/
|
||||
findById(id: string): object {
|
||||
findById(id: string): T {
|
||||
const entry = this.idToEntry[id];
|
||||
if (!entry) {
|
||||
logger.error("Object with id", id, "is not registered on factory", this.id, "!");
|
||||
@ -43,23 +45,23 @@ export class Factory {
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all entries
|
||||
* {}
|
||||
*/
|
||||
getEntries(): Array<object> {
|
||||
getEntries(): Array<T> {
|
||||
return this.entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all registered ids
|
||||
* {}
|
||||
*/
|
||||
getAllIds(): Array<string> {
|
||||
return this.entryIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns amount of stored entries
|
||||
* {}
|
||||
*/
|
||||
getNumEntries(): number {
|
||||
return this.entries.length;
|
||||
|
@ -1,48 +1,53 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
import type { StateManager } from "./state_manager";
|
||||
/* typehints:end */
|
||||
|
||||
import { globalConfig } from "./config";
|
||||
import { ClickDetector } from "./click_detector";
|
||||
import { ClickDetector, ClickDetectorConstructorArgs } from "./click_detector";
|
||||
import { logSection, createLogger } from "./logging";
|
||||
import { InputReceiver } from "./input_receiver";
|
||||
import { waitNextFrame } from "./utils";
|
||||
import { RequestChannel } from "./request_channel";
|
||||
import { MUSIC } from "../platform/sound";
|
||||
|
||||
const logger = createLogger("game_state");
|
||||
|
||||
/**
|
||||
* Basic state of the game state machine. This is the base of the whole game
|
||||
*/
|
||||
export class GameState {
|
||||
public key = key;
|
||||
export abstract class GameState {
|
||||
public stateManager: StateManager = null;
|
||||
public app: Application = null;
|
||||
public fadingOut = false;
|
||||
public clickDetectors: Array<ClickDetector> = [];
|
||||
public inputReciever = new InputReceiver("state-" + key);
|
||||
public inputReceiver: InputReceiver;
|
||||
public asyncChannel = new RequestChannel();
|
||||
|
||||
public htmlElement: HTMLElement;
|
||||
|
||||
/**
|
||||
* Constructs a new state with the given id
|
||||
*/
|
||||
|
||||
constructor(key) {
|
||||
this.inputReciever.backButton.add(this.onBackButton, this);
|
||||
constructor(public key) {
|
||||
this.inputReceiver = new InputReceiver("state-" + key);
|
||||
this.inputReceiver.backButton.add(this.onBackButton, this);
|
||||
}
|
||||
|
||||
//// GETTERS / HELPER METHODS ////
|
||||
|
||||
/**
|
||||
* Returns the states key
|
||||
* {}
|
||||
*/
|
||||
getKey(): string {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the html element of the state
|
||||
* {}
|
||||
*/
|
||||
getDivElement(): HTMLElement {
|
||||
return document.getElementById("state_" + this.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers to a new state
|
||||
*/
|
||||
@ -51,8 +56,10 @@ export class GameState {
|
||||
logger.warn("Skipping move to '" + stateKey + "' since already fading out");
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up event listeners
|
||||
this.internalCleanUpClickDetectors();
|
||||
|
||||
// Fading
|
||||
const fadeTime = this.internalGetFadeInOutTime();
|
||||
const doFade = !skipFadeOut && this.getHasFadeOut() && fadeTime !== 0;
|
||||
@ -63,16 +70,16 @@ export class GameState {
|
||||
setTimeout(() => {
|
||||
this.stateManager.moveToState(stateKey, payload);
|
||||
}, fadeTime);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.stateManager.moveToState(stateKey, payload);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks clicks on a given element and calls the given callback *on this state*.
|
||||
* If you want to call another function wrap it inside a lambda.
|
||||
*/
|
||||
trackClicks(element: Element, handler: function():void, args: import("./click_detector").ClickDetectorConstructorArgs= = {}) {
|
||||
trackClicks(element: Element, handler: () => void, args: ClickDetectorConstructorArgs = {}) {
|
||||
const detector = new ClickDetector(element, args);
|
||||
detector.click.add(handler, this);
|
||||
if (G_IS_DEV) {
|
||||
@ -82,50 +89,62 @@ export class GameState {
|
||||
}
|
||||
this.clickDetectors.push(detector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all promises on the api as well as our async channel
|
||||
*/
|
||||
cancelAllAsyncOperations() {
|
||||
this.asyncChannel.cancelAll();
|
||||
}
|
||||
|
||||
//// CALLBACKS ////
|
||||
|
||||
/**
|
||||
* Callback when entering the state, to be overriddemn
|
||||
*/
|
||||
onEnter(payload: any) { }
|
||||
onEnter(payload: any) {}
|
||||
|
||||
/**
|
||||
* Callback when leaving the state
|
||||
*/
|
||||
onLeave() { }
|
||||
onLeave() {}
|
||||
|
||||
/**
|
||||
* Callback when the app got paused (on android, this means in background)
|
||||
*/
|
||||
onAppPause() { }
|
||||
onAppPause() {}
|
||||
|
||||
/**
|
||||
* Callback when the app got resumed (on android, this means in foreground again)
|
||||
*/
|
||||
onAppResume() { }
|
||||
onAppResume() {}
|
||||
|
||||
/**
|
||||
* Render callback
|
||||
*/
|
||||
onRender(dt: number) { }
|
||||
onRender(dt: number) {}
|
||||
|
||||
/**
|
||||
* Background tick callback, called while the game is inactiev
|
||||
*/
|
||||
onBackgroundTick(dt: number) { }
|
||||
onBackgroundTick(dt: number) {}
|
||||
|
||||
/**
|
||||
* Called when the screen resized
|
||||
*/
|
||||
onResized(w: number, h: number) { }
|
||||
onResized(w: number, h: number) {}
|
||||
|
||||
/**
|
||||
* Internal backbutton handler, called when the hardware back button is pressed or
|
||||
* the escape key is pressed
|
||||
*/
|
||||
onBackButton() { }
|
||||
onBackButton() {}
|
||||
|
||||
//// INTERFACE ////
|
||||
|
||||
/**
|
||||
* Should return how many mulliseconds to fade in / out the state. Not recommended to override!
|
||||
* {} Time in milliseconds to fade out
|
||||
* @returns Time in milliseconds to fade out
|
||||
*/
|
||||
getInOutFadeTime(): number {
|
||||
if (globalConfig.debug.noArtificialDelays) {
|
||||
@ -133,38 +152,36 @@ export class GameState {
|
||||
}
|
||||
return 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return whether to fade in the game state. This will then apply the right css classes
|
||||
* for the fadein.
|
||||
* {}
|
||||
*/
|
||||
getHasFadeIn(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return whether to fade out the game state. This will then apply the right css classes
|
||||
* for the fadeout and wait the delay before moving states
|
||||
* {}
|
||||
*/
|
||||
getHasFadeOut(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this state should get paused if it does not have focus
|
||||
* {} true to pause the updating of the game
|
||||
* @returns true to pause the updating of the game
|
||||
*/
|
||||
getPauseOnFocusLost(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return the html code of the state.
|
||||
* {}
|
||||
* @abstract
|
||||
*/
|
||||
getInnerHTML(): string {
|
||||
abstract;
|
||||
return "";
|
||||
}
|
||||
abstract getInnerHTML(): string;
|
||||
|
||||
/**
|
||||
* Returns if the state has an unload confirmation, this is the
|
||||
* "Are you sure you want to leave the page" message.
|
||||
@ -172,46 +189,52 @@ export class GameState {
|
||||
getHasUnloadConfirmation() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return the theme music for this state
|
||||
* {}
|
||||
*/
|
||||
getThemeMusic(): string | null {
|
||||
return MUSIC.menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return true if the player is currently ingame
|
||||
* {}
|
||||
*/
|
||||
getIsIngame(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return whether to clear the whole body content before entering the state.
|
||||
* {}
|
||||
*/
|
||||
getRemovePreviousContent(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////
|
||||
|
||||
//// INTERNAL ////
|
||||
|
||||
/**
|
||||
* Internal callback from the manager. Do not override!
|
||||
*/
|
||||
internalRegisterCallback(stateManager: StateManager, app) {
|
||||
internalRegisterCallback(stateManager: StateManager, app: Application) {
|
||||
assert(stateManager, "No state manager");
|
||||
assert(app, "No app");
|
||||
this.stateManager = stateManager;
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal callback when entering the state. Do not override!
|
||||
*/
|
||||
internalEnterCallback(payload: any, callCallback: boolean = true) {
|
||||
logSection(this.key, "#26a69a");
|
||||
this.app.inputMgr.pushReciever(this.inputReciever);
|
||||
this.app.inputMgr.pushReciever(this.inputReceiver);
|
||||
|
||||
this.htmlElement = this.getDivElement();
|
||||
this.htmlElement.classList.add("active");
|
||||
|
||||
// Apply classes in the next frame so the css transition keeps up
|
||||
waitNextFrame().then(() => {
|
||||
if (this.htmlElement) {
|
||||
@ -219,33 +242,38 @@ export class GameState {
|
||||
this.htmlElement.classList.remove("fadingIn");
|
||||
}
|
||||
});
|
||||
|
||||
// Call handler
|
||||
if (callCallback) {
|
||||
this.onEnter(payload);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal callback when the state is left. Do not override!
|
||||
*/
|
||||
internalLeaveCallback() {
|
||||
this.onLeave();
|
||||
this.htmlElement.classList.remove("active");
|
||||
this.app.inputMgr.popReciever(this.inputReciever);
|
||||
this.app.inputMgr.popReciever(this.inputReceiver);
|
||||
this.internalCleanUpClickDetectors();
|
||||
this.asyncChannel.cancelAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal app pause callback
|
||||
*/
|
||||
internalOnAppPauseCallback() {
|
||||
this.onAppPause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal app resume callback
|
||||
*/
|
||||
internalOnAppResumeCallback() {
|
||||
this.onAppResume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up all click detectors
|
||||
*/
|
||||
@ -257,16 +285,17 @@ export class GameState {
|
||||
this.clickDetectors = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to get the HTML of the game state.
|
||||
* {}
|
||||
*/
|
||||
internalGetFullHtml(): string {
|
||||
return this.getInnerHTML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to compute the time to fade in / out
|
||||
* {} time to fade in / out in ms
|
||||
* @returns time to fade in / out in ms
|
||||
*/
|
||||
internalGetFadeInOutTime(): number {
|
||||
if (G_IS_DEV && globalConfig.debug.fastGameEnter) {
|
||||
|
@ -1,22 +1,27 @@
|
||||
import { SingletonFactory } from "./singleton_factory";
|
||||
import { Factory } from "./factory";
|
||||
export type BaseGameSpeed = import("../game/time/base_game_speed").BaseGameSpeed;
|
||||
export type Component = import("../game/component").Component;
|
||||
export type BaseItem = import("../game/base_item").BaseItem;
|
||||
export type GameMode = import("../game/game_mode").GameMode;
|
||||
export type MetaBuilding = import("../game/meta_building").MetaBuilding;
|
||||
|
||||
export let gMetaBuildingRegistry: SingletonFactoryTemplate<MetaBuilding> = new SingletonFactory();
|
||||
import { BaseGameSpeed } from "../game/time/base_game_speed";
|
||||
import { Component } from "../game/component";
|
||||
import { BaseItem } from "../game/base_item";
|
||||
import { GameMode } from "../game/game_mode";
|
||||
import { MetaBuilding } from "../game/meta_building";
|
||||
|
||||
// These factories are here to remove circular dependencies
|
||||
|
||||
export let gMetaBuildingRegistry = new SingletonFactory<MetaBuilding>();
|
||||
|
||||
export let gBuildingsByCategory: {
|
||||
[idx: string]: Array<Class<MetaBuilding>>;
|
||||
} = null;
|
||||
export let gComponentRegistry: FactoryTemplate<Component> = new Factory("component");
|
||||
export let gGameModeRegistry: FactoryTemplate<GameMode> = new Factory("gameMode");
|
||||
export let gGameSpeedRegistry: FactoryTemplate<BaseGameSpeed> = new Factory("gamespeed");
|
||||
export let gItemRegistry: FactoryTemplate<BaseItem> = new Factory("item");
|
||||
|
||||
export let gComponentRegistry = new Factory<Component>("component");
|
||||
export let gGameModeRegistry = new Factory<GameMode>("gameMode");
|
||||
export let gGameSpeedRegistry = new Factory<BaseGameSpeed>("gamespeed");
|
||||
export let gItemRegistry = new Factory<BaseItem>("item");
|
||||
|
||||
// Helpers
|
||||
export function initBuildingsByCategory(buildings: {
|
||||
[idx: string]: Array<Class<MetaBuilding>>;
|
||||
}) {
|
||||
|
||||
export function initBuildingsByCategory(buildings: { [idx: string]: Array<Class<MetaBuilding>> }) {
|
||||
gBuildingsByCategory = buildings;
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
/**
|
||||
* Used for the bug reporter, and the click detector which both have no handles to this.
|
||||
* It would be nicer to have no globals, but this is the only one. I promise!
|
||||
*/
|
||||
export let GLOBAL_APP: Application = null;
|
||||
|
||||
export function setGlobalApp(app: Application) {
|
||||
assert(!GLOBAL_APP, "Create application twice!");
|
||||
GLOBAL_APP = app;
|
||||
}
|
||||
|
||||
export const BUILD_OPTIONS = {
|
||||
HAVE_ASSERT: G_HAVE_ASSERT,
|
||||
APP_ENVIRONMENT: G_APP_ENVIRONMENT,
|
||||
|
@ -1,91 +1,120 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
import { Application } from "../application";
|
||||
import type { InputReceiver } from "./input_receiver";
|
||||
/* typehints:end */
|
||||
|
||||
import { Signal, STOP_PROPAGATION } from "./signal";
|
||||
import { createLogger } from "./logging";
|
||||
import { arrayDeleteValue, fastArrayDeleteValue } from "./utils";
|
||||
const logger = createLogger("input_distributor");
|
||||
export class InputDistributor {
|
||||
public app = app;
|
||||
public recieverStack: Array<InputReceiver> = [];
|
||||
public filters: Array<function(: boolean):boolean> = [];
|
||||
public app: Application;
|
||||
public receiverStack: Array<InputReceiver> = [];
|
||||
public filters: Array<(arg: any) => boolean> = [];
|
||||
/** All keys which are currently down */
|
||||
public keysDown = new Set();
|
||||
|
||||
constructor(app) {
|
||||
constructor(app: Application) {
|
||||
this.app = app;
|
||||
this.bindToEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a new filter which can filter and reject events
|
||||
*/
|
||||
installFilter(filter: function(: boolean):boolean) {
|
||||
installFilter(filter: (arg: any) => boolean) {
|
||||
this.filters.push(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an attached filter
|
||||
*/
|
||||
dismountFilter(filter: function(: boolean):boolean) {
|
||||
dismountFilter(filter: (arg: any) => boolean) {
|
||||
fastArrayDeleteValue(this.filters, filter);
|
||||
}
|
||||
pushReciever(reciever: InputReceiver) {
|
||||
|
||||
pushReciever(reciever: InputReceiver) {
|
||||
if (this.isRecieverAttached(reciever)) {
|
||||
assert(false, "Can not add reciever " + reciever.context + " twice");
|
||||
logger.error("Can not add reciever", reciever.context, "twice");
|
||||
return;
|
||||
}
|
||||
this.recieverStack.push(reciever);
|
||||
if (this.recieverStack.length > 10) {
|
||||
logger.error("Reciever stack is huge, probably some dead receivers arround:", this.recieverStack.map(x => x.context));
|
||||
this.receiverStack.push(reciever);
|
||||
|
||||
if (this.receiverStack.length > 10) {
|
||||
logger.error(
|
||||
"Reciever stack is huge, probably some dead receivers arround:",
|
||||
this.receiverStack.map(x => x.context)
|
||||
);
|
||||
}
|
||||
}
|
||||
popReciever(reciever: InputReceiver) {
|
||||
if (this.recieverStack.indexOf(reciever) < 0) {
|
||||
|
||||
popReciever(reciever: InputReceiver) {
|
||||
if (this.receiverStack.indexOf(reciever) < 0) {
|
||||
assert(false, "Can not pop reciever " + reciever.context + " since its not contained");
|
||||
logger.error("Can not pop reciever", reciever.context, "since its not contained");
|
||||
return;
|
||||
}
|
||||
if (this.recieverStack[this.recieverStack.length - 1] !== reciever) {
|
||||
logger.warn("Popping reciever", reciever.context, "which is not on top of the stack. Stack is: ", this.recieverStack.map(x => x.context));
|
||||
if (this.receiverStack[this.receiverStack.length - 1] !== reciever) {
|
||||
logger.warn(
|
||||
"Popping reciever",
|
||||
reciever.context,
|
||||
"which is not on top of the stack. Stack is: ",
|
||||
this.receiverStack.map(x => x.context)
|
||||
);
|
||||
}
|
||||
arrayDeleteValue(this.recieverStack, reciever);
|
||||
arrayDeleteValue(this.receiverStack, reciever);
|
||||
}
|
||||
isRecieverAttached(reciever: InputReceiver) {
|
||||
return this.recieverStack.indexOf(reciever) >= 0;
|
||||
|
||||
isRecieverAttached(reciever: InputReceiver) {
|
||||
return this.receiverStack.indexOf(reciever) >= 0;
|
||||
}
|
||||
isRecieverOnTop(reciever: InputReceiver) {
|
||||
return (this.isRecieverAttached(reciever) &&
|
||||
this.recieverStack[this.recieverStack.length - 1] === reciever);
|
||||
|
||||
isRecieverOnTop(reciever: InputReceiver) {
|
||||
return (
|
||||
this.isRecieverAttached(reciever) &&
|
||||
this.receiverStack[this.receiverStack.length - 1] === reciever
|
||||
);
|
||||
}
|
||||
makeSureAttachedAndOnTop(reciever: InputReceiver) {
|
||||
|
||||
makeSureAttachedAndOnTop(reciever: InputReceiver) {
|
||||
this.makeSureDetached(reciever);
|
||||
this.pushReciever(reciever);
|
||||
}
|
||||
makeSureDetached(reciever: InputReceiver) {
|
||||
|
||||
makeSureDetached(reciever: InputReceiver) {
|
||||
if (this.isRecieverAttached(reciever)) {
|
||||
arrayDeleteValue(this.recieverStack, reciever);
|
||||
arrayDeleteValue(this.receiverStack, reciever);
|
||||
}
|
||||
}
|
||||
destroyReceiver(reciever: InputReceiver) {
|
||||
|
||||
destroyReceiver(reciever: InputReceiver) {
|
||||
this.makeSureDetached(reciever);
|
||||
reciever.cleanup();
|
||||
}
|
||||
|
||||
// Internal
|
||||
|
||||
getTopReciever() {
|
||||
if (this.recieverStack.length > 0) {
|
||||
return this.recieverStack[this.recieverStack.length - 1];
|
||||
if (this.receiverStack.length > 0) {
|
||||
return this.receiverStack[this.receiverStack.length - 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bindToEvents() {
|
||||
window.addEventListener("popstate", this.handleBackButton.bind(this), false);
|
||||
document.addEventListener("backbutton", this.handleBackButton.bind(this), false);
|
||||
|
||||
window.addEventListener("keydown", this.handleKeyMouseDown.bind(this));
|
||||
window.addEventListener("keyup", this.handleKeyMouseUp.bind(this));
|
||||
|
||||
window.addEventListener("mousedown", this.handleKeyMouseDown.bind(this));
|
||||
window.addEventListener("mouseup", this.handleKeyMouseUp.bind(this));
|
||||
|
||||
window.addEventListener("blur", this.handleBlur.bind(this));
|
||||
|
||||
document.addEventListener("paste", this.handlePaste.bind(this));
|
||||
}
|
||||
|
||||
forwardToReceiver(eventId, payload = null) {
|
||||
// Check filters
|
||||
for (let i = 0; i < this.filters.length; ++i) {
|
||||
@ -93,6 +122,7 @@ export class InputDistributor {
|
||||
return STOP_PROPAGATION;
|
||||
}
|
||||
}
|
||||
|
||||
const reciever = this.getTopReciever();
|
||||
if (!reciever) {
|
||||
logger.warn("Dismissing event because not reciever was found:", eventId);
|
||||
@ -102,11 +132,13 @@ export class InputDistributor {
|
||||
assert(signal instanceof Signal, "Not a valid event id");
|
||||
return signal.dispatch(payload);
|
||||
}
|
||||
handleBackButton(event: Event) {
|
||||
|
||||
handleBackButton(event: Event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.forwardToReceiver("backButton");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles when the page got blurred
|
||||
*/
|
||||
@ -114,13 +146,15 @@ export class InputDistributor {
|
||||
this.forwardToReceiver("pageBlur", {});
|
||||
this.keysDown.clear();
|
||||
}
|
||||
|
||||
|
||||
handlePaste(ev) {
|
||||
this.forwardToReceiver("paste", ev);
|
||||
}
|
||||
handleKeyMouseDown(event: KeyboardEvent | MouseEvent) {
|
||||
|
||||
handleKeyMouseDown(event: KeyboardEvent | MouseEvent) {
|
||||
const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
|
||||
if (keyCode === 4 || // MB4
|
||||
if (
|
||||
keyCode === 4 || // MB4
|
||||
keyCode === 5 || // MB5
|
||||
keyCode === 9 || // TAB
|
||||
keyCode === 16 || // SHIFT
|
||||
@ -130,18 +164,23 @@ export class InputDistributor {
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
const isInitial = !this.keysDown.has(keyCode);
|
||||
this.keysDown.add(keyCode);
|
||||
if (this.forwardToReceiver("keydown", {
|
||||
keyCode: keyCode,
|
||||
shift: event.shiftKey,
|
||||
alt: event.altKey,
|
||||
ctrl: event.ctrlKey,
|
||||
initial: isInitial,
|
||||
event,
|
||||
}) === STOP_PROPAGATION) {
|
||||
|
||||
if (
|
||||
this.forwardToReceiver("keydown", {
|
||||
keyCode: keyCode,
|
||||
shift: event.shiftKey,
|
||||
alt: event.altKey,
|
||||
ctrl: event.ctrlKey,
|
||||
initial: isInitial,
|
||||
event,
|
||||
}) === STOP_PROPAGATION
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyCode === 27) {
|
||||
// Escape key
|
||||
event.preventDefault();
|
||||
@ -149,9 +188,11 @@ export class InputDistributor {
|
||||
return this.forwardToReceiver("backButton");
|
||||
}
|
||||
}
|
||||
handleKeyMouseUp(event: KeyboardEvent | MouseEvent) {
|
||||
|
||||
handleKeyMouseUp(event: KeyboardEvent | MouseEvent) {
|
||||
const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
|
||||
this.keysDown.delete(keyCode);
|
||||
|
||||
this.forwardToReceiver("keyup", {
|
||||
keyCode: keyCode,
|
||||
shift: event.shiftKey,
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Signal } from "./signal";
|
||||
export class InputReceiver {
|
||||
public context = context;
|
||||
public backButton = new Signal();
|
||||
public keydown = new Signal();
|
||||
public keyup = new Signal();
|
||||
@ -8,13 +7,14 @@ export class InputReceiver {
|
||||
public destroyed = new Signal();
|
||||
public paste = new Signal();
|
||||
|
||||
constructor(context = "unknown") {
|
||||
}
|
||||
constructor(public context: string = "unknown") {}
|
||||
|
||||
cleanup() {
|
||||
this.backButton.removeAll();
|
||||
this.keydown.removeAll();
|
||||
this.keyup.removeAll();
|
||||
this.paste.removeAll();
|
||||
|
||||
this.destroyed.dispatch();
|
||||
}
|
||||
}
|
||||
|
@ -2,25 +2,27 @@ import { makeOffscreenBuffer } from "./buffer_utils";
|
||||
import { AtlasSprite, BaseSprite, RegularSprite, SpriteAtlasLink } from "./sprites";
|
||||
import { cachebust } from "./cachebust";
|
||||
import { createLogger } from "./logging";
|
||||
export type Application = import("../application").Application;
|
||||
export type AtlasDefinition = import("./atlas_definitions").AtlasDefinition;
|
||||
|
||||
import type { Application } from "../application";
|
||||
import type { AtlasDefinition } from "./atlas_definitions";
|
||||
|
||||
const logger = createLogger("loader");
|
||||
|
||||
const missingSpriteIds = {};
|
||||
|
||||
class LoaderImpl {
|
||||
public app = null;
|
||||
public sprites: Map<string, BaseSprite> = new Map();
|
||||
public rawImages = [];
|
||||
public spriteNotFoundSprite: AtlasSprite;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
linkAppAfterBoot(app: Application) {
|
||||
linkAppAfterBoot(app: Application) {
|
||||
this.app = app;
|
||||
this.makeSpriteNotFoundCanvas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a given sprite from the cache
|
||||
* {}
|
||||
*/
|
||||
getSpriteInternal(key: string): BaseSprite {
|
||||
const sprite = this.sprites.get(key);
|
||||
@ -34,45 +36,53 @@ class LoaderImpl {
|
||||
}
|
||||
return sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an atlas sprite from the cache
|
||||
* {}
|
||||
*/
|
||||
getSprite(key: string): AtlasSprite {
|
||||
const sprite = this.getSpriteInternal(key);
|
||||
assert(sprite instanceof AtlasSprite || sprite === this.spriteNotFoundSprite, "Not an atlas sprite");
|
||||
return sprite as AtlasSprite);
|
||||
return sprite as AtlasSprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a regular sprite from the cache
|
||||
* {}
|
||||
*/
|
||||
getRegularSprite(key: string): RegularSprite {
|
||||
const sprite = this.getSpriteInternal(key);
|
||||
assert(sprite instanceof RegularSprite || sprite === this.spriteNotFoundSprite, "Not a regular sprite");
|
||||
return sprite as RegularSprite);
|
||||
assert(
|
||||
sprite instanceof RegularSprite || sprite === this.spriteNotFoundSprite,
|
||||
"Not a regular sprite"
|
||||
);
|
||||
return sprite as RegularSprite;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {}
|
||||
*/
|
||||
internalPreloadImage(key: string, progressHandler: (progress: number) => void): Promise<HTMLImageElement | null> {
|
||||
internalPreloadImage(
|
||||
key: string,
|
||||
progressHandler: (progress: number) => void
|
||||
): Promise<HTMLImageElement | null> {
|
||||
return this.app.backgroundResourceLoader
|
||||
.preloadWithProgress("res/" + key, progress => {
|
||||
progressHandler(progress);
|
||||
})
|
||||
progressHandler(progress);
|
||||
})
|
||||
.then(url => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.addEventListener("load", () => resolve(image));
|
||||
image.addEventListener("error", err => reject("Failed to load sprite " + key + ": " + err));
|
||||
image.src = url;
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.addEventListener("load", () => resolve(image));
|
||||
image.addEventListener("error", err =>
|
||||
reject("Failed to load sprite " + key + ": " + err)
|
||||
);
|
||||
image.src = url;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloads a sprite
|
||||
* {}
|
||||
*/
|
||||
preloadCSSSprite(key: string, progressHandler: (progress: number) => void): Promise<void> {
|
||||
return this.internalPreloadImage(key, progressHandler).then(image => {
|
||||
@ -83,9 +93,9 @@ class LoaderImpl {
|
||||
this.rawImages.push(image);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloads an atlas
|
||||
* {}
|
||||
*/
|
||||
preloadAtlas(atlas: AtlasDefinition, progressHandler: (progress: number) => void): Promise<void> {
|
||||
return this.internalPreloadImage(atlas.getFullSourcePath(), progressHandler).then(image => {
|
||||
@ -94,11 +104,15 @@ class LoaderImpl {
|
||||
return this.internalParseAtlas(atlas, image);
|
||||
});
|
||||
}
|
||||
internalParseAtlas({ meta: { scale }, sourceData }: AtlasDefinition, loadedImage: HTMLImageElement) {
|
||||
|
||||
internalParseAtlas({ meta: { scale }, sourceData }: AtlasDefinition, loadedImage: HTMLImageElement) {
|
||||
this.rawImages.push(loadedImage);
|
||||
|
||||
for (const spriteName in sourceData) {
|
||||
const { frame, sourceSize, spriteSourceSize } = sourceData[spriteName];
|
||||
let sprite = this.sprites.get(spriteName) as AtlasSprite);
|
||||
|
||||
let sprite = this.sprites.get(spriteName) as AtlasSprite;
|
||||
|
||||
if (!sprite) {
|
||||
sprite = new AtlasSprite(spriteName);
|
||||
this.sprites.set(spriteName, sprite);
|
||||
@ -106,6 +120,7 @@ class LoaderImpl {
|
||||
if (sprite.frozen) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const link = new SpriteAtlasLink({
|
||||
packedX: frame.x,
|
||||
packedY: frame.y,
|
||||
@ -117,9 +132,11 @@ class LoaderImpl {
|
||||
w: sourceSize.w,
|
||||
h: sourceSize.h,
|
||||
});
|
||||
|
||||
sprite.linksByResolution[scale] = link;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the canvas which shows the question mark, shown when a sprite was not found
|
||||
*/
|
||||
@ -156,4 +173,5 @@ class LoaderImpl {
|
||||
this.spriteNotFoundSprite = sprite;
|
||||
}
|
||||
}
|
||||
|
||||
export const Loader = new LoaderImpl();
|
||||
|
@ -8,10 +8,7 @@ Logging functions
|
||||
* Base logger class
|
||||
*/
|
||||
class Logger {
|
||||
public context = context;
|
||||
|
||||
constructor(context) {
|
||||
}
|
||||
constructor(public context: string) {}
|
||||
debug(...args) {
|
||||
globalDebug(this.context, ...args);
|
||||
}
|
||||
@ -25,10 +22,12 @@ class Logger {
|
||||
globalError(this.context, ...args);
|
||||
}
|
||||
}
|
||||
export function createLogger(context) {
|
||||
|
||||
export function createLogger(context: string) {
|
||||
return new Logger(context);
|
||||
}
|
||||
function prepareObjectForLogging(obj, maxDepth = 1) {
|
||||
|
||||
function prepareObjectForLogging(obj: object, maxDepth = 1) {
|
||||
if (!window.Sentry) {
|
||||
// Not required without sentry
|
||||
return obj;
|
||||
@ -39,20 +38,33 @@ function prepareObjectForLogging(obj, maxDepth = 1) {
|
||||
const result = {};
|
||||
for (const key in obj) {
|
||||
const val = obj[key];
|
||||
|
||||
if (typeof val === "object") {
|
||||
if (maxDepth > 0) {
|
||||
result[key] = prepareObjectForLogging(val, maxDepth - 1);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
result[key] = "[object]";
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
result[key] = val;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
type SerializedError = {
|
||||
type: string;
|
||||
|
||||
message?: string;
|
||||
name?: string;
|
||||
stack?: string;
|
||||
|
||||
filename?: string;
|
||||
lineno?: number;
|
||||
colno?: number;
|
||||
error?: string | SerializedError;
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializes an error
|
||||
*/
|
||||
@ -60,17 +72,17 @@ export function serializeError(err: Error | ErrorEvent) {
|
||||
if (!err) {
|
||||
return null;
|
||||
}
|
||||
const result = {
|
||||
|
||||
const result: SerializedError = {
|
||||
type: err.constructor.name,
|
||||
};
|
||||
|
||||
if (err instanceof Error) {
|
||||
result.message = err.message;
|
||||
result.name = err.name;
|
||||
result.stack = err.stack;
|
||||
result.type = "{type.Error}";
|
||||
}
|
||||
else if (err instanceof ErrorEvent) {
|
||||
} else if (err instanceof ErrorEvent) {
|
||||
result.filename = err.filename;
|
||||
result.message = err.message;
|
||||
result.lineno = err.lineno;
|
||||
@ -78,26 +90,26 @@ export function serializeError(err: Error | ErrorEvent) {
|
||||
result.type = "{type.ErrorEvent}";
|
||||
if (err.error) {
|
||||
result.error = serializeError(err.error);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
result.error = "{not-provided}";
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
result.type = "{unkown-type:" + typeof err + "}";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes an event
|
||||
*/
|
||||
function serializeEvent(event: Event) {
|
||||
let result = {
|
||||
return {
|
||||
type: "{type.Event:" + typeof event + "}",
|
||||
eventType: event.type,
|
||||
};
|
||||
result.eventType = event.type;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a json payload
|
||||
*/
|
||||
@ -113,25 +125,30 @@ function preparePayload(key: string, value: any) {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringifies an object containing circular references and errors
|
||||
*/
|
||||
export function stringifyObjectContainingErrors(payload: any) {
|
||||
return circularJson.stringify(payload, preparePayload);
|
||||
}
|
||||
|
||||
export function globalDebug(context, ...args) {
|
||||
if (G_IS_DEV) {
|
||||
logInternal(context, console.log, prepareArgsForLogging(args));
|
||||
}
|
||||
}
|
||||
|
||||
export function globalLog(context, ...args) {
|
||||
// eslint-disable-next-line no-console
|
||||
logInternal(context, console.log, prepareArgsForLogging(args));
|
||||
}
|
||||
|
||||
export function globalWarn(context, ...args) {
|
||||
// eslint-disable-next-line no-console
|
||||
logInternal(context, console.warn, prepareArgsForLogging(args));
|
||||
}
|
||||
|
||||
export function globalError(context, ...args) {
|
||||
args = prepareArgsForLogging(args);
|
||||
// eslint-disable-next-line no-console
|
||||
@ -143,66 +160,77 @@ export function globalError(context, ...args) {
|
||||
});
|
||||
}
|
||||
}
|
||||
function prepareArgsForLogging(args) {
|
||||
|
||||
function prepareArgsForLogging(args: any[]) {
|
||||
let result = [];
|
||||
for (let i = 0; i < args.length; ++i) {
|
||||
result.push(prepareObjectForLogging(args[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function internalBuildStringFromArgs(args: Array<any>) {
|
||||
let result = [];
|
||||
|
||||
for (let i = 0; i < args.length; ++i) {
|
||||
let arg = args[i];
|
||||
if (typeof arg === "string" ||
|
||||
if (
|
||||
typeof arg === "string" ||
|
||||
typeof arg === "number" ||
|
||||
typeof arg === "boolean" ||
|
||||
arg === null ||
|
||||
arg === undefined) {
|
||||
arg === undefined
|
||||
) {
|
||||
result.push("" + arg);
|
||||
}
|
||||
else if (arg instanceof Error) {
|
||||
} else if (arg instanceof Error) {
|
||||
result.push(arg.message);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
result.push("[object]");
|
||||
}
|
||||
}
|
||||
return result.join(" ");
|
||||
}
|
||||
export function logSection(name, color) {
|
||||
|
||||
export function logSection(name: string, color: string) {
|
||||
while (name.length <= 14) {
|
||||
name = " " + name + " ";
|
||||
}
|
||||
name = name.padEnd(19, " ");
|
||||
const lineCss = "letter-spacing: -3px; color: " + color + "; font-size: 6px; background: #eee; color: #eee;";
|
||||
|
||||
const lineCss =
|
||||
"letter-spacing: -3px; color: " + color + "; font-size: 6px; background: #eee; color: #eee;";
|
||||
const line = "%c----------------------------";
|
||||
console.log("\n" + line + " %c" + name + " " + line + "\n", lineCss, "color: " + color, lineCss);
|
||||
}
|
||||
function extractHandleContext(handle) {
|
||||
|
||||
function extractHandleContext(handle: string | ({ new (): any; name: string } & Function)) {
|
||||
let context = handle || "unknown";
|
||||
|
||||
|
||||
if (handle && handle.constructor && handle.constructor.name) {
|
||||
|
||||
context = handle.constructor.name;
|
||||
if (context === "String") {
|
||||
context = handle;
|
||||
}
|
||||
}
|
||||
if (handle && handle.name) {
|
||||
|
||||
if (handle && typeof handle !== "string" && handle.name) {
|
||||
context = handle.name;
|
||||
}
|
||||
return context + "";
|
||||
}
|
||||
|
||||
function logInternal(handle, consoleMethod, args) {
|
||||
const context = extractHandleContext(handle).padEnd(20, " ");
|
||||
const labelColor = handle && handle.LOG_LABEL_COLOR ? handle.LOG_LABEL_COLOR : "#aaa";
|
||||
if (G_IS_DEV && globalConfig.debug.logTimestamps) {
|
||||
const timestamp = "⏱ %c" + (Math.floor(performance.now()) + "").padEnd(6, " ") + "";
|
||||
consoleMethod.call(console, timestamp + " %c" + context, "color: #7f7;", "color: " + labelColor + ";", ...args);
|
||||
}
|
||||
else {
|
||||
consoleMethod.call(
|
||||
console,
|
||||
timestamp + " %c" + context,
|
||||
"color: #7f7;",
|
||||
"color: " + labelColor + ";",
|
||||
...args
|
||||
);
|
||||
} else {
|
||||
// if (G_IS_DEV && !globalConfig.debug.disableLoggingLogSources) {
|
||||
consoleMethod.call(console, "%c" + context, "color: " + labelColor, ...args);
|
||||
// } else {
|
||||
|
@ -9,9 +9,11 @@
|
||||
// LZ-based compression algorithm, version 1.4.4
|
||||
const fromCharCode = String.fromCharCode;
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
const keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
|
||||
const baseReverseDic = {};
|
||||
function getBaseValue(alphabet, character) {
|
||||
|
||||
function getBaseValue(alphabet: string, character: string) {
|
||||
if (!baseReverseDic[alphabet]) {
|
||||
baseReverseDic[alphabet] = {};
|
||||
for (let i = 0; i < alphabet.length; i++) {
|
||||
@ -20,8 +22,9 @@ function getBaseValue(alphabet, character) {
|
||||
}
|
||||
return baseReverseDic[alphabet][character];
|
||||
}
|
||||
|
||||
//compress into uint8array (UCS-2 big endian format)
|
||||
export function compressU8(uncompressed) {
|
||||
export function compressU8(uncompressed: string) {
|
||||
let compressed = compress(uncompressed);
|
||||
let buf = new Uint8Array(compressed.length * 2); // 2 bytes per character
|
||||
for (let i = 0, TotalLen = compressed.length; i < TotalLen; i++) {
|
||||
@ -31,10 +34,12 @@ export function compressU8(uncompressed) {
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Compreses with header
|
||||
export function compressU8WHeader(uncompressed: string, header: number) {
|
||||
let compressed = compress(uncompressed);
|
||||
let buf = new Uint8Array(2 + compressed.length * 2); // 2 bytes per character
|
||||
|
||||
buf[0] = header >>> 8;
|
||||
buf[1] = header % 256;
|
||||
for (let i = 0, TotalLen = compressed.length; i < TotalLen; i++) {
|
||||
@ -44,6 +49,7 @@ export function compressU8WHeader(uncompressed: string, header: number) {
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
//decompress from uint8array (UCS-2 big endian format)
|
||||
export function decompressU8WHeader(compressed: Uint8Array) {
|
||||
// let buf = new Array(compressed.length / 2); // 2 bytes per character
|
||||
@ -61,46 +67,59 @@ export function decompressU8WHeader(compressed: Uint8Array) {
|
||||
}
|
||||
return decompress(result.join(""));
|
||||
}
|
||||
|
||||
//compress into a string that is already URI encoded
|
||||
export function compressX64(input) {
|
||||
if (input == null)
|
||||
return "";
|
||||
export function compressX64(input: string) {
|
||||
if (input == null) return "";
|
||||
return _compress(input, 6, function (a) {
|
||||
return keyStrUriSafe.charAt(a);
|
||||
});
|
||||
}
|
||||
|
||||
//decompress from an output of compressToEncodedURIComponent
|
||||
export function decompressX64(input) {
|
||||
if (input == null)
|
||||
return "";
|
||||
if (input == "")
|
||||
return null;
|
||||
if (input == null) return "";
|
||||
if (input == "") return null;
|
||||
input = input.replace(/ /g, "+");
|
||||
return _decompress(input.length, 32, function (index) {
|
||||
return getBaseValue(keyStrUriSafe, input.charAt(index));
|
||||
});
|
||||
}
|
||||
|
||||
function compress(uncompressed) {
|
||||
return _compress(uncompressed, 16, function (a) {
|
||||
return fromCharCode(a);
|
||||
});
|
||||
}
|
||||
function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
if (uncompressed == null)
|
||||
return "";
|
||||
let i, value, context_dictionary = {}, context_dictionaryToCreate = {}, context_c = "", context_wc = "", context_w = "", context_enlargeIn = 2, // Compensate for the first entry which should not count
|
||||
context_dictSize = 3, context_numBits = 2, context_data = [], context_data_val = 0, context_data_position = 0, ii;
|
||||
|
||||
function _compress(uncompressed: string, bitsPerChar: number, getCharFromInt: (char: number) => string) {
|
||||
if (uncompressed == null) return "";
|
||||
let i,
|
||||
value,
|
||||
context_dictionary = {},
|
||||
context_dictionaryToCreate = {},
|
||||
context_c = "",
|
||||
context_wc = "",
|
||||
context_w = "",
|
||||
context_enlargeIn = 2, // Compensate for the first entry which should not count
|
||||
context_dictSize = 3,
|
||||
context_numBits = 2,
|
||||
context_data = [],
|
||||
context_data_val = 0,
|
||||
context_data_position = 0,
|
||||
ii;
|
||||
|
||||
for (ii = 0; ii < uncompressed.length; ii += 1) {
|
||||
context_c = uncompressed.charAt(ii);
|
||||
if (!hasOwnProperty.call(context_dictionary, context_c)) {
|
||||
context_dictionary[context_c] = context_dictSize++;
|
||||
context_dictionaryToCreate[context_c] = true;
|
||||
}
|
||||
|
||||
context_wc = context_w + context_c;
|
||||
if (hasOwnProperty.call(context_dictionary, context_wc)) {
|
||||
context_w = context_wc;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
|
||||
if (context_w.charCodeAt(0) < 256) {
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
@ -109,8 +128,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context_data_position++;
|
||||
}
|
||||
}
|
||||
@ -121,14 +139,12 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
value = 1;
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
context_data_val = (context_data_val << 1) | value;
|
||||
@ -136,8 +152,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = 0;
|
||||
@ -149,8 +164,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
@ -162,8 +176,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_numBits++;
|
||||
}
|
||||
delete context_dictionaryToCreate[context_w];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
value = context_dictionary[context_w];
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
context_data_val = (context_data_val << 1) | (value & 1);
|
||||
@ -171,8 +184,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
@ -198,8 +210,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context_data_position++;
|
||||
}
|
||||
}
|
||||
@ -210,14 +221,12 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
value = 1;
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
context_data_val = (context_data_val << 1) | value;
|
||||
@ -225,8 +234,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = 0;
|
||||
@ -238,8 +246,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
@ -251,8 +258,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_numBits++;
|
||||
}
|
||||
delete context_dictionaryToCreate[context_w];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
value = context_dictionary[context_w];
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
context_data_val = (context_data_val << 1) | (value & 1);
|
||||
@ -260,8 +266,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
@ -273,6 +278,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_numBits++;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the end of the stream
|
||||
value = 2;
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
@ -281,12 +287,12 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
}
|
||||
|
||||
// Flush the last char
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
@ -294,26 +300,39 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
break;
|
||||
}
|
||||
else
|
||||
context_data_position++;
|
||||
} else context_data_position++;
|
||||
}
|
||||
return context_data.join("");
|
||||
}
|
||||
function decompress(compressed) {
|
||||
if (compressed == null)
|
||||
return "";
|
||||
if (compressed == "")
|
||||
return null;
|
||||
|
||||
function decompress(compressed: string) {
|
||||
if (compressed == null) return "";
|
||||
if (compressed == "") return null;
|
||||
return _decompress(compressed.length, 32768, function (index) {
|
||||
return compressed.charCodeAt(index);
|
||||
});
|
||||
}
|
||||
function _decompress(length, resetValue, getNextValue) {
|
||||
let dictionary = [], next, enlargeIn = 4, dictSize = 4, numBits = 3, entry = "", result = [], i, w, bits, resb, maxpower, power, c, data = { val: getNextValue(0), position: resetValue, index: 1 };
|
||||
|
||||
function _decompress(length: number, resetValue, getNextValue) {
|
||||
let dictionary = [],
|
||||
next,
|
||||
enlargeIn = 4,
|
||||
dictSize = 4,
|
||||
numBits = 3,
|
||||
entry = "",
|
||||
result = [],
|
||||
i,
|
||||
w,
|
||||
bits,
|
||||
resb,
|
||||
maxpower,
|
||||
power,
|
||||
c,
|
||||
data = { val: getNextValue(0), position: resetValue, index: 1 };
|
||||
for (i = 0; i < 3; i += 1) {
|
||||
dictionary[i] = i;
|
||||
}
|
||||
|
||||
bits = 0;
|
||||
maxpower = Math.pow(2, 2);
|
||||
power = 1;
|
||||
@ -327,6 +346,7 @@ function _decompress(length, resetValue, getNextValue) {
|
||||
bits |= (resb > 0 ? 1 : 0) * power;
|
||||
power <<= 1;
|
||||
}
|
||||
|
||||
switch ((next = bits)) {
|
||||
case 0:
|
||||
bits = 0;
|
||||
@ -366,6 +386,7 @@ function _decompress(length, resetValue, getNextValue) {
|
||||
dictionary[3] = c;
|
||||
w = c;
|
||||
result.push(c);
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
if (data.index > length) {
|
||||
@ -424,23 +445,24 @@ function _decompress(length, resetValue, getNextValue) {
|
||||
case 2:
|
||||
return result.join("");
|
||||
}
|
||||
|
||||
if (enlargeIn == 0) {
|
||||
enlargeIn = Math.pow(2, numBits);
|
||||
numBits++;
|
||||
}
|
||||
|
||||
if (dictionary[c]) {
|
||||
// @ts-ignore
|
||||
entry = dictionary[c];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (c === dictSize) {
|
||||
entry = w + w.charAt(0);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
result.push(entry);
|
||||
|
||||
// Add w+entry[0] to the dictionary.
|
||||
dictionary[dictSize++] = w + entry.charAt(0);
|
||||
enlargeIn--;
|
||||
|
@ -1,9 +1,7 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end */
|
||||
import { Signal, STOP_PROPAGATION } from "./signal";
|
||||
import { arrayDeleteValue, waitNextFrame } from "./utils";
|
||||
import { ClickDetector } from "./click_detector";
|
||||
import { ClickDetector, ClickDetectorConstructorArgs } from "./click_detector";
|
||||
import { SOUNDS } from "../platform/sound";
|
||||
import { InputReceiver } from "./input_receiver";
|
||||
import { FormElement } from "./modal_dialog_forms";
|
||||
@ -11,6 +9,7 @@ import { globalConfig } from "./config";
|
||||
import { getStringForKeyCode } from "../game/key_action_mapper";
|
||||
import { createLogger } from "./logging";
|
||||
import { T } from "../translations";
|
||||
|
||||
/*
|
||||
* ***************************************************
|
||||
*
|
||||
@ -21,33 +20,74 @@ import { T } from "../translations";
|
||||
*
|
||||
* ***************************************************
|
||||
*/
|
||||
|
||||
const kbEnter = 13;
|
||||
const kbCancel = 27;
|
||||
|
||||
const logger = createLogger("dialogs");
|
||||
|
||||
// Button options
|
||||
type DialogButtonStyles = ["good", "bad", "misc", "info", "loading"];
|
||||
type DialogButtonOptions = ["timeout", "kb_enter", "kb_escape"];
|
||||
type DialogButtonOption = DialogButtonOptions[number];
|
||||
type DialogButtonOptionArr = `${DialogButtonOption}${
|
||||
| `/${DialogButtonOption}${`/${DialogButtonOption}` | ""}`
|
||||
| ""}`;
|
||||
|
||||
/**
|
||||
* Basic text based dialog
|
||||
*/
|
||||
export class Dialog {
|
||||
public app = app;
|
||||
public title = title;
|
||||
public contentHTML = contentHTML;
|
||||
public type = type;
|
||||
public buttonIds = buttons;
|
||||
public closeButton = closeButton;
|
||||
export class Dialog<Buttons extends string[] = []> {
|
||||
public app: Application;
|
||||
public title: string;
|
||||
public contentHTML: string;
|
||||
public type: string;
|
||||
public buttonIds: string[];
|
||||
public closeButton: boolean;
|
||||
|
||||
public closeRequested = new Signal();
|
||||
public buttonSignals = {};
|
||||
public buttonSignals: {
|
||||
[key in Buttons[number]]: Signal<any[]>;
|
||||
} = {} as any;
|
||||
|
||||
public valueChosen = new Signal();
|
||||
public timeouts = [];
|
||||
public clickDetectors = [];
|
||||
public inputReciever = new InputReceiver("dialog-" + this.title);
|
||||
public inputReciever: InputReceiver;
|
||||
public enterHandler = null;
|
||||
public escapeHandler = null;
|
||||
|
||||
public dialogElem: HTMLDivElement;
|
||||
public element: HTMLDivElement;
|
||||
|
||||
/**
|
||||
*
|
||||
* Constructs a new dialog with the given options
|
||||
*/
|
||||
constructor({
|
||||
app,
|
||||
title,
|
||||
contentHTML,
|
||||
buttons,
|
||||
type = "info",
|
||||
closeButton = false,
|
||||
}: {
|
||||
app: Application;
|
||||
title: string;
|
||||
contentHTML: string;
|
||||
buttons: `${Buttons[number]}:${DialogButtonStyles[number]}${"" | `:${DialogButtonOptionArr}`}`[];
|
||||
type: DialogButtonStyles[number];
|
||||
closeButton?: boolean;
|
||||
}) {
|
||||
this.app = app;
|
||||
this.title = title;
|
||||
this.buttonIds = buttons;
|
||||
this.contentHTML = contentHTML;
|
||||
this.type = type;
|
||||
this.closeButton = closeButton;
|
||||
|
||||
this.inputReciever = new InputReceiver("dialog-" + this.title);
|
||||
|
||||
constructor({ app, title, contentHTML, buttons, type = "info", closeButton = false }) {
|
||||
for (let i = 0; i < buttons.length; ++i) {
|
||||
if (G_IS_DEV && globalConfig.debug.disableTimedButtons) {
|
||||
this.buttonIds[i] = this.buttonIds[i].replace(":timeout", "");
|
||||
@ -57,76 +97,100 @@ export class Dialog {
|
||||
}
|
||||
this.inputReciever.keydown.add(this.handleKeydown, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal keydown handler
|
||||
*/
|
||||
handleKeydown({ keyCode, shift, alt, ctrl }: {
|
||||
handleKeydown({
|
||||
keyCode,
|
||||
shift,
|
||||
alt,
|
||||
ctrl,
|
||||
}: {
|
||||
keyCode: number;
|
||||
shift: boolean;
|
||||
alt: boolean;
|
||||
ctrl: boolean;
|
||||
}) {
|
||||
}): void | STOP_PROPAGATION {
|
||||
if (keyCode === kbEnter && this.enterHandler) {
|
||||
this.internalButtonHandler(this.enterHandler);
|
||||
return STOP_PROPAGATION;
|
||||
}
|
||||
|
||||
if (keyCode === kbCancel && this.escapeHandler) {
|
||||
this.internalButtonHandler(this.escapeHandler);
|
||||
return STOP_PROPAGATION;
|
||||
}
|
||||
}
|
||||
internalButtonHandler(id, ...payload) {
|
||||
|
||||
internalButtonHandler(id: string, ...payload: any[]) {
|
||||
this.app.inputMgr.popReciever(this.inputReciever);
|
||||
|
||||
if (id !== "close-button") {
|
||||
this.buttonSignals[id].dispatch(...payload);
|
||||
}
|
||||
this.closeRequested.dispatch();
|
||||
}
|
||||
|
||||
createElement() {
|
||||
const elem = document.createElement("div");
|
||||
elem.classList.add("ingameDialog");
|
||||
|
||||
this.dialogElem = document.createElement("div");
|
||||
this.dialogElem.classList.add("dialogInner");
|
||||
|
||||
if (this.type) {
|
||||
this.dialogElem.classList.add(this.type);
|
||||
}
|
||||
elem.appendChild(this.dialogElem);
|
||||
|
||||
const title = document.createElement("h1");
|
||||
title.innerText = this.title;
|
||||
title.classList.add("title");
|
||||
this.dialogElem.appendChild(title);
|
||||
|
||||
if (this.closeButton) {
|
||||
this.dialogElem.classList.add("hasCloseButton");
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.classList.add("closeButton");
|
||||
|
||||
this.trackClicks(closeBtn, () => this.internalButtonHandler("close-button"), {
|
||||
applyCssClass: "pressedSmallElement",
|
||||
});
|
||||
|
||||
title.appendChild(closeBtn);
|
||||
this.inputReciever.backButton.add(() => this.internalButtonHandler("close-button"));
|
||||
}
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.classList.add("content");
|
||||
content.innerHTML = this.contentHTML;
|
||||
this.dialogElem.appendChild(content);
|
||||
|
||||
if (this.buttonIds.length > 0) {
|
||||
const buttons = document.createElement("div");
|
||||
buttons.classList.add("buttons");
|
||||
// Create buttons
|
||||
for (let i = 0; i < this.buttonIds.length; ++i) {
|
||||
const [buttonId, buttonStyle, rawParams] = this.buttonIds[i].split(":");
|
||||
|
||||
const button = document.createElement("button");
|
||||
button.classList.add("button");
|
||||
button.classList.add("styledButton");
|
||||
button.classList.add(buttonStyle);
|
||||
button.innerText = T.dialogs.buttons[buttonId];
|
||||
|
||||
const params = (rawParams || "").split("/");
|
||||
const useTimeout = params.indexOf("timeout") >= 0;
|
||||
|
||||
const isEnter = params.indexOf("enter") >= 0;
|
||||
const isEscape = params.indexOf("escape") >= 0;
|
||||
|
||||
if (isEscape && this.closeButton) {
|
||||
logger.warn("Showing dialog with close button, and additional cancel button");
|
||||
}
|
||||
|
||||
if (useTimeout) {
|
||||
button.classList.add("timedButton");
|
||||
const timeout = setTimeout(() => {
|
||||
@ -143,6 +207,7 @@ export class Dialog {
|
||||
spacer.innerHTML = getStringForKeyCode(isEnter ? kbEnter : kbCancel);
|
||||
button.appendChild(spacer);
|
||||
// }
|
||||
|
||||
if (isEnter) {
|
||||
this.enterHandler = buttonId;
|
||||
}
|
||||
@ -150,21 +215,26 @@ export class Dialog {
|
||||
this.escapeHandler = buttonId;
|
||||
}
|
||||
}
|
||||
|
||||
this.trackClicks(button, () => this.internalButtonHandler(buttonId));
|
||||
buttons.appendChild(button);
|
||||
}
|
||||
|
||||
this.dialogElem.appendChild(buttons);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.dialogElem.classList.add("buttonless");
|
||||
}
|
||||
|
||||
this.element = elem;
|
||||
this.app.inputMgr.pushReciever(this.inputReciever);
|
||||
|
||||
return this.element;
|
||||
}
|
||||
|
||||
setIndex(index) {
|
||||
this.element.style.zIndex = index;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.element) {
|
||||
assert(false, "Tried to destroy dialog twice");
|
||||
@ -174,39 +244,46 @@ export class Dialog {
|
||||
// dispatched to the modal dialogs, it will not call the internalButtonHandler,
|
||||
// and thus our receiver stays attached the whole time
|
||||
this.app.inputMgr.destroyReceiver(this.inputReciever);
|
||||
|
||||
for (let i = 0; i < this.clickDetectors.length; ++i) {
|
||||
this.clickDetectors[i].cleanup();
|
||||
}
|
||||
|
||||
this.clickDetectors = [];
|
||||
|
||||
this.element.remove();
|
||||
this.element = null;
|
||||
|
||||
for (let i = 0; i < this.timeouts.length; ++i) {
|
||||
clearTimeout(this.timeouts[i]);
|
||||
}
|
||||
this.timeouts = [];
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.element.classList.remove("visible");
|
||||
}
|
||||
|
||||
show() {
|
||||
this.element.classList.add("visible");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to track clicks on an element
|
||||
* {}
|
||||
*/
|
||||
trackClicks(elem: Element, handler: function():void, args: import("./click_detector").ClickDetectorConstructorArgs= = {}): ClickDetector {
|
||||
trackClicks(elem: Element, handler: () => void, args: ClickDetectorConstructorArgs = {}): ClickDetector {
|
||||
const detector = new ClickDetector(elem, args);
|
||||
detector.click.add(handler, this);
|
||||
this.clickDetectors.push(detector);
|
||||
return detector;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog which simply shows a loading spinner
|
||||
*/
|
||||
export class DialogLoading extends Dialog {
|
||||
public text = text;
|
||||
public text: string;
|
||||
|
||||
constructor(app, text = "") {
|
||||
super({
|
||||
@ -216,46 +293,85 @@ export class DialogLoading extends Dialog {
|
||||
buttons: [],
|
||||
type: "loading",
|
||||
});
|
||||
|
||||
// Loading dialog can not get closed with back button
|
||||
this.inputReciever.backButton.removeAll();
|
||||
this.inputReciever.context = "dialog-loading";
|
||||
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
createElement() {
|
||||
const elem = document.createElement("div");
|
||||
elem.classList.add("ingameDialog");
|
||||
elem.classList.add("loadingDialog");
|
||||
this.element = elem;
|
||||
|
||||
if (this.text) {
|
||||
const text = document.createElement("div");
|
||||
text.classList.add("text");
|
||||
text.innerText = this.text;
|
||||
elem.appendChild(text);
|
||||
}
|
||||
|
||||
const loader = document.createElement("div");
|
||||
loader.classList.add("prefab_LoadingTextWithAnim");
|
||||
loader.classList.add("loadingIndicator");
|
||||
elem.appendChild(loader);
|
||||
|
||||
this.app.inputMgr.pushReciever(this.inputReciever);
|
||||
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
export class DialogOptionChooser extends Dialog {
|
||||
public options = options;
|
||||
public initialOption = options.active;
|
||||
|
||||
constructor({ app, title, options }) {
|
||||
interface DialogOptionOptions {
|
||||
value: string;
|
||||
text: string;
|
||||
desc?: string;
|
||||
iconPrefix?: string;
|
||||
}
|
||||
|
||||
export class DialogOptionChooser extends Dialog {
|
||||
public options: {
|
||||
options: DialogOptionOptions[];
|
||||
active: string;
|
||||
};
|
||||
|
||||
public declare buttonSignals: {
|
||||
optionSelected: Signal<[]>;
|
||||
};
|
||||
|
||||
public initialOption: string;
|
||||
|
||||
constructor({
|
||||
app,
|
||||
title,
|
||||
options,
|
||||
}: {
|
||||
app: Application;
|
||||
title: string;
|
||||
options: {
|
||||
options: DialogOptionOptions[];
|
||||
active: string;
|
||||
};
|
||||
}) {
|
||||
let html = "<div class='optionParent'>";
|
||||
|
||||
options.options.forEach(({ value, text, desc = null, iconPrefix = null }) => {
|
||||
const descHtml = desc ? `<span class="desc">${desc}</span>` : "";
|
||||
let iconHtml = iconPrefix ? `<span class="icon icon-${iconPrefix}-${value}"></span>` : "";
|
||||
html += `
|
||||
<div class='option ${value === options.active ? "active" : ""} ${iconPrefix ? "hasIcon" : ""}' data-optionvalue='${value}'>
|
||||
<div class='option ${value === options.active ? "active" : ""} ${
|
||||
iconPrefix ? "hasIcon" : ""
|
||||
}' data-optionvalue='${value}'>
|
||||
${iconHtml}
|
||||
<span class='title'>${text}</span>
|
||||
${descHtml}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += "</div>";
|
||||
super({
|
||||
app,
|
||||
@ -265,11 +381,16 @@ export class DialogOptionChooser extends Dialog {
|
||||
type: "info",
|
||||
closeButton: true,
|
||||
});
|
||||
|
||||
this.options = options;
|
||||
this.initialOption = options.active;
|
||||
|
||||
this.buttonSignals.optionSelected = new Signal();
|
||||
}
|
||||
createElement() {
|
||||
const div = super.createElement();
|
||||
this.dialogElem.classList.add("optionChooserDialog");
|
||||
|
||||
div.querySelectorAll("[data-optionvalue]").forEach(handle => {
|
||||
const value = handle.getAttribute("data-optionvalue");
|
||||
if (!handle) {
|
||||
@ -285,13 +406,13 @@ export class DialogOptionChooser extends Dialog {
|
||||
targetOnly: true,
|
||||
});
|
||||
this.clickDetectors.push(detector);
|
||||
|
||||
if (value !== this.initialOption) {
|
||||
detector.click.add(() => {
|
||||
const selected = div.querySelector(".option.active");
|
||||
if (selected) {
|
||||
selected.classList.remove("active");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
logger.warn("No selected option");
|
||||
}
|
||||
handle.classList.add("active");
|
||||
@ -303,27 +424,51 @@ export class DialogOptionChooser extends Dialog {
|
||||
return div;
|
||||
}
|
||||
}
|
||||
export class DialogWithForm extends Dialog {
|
||||
public confirmButtonId = confirmButtonId;
|
||||
public formElements = formElements;
|
||||
public enterHandler = confirmButtonId;
|
||||
|
||||
constructor({ app, title, desc, formElements, buttons = ["cancel", "ok:good"], confirmButtonId = "ok", closeButton = true, }) {
|
||||
export class DialogWithForm extends Dialog {
|
||||
public confirmButtonId: string;
|
||||
public formElements: FormElement[];
|
||||
public enterHandler: string;
|
||||
|
||||
constructor({
|
||||
app,
|
||||
title,
|
||||
desc,
|
||||
formElements,
|
||||
buttons = ["cancel", "ok:good"],
|
||||
confirmButtonId = "ok",
|
||||
closeButton = true,
|
||||
}: {
|
||||
app: Application;
|
||||
title: string;
|
||||
desc: string;
|
||||
buttons?: string[];
|
||||
confirmButtonId?: string;
|
||||
extraButton?: string;
|
||||
closeButton?: boolean;
|
||||
formElements: FormElement[];
|
||||
}) {
|
||||
let html = "";
|
||||
html += desc + "<br>";
|
||||
for (let i = 0; i < formElements.length; ++i) {
|
||||
html += formElements[i].getHtml();
|
||||
}
|
||||
|
||||
super({
|
||||
app,
|
||||
title: title,
|
||||
contentHTML: html,
|
||||
buttons: buttons,
|
||||
buttons: buttons as any,
|
||||
type: "info",
|
||||
closeButton,
|
||||
});
|
||||
this.confirmButtonId = confirmButtonId;
|
||||
this.formElements = formElements;
|
||||
|
||||
this.enterHandler = confirmButtonId;
|
||||
}
|
||||
internalButtonHandler(id, ...payload) {
|
||||
|
||||
internalButtonHandler(id: string, ...payload) {
|
||||
if (id === this.confirmButtonId) {
|
||||
if (this.hasAnyInvalid()) {
|
||||
this.dialogElem.classList.remove("errorShake");
|
||||
@ -336,8 +481,10 @@ export class DialogWithForm extends Dialog {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
super.internalButtonHandler(id, payload);
|
||||
}
|
||||
|
||||
hasAnyInvalid() {
|
||||
for (let i = 0; i < this.formElements.length; ++i) {
|
||||
if (!this.formElements[i].isValid()) {
|
||||
@ -346,6 +493,7 @@ export class DialogWithForm extends Dialog {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
createElement() {
|
||||
const div = super.createElement();
|
||||
for (let i = 0; i < this.formElements.length; ++i) {
|
||||
|
@ -11,42 +11,58 @@ import { Signal } from "./signal";
|
||||
*
|
||||
* ***************************************************
|
||||
*/
|
||||
export class FormElement {
|
||||
public id = id;
|
||||
public label = label;
|
||||
export abstract class FormElement {
|
||||
public valueChosen = new Signal();
|
||||
|
||||
constructor(id, label) {
|
||||
constructor(public id: string, public label: string) {}
|
||||
|
||||
abstract getHtml();
|
||||
|
||||
getFormElement(parent: Element) {
|
||||
return parent.querySelector("[data-formId='" + this.id + "']") as HTMLFormElement;
|
||||
}
|
||||
getHtml() {
|
||||
abstract;
|
||||
return "";
|
||||
}
|
||||
getFormElement(parent) {
|
||||
return parent.querySelector("[data-formId='" + this.id + "']");
|
||||
}
|
||||
bindEvents(parent, clickTrackers) {
|
||||
abstract;
|
||||
}
|
||||
focus() { }
|
||||
|
||||
abstract bindEvents(parent: Element, clickTrackers: ClickDetector[]);
|
||||
|
||||
focus() {}
|
||||
|
||||
isValid() {
|
||||
return true;
|
||||
}
|
||||
/** {} */
|
||||
getValue(): any {
|
||||
abstract;
|
||||
}
|
||||
}
|
||||
export class FormElementInput extends FormElement {
|
||||
public placeholder = placeholder;
|
||||
public defaultValue = defaultValue;
|
||||
public inputType = inputType;
|
||||
public validator = validator;
|
||||
public element = null;
|
||||
|
||||
constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) {
|
||||
abstract getValue(): any;
|
||||
}
|
||||
|
||||
export class FormElementInput extends FormElement {
|
||||
public placeholder: string;
|
||||
public defaultValue: string;
|
||||
public inputType: string;
|
||||
public validator: (str: string) => boolean;
|
||||
public element: HTMLFormElement = null;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
label = null,
|
||||
placeholder,
|
||||
defaultValue = "",
|
||||
inputType = "text",
|
||||
validator = null,
|
||||
}: {
|
||||
id: string;
|
||||
label?: string;
|
||||
placeholder: string;
|
||||
defaultValue?: string;
|
||||
inputType?: string;
|
||||
validator: (str: string) => boolean;
|
||||
}) {
|
||||
super(id, label);
|
||||
|
||||
this.placeholder = placeholder;
|
||||
this.defaultValue = defaultValue;
|
||||
this.inputType = inputType;
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
let classes = [];
|
||||
let inputType = "text";
|
||||
@ -85,37 +101,46 @@ export class FormElementInput extends FormElement {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
bindEvents(parent, clickTrackers) {
|
||||
this.element = this.getFormElement(parent);
|
||||
this.element.addEventListener("input", event => this.updateErrorState());
|
||||
this.updateErrorState();
|
||||
}
|
||||
|
||||
updateErrorState() {
|
||||
this.element.classList.toggle("errored", !this.isValid());
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return !this.validator || this.validator(this.element.value);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.element.value;
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
this.element.value = value;
|
||||
this.updateErrorState();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.element.focus();
|
||||
this.element.select();
|
||||
}
|
||||
}
|
||||
export class FormElementCheckbox extends FormElement {
|
||||
public defaultValue = defaultValue;
|
||||
public value = this.defaultValue;
|
||||
public element = null;
|
||||
public defaultValue: boolean;
|
||||
public value: boolean;
|
||||
public element: Element = null;
|
||||
|
||||
constructor({ id, label, defaultValue = true }) {
|
||||
constructor({ id, label, defaultValue = true }: { id: string; label: string; defaultValue?: boolean }) {
|
||||
super(id, label);
|
||||
this.defaultValue = defaultValue;
|
||||
this.value = this.defaultValue;
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
return `
|
||||
<div class="formElement checkBoxFormElem">
|
||||
@ -126,7 +151,8 @@ export class FormElementCheckbox extends FormElement {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
bindEvents(parent, clickTrackers) {
|
||||
|
||||
bindEvents(parent: Element, clickTrackers: ClickDetector[]) {
|
||||
this.element = this.getFormElement(parent);
|
||||
const detector = new ClickDetector(this.element, {
|
||||
consumeEvents: false,
|
||||
@ -135,23 +161,30 @@ export class FormElementCheckbox extends FormElement {
|
||||
clickTrackers.push(detector);
|
||||
detector.click.add(this.toggle, this);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.value = !this.value;
|
||||
this.element.classList.toggle("checked", this.value);
|
||||
}
|
||||
focus(parent) { }
|
||||
}
|
||||
export class FormElementItemChooser extends FormElement {
|
||||
public items = items;
|
||||
public element = null;
|
||||
public chosenItem: BaseItem = null;
|
||||
|
||||
constructor({ id, label, items = [] }) {
|
||||
// focus(parent) { }
|
||||
}
|
||||
|
||||
export class FormElementItemChooser extends FormElement {
|
||||
public element: Element = null;
|
||||
public chosenItem: BaseItem = null;
|
||||
public items: any[];
|
||||
|
||||
constructor({ id, label, items = [] }) {
|
||||
super(id, label);
|
||||
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
let classes = [];
|
||||
return `
|
||||
@ -161,10 +194,13 @@ export class FormElementItemChooser extends FormElement {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
bindEvents(parent: HTMLElement, clickTrackers: Array<ClickDetector>) {
|
||||
|
||||
bindEvents(parent: HTMLElement, clickTrackers: Array<ClickDetector>) {
|
||||
this.element = this.getFormElement(parent);
|
||||
|
||||
for (let i = 0; i < this.items.length; ++i) {
|
||||
const item = this.items[i];
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 128;
|
||||
canvas.height = 128;
|
||||
@ -172,6 +208,7 @@ export class FormElementItemChooser extends FormElement {
|
||||
item.drawFullSizeOnCanvas(context, 128);
|
||||
this.element.appendChild(canvas);
|
||||
const detector = new ClickDetector(canvas, {});
|
||||
|
||||
clickTrackers.push(detector);
|
||||
detector.click.add(() => {
|
||||
this.chosenItem = item;
|
||||
@ -179,11 +216,14 @@ export class FormElementItemChooser extends FormElement {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return null;
|
||||
}
|
||||
focus() { }
|
||||
|
||||
focus() {}
|
||||
}
|
||||
|
@ -17,8 +17,7 @@ function stringPolyfills() {
|
||||
padString = String(typeof padString !== "undefined" ? padString : " ");
|
||||
if (this.length >= targetLength) {
|
||||
return String(this);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
targetLength = targetLength - this.length;
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
|
||||
@ -35,8 +34,7 @@ function stringPolyfills() {
|
||||
padString = String(typeof padString !== "undefined" ? padString : " ");
|
||||
if (this.length > targetLength) {
|
||||
return String(this);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
targetLength = targetLength - this.length;
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
|
||||
@ -59,13 +57,21 @@ function objectPolyfills() {
|
||||
if (!Object.values) {
|
||||
// @ts-ignore
|
||||
Object.values = function values(O) {
|
||||
return reduce(keys(O), (v, k) => concat(v, typeof k === "string" && isEnumerable(O, k) ? [O[k]] : []), []);
|
||||
return reduce(
|
||||
keys(O),
|
||||
(v, k) => concat(v, typeof k === "string" && isEnumerable(O, k) ? [O[k]] : []),
|
||||
[]
|
||||
);
|
||||
};
|
||||
}
|
||||
if (!Object.entries) {
|
||||
// @ts-ignore
|
||||
Object.entries = function entries(O) {
|
||||
return reduce(keys(O), (e, k) => concat(e, typeof k === "string" && isEnumerable(O, k) ? [[k, O[k]]] : []), []);
|
||||
return reduce(
|
||||
keys(O),
|
||||
(e, k) => concat(e, typeof k === "string" && isEnumerable(O, k) ? [[k, O[k]]] : []),
|
||||
[]
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
const queryString = require("query-string");
|
||||
const options = queryString.parse(location.search);
|
||||
|
||||
export let queryParamOptions = {
|
||||
embedProvider: null,
|
||||
abtVariant: null,
|
||||
|
@ -1,6 +1,5 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
import { sha1, CRC_PREFIX, computeCrc } from "./sensitive_utils.encrypt";
|
||||
import { createLogger } from "./logging";
|
||||
import { FILE_NOT_FOUND } from "../platform/storage";
|
||||
@ -14,55 +13,53 @@ const debounce = require("debounce-promise");
|
||||
const logger = createLogger("read_write_proxy");
|
||||
const salt = accessNestedPropertyReverse(globalConfig, ["file", "info"]);
|
||||
// Helper which only writes / reads if verify() works. Also performs migration
|
||||
export class ReadWriteProxy {
|
||||
public app: Application = app;
|
||||
public filename = filename;
|
||||
|
||||
export abstract class ReadWriteProxy {
|
||||
public currentData: object = null;
|
||||
public debouncedWrite = debounce(this.doWriteAsync.bind(this), 50);
|
||||
|
||||
constructor(app, filename) {
|
||||
constructor(public app: Application, public filename: string) {
|
||||
// TODO: EXTREMELY HACKY! To verify we need to do this a step later
|
||||
if (G_IS_DEV && IS_DEBUG) {
|
||||
setTimeout(() => {
|
||||
assert(this.verify(this.getDefaultData()).result, "Verify() failed for default data: " + this.verify(this.getDefaultData()).reason);
|
||||
assert(
|
||||
this.verify(this.getDefaultData()).result,
|
||||
"Verify() failed for default data: " + this.verify(this.getDefaultData()).reason
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// -- Methods to override
|
||||
/** {} */
|
||||
verify(data): ExplainedResult {
|
||||
abstract;
|
||||
return ExplainedResult.bad();
|
||||
}
|
||||
abstract verify(data): ExplainedResult;
|
||||
|
||||
// Should return the default data
|
||||
getDefaultData() {
|
||||
abstract;
|
||||
return {};
|
||||
}
|
||||
abstract getDefaultData(): object;
|
||||
|
||||
// Should return the current version as an integer
|
||||
getCurrentVersion() {
|
||||
abstract;
|
||||
return 0;
|
||||
}
|
||||
abstract getCurrentVersion(): number;
|
||||
|
||||
// Should migrate the data (Modify in place)
|
||||
/** {} */
|
||||
migrate(data): ExplainedResult {
|
||||
abstract;
|
||||
return ExplainedResult.bad();
|
||||
}
|
||||
abstract migrate(data: object): ExplainedResult;
|
||||
|
||||
// -- / Methods
|
||||
|
||||
// Resets whole data, returns promise
|
||||
resetEverythingAsync() {
|
||||
logger.warn("Reset data to default");
|
||||
this.currentData = this.getDefaultData();
|
||||
return this.writeAsync();
|
||||
}
|
||||
static serializeObject(obj: object) {
|
||||
|
||||
static serializeObject(obj: object) {
|
||||
const jsonString = JSON.stringify(compressObject(obj));
|
||||
const checksum = computeCrc(jsonString + salt);
|
||||
return compressionPrefix + compressX64(checksum + jsonString);
|
||||
}
|
||||
static deserializeObject(text: object) {
|
||||
|
||||
// @Bagel: This was an object, but then immediately called substr??
|
||||
// Also look at removing the substr
|
||||
static deserializeObject(text: string) {
|
||||
const decompressed = decompressX64(text.substr(compressionPrefix.length));
|
||||
if (!decompressed) {
|
||||
// LZ string decompression failure
|
||||
@ -72,154 +69,185 @@ export class ReadWriteProxy {
|
||||
// String too short
|
||||
throw new Error("bad-content / payload-too-small");
|
||||
}
|
||||
|
||||
// Compare stored checksum with actual checksum
|
||||
const checksum = decompressed.substring(0, 40);
|
||||
const jsonString = decompressed.substr(40);
|
||||
|
||||
const desiredChecksum = checksum.startsWith(CRC_PREFIX)
|
||||
? computeCrc(jsonString + salt)
|
||||
: sha1(jsonString + salt);
|
||||
|
||||
if (desiredChecksum !== checksum) {
|
||||
// Checksum mismatch
|
||||
throw new Error("bad-content / checksum-mismatch");
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonString);
|
||||
const decoded = decompressObject(parsed);
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the data asychronously, fails if verify() fails.
|
||||
* Debounces the operation by up to 50ms
|
||||
* {}
|
||||
*/
|
||||
writeAsync(): Promise<void> {
|
||||
const verifyResult = this.internalVerifyEntry(this.currentData);
|
||||
|
||||
if (!verifyResult.result) {
|
||||
logger.error("Tried to write invalid data to", this.filename, "reason:", verifyResult.reason);
|
||||
return Promise.reject(verifyResult.reason);
|
||||
}
|
||||
|
||||
return this.debouncedWrite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually writes the data asychronously
|
||||
* {}
|
||||
*/
|
||||
doWriteAsync(): Promise<void> {
|
||||
return asyncCompressor
|
||||
.compressObjectAsync(this.currentData)
|
||||
.then(compressed => {
|
||||
return this.app.storage.writeFileAsync(this.filename, compressed);
|
||||
})
|
||||
return this.app.storage.writeFileAsync(this.filename, compressed);
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("📄 Wrote", this.filename);
|
||||
})
|
||||
logger.log("📄 Wrote", this.filename);
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error("Failed to write", this.filename, ":", err);
|
||||
throw err;
|
||||
});
|
||||
logger.error("Failed to write", this.filename, ":", err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Reads the data asynchronously, fails if verify() fails
|
||||
readAsync() {
|
||||
// Start read request
|
||||
return (this.app.storage
|
||||
.readFileAsync(this.filename)
|
||||
// Check for errors during read
|
||||
.catch(err => {
|
||||
if (err === FILE_NOT_FOUND) {
|
||||
logger.log("File not found, using default data");
|
||||
// File not found or unreadable, assume default file
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return Promise.reject("file-error: " + err);
|
||||
})
|
||||
// Decrypt data (if its encrypted)
|
||||
// @ts-ignore
|
||||
.then(rawData => {
|
||||
if (rawData == null) {
|
||||
// So, the file has not been found, use default data
|
||||
return JSON.stringify(compressObject(this.getDefaultData()));
|
||||
}
|
||||
if (rawData.startsWith(compressionPrefix)) {
|
||||
const decompressed = decompressX64(rawData.substr(compressionPrefix.length));
|
||||
if (!decompressed) {
|
||||
// LZ string decompression failure
|
||||
return Promise.reject("bad-content / decompression-failed");
|
||||
}
|
||||
if (decompressed.length < 40) {
|
||||
// String too short
|
||||
return Promise.reject("bad-content / payload-too-small");
|
||||
}
|
||||
// Compare stored checksum with actual checksum
|
||||
const checksum = decompressed.substring(0, 40);
|
||||
const jsonString = decompressed.substr(40);
|
||||
const desiredChecksum = checksum.startsWith(CRC_PREFIX)
|
||||
? computeCrc(jsonString + salt)
|
||||
: sha1(jsonString + salt);
|
||||
if (desiredChecksum !== checksum) {
|
||||
// Checksum mismatch
|
||||
return Promise.reject("bad-content / checksum-mismatch: " + desiredChecksum + " vs " + checksum);
|
||||
}
|
||||
return jsonString;
|
||||
}
|
||||
else {
|
||||
if (!G_IS_DEV) {
|
||||
return Promise.reject("bad-content / missing-compression");
|
||||
}
|
||||
}
|
||||
return rawData;
|
||||
})
|
||||
// Parse JSON, this could throw but that's fine
|
||||
.then(res => {
|
||||
try {
|
||||
return JSON.parse(res);
|
||||
}
|
||||
catch (ex) {
|
||||
logger.error("Failed to parse file content of", this.filename, ":", ex, "(content was:", res, ")");
|
||||
throw new Error("invalid-serialized-data");
|
||||
}
|
||||
})
|
||||
// Decompress
|
||||
.then(compressed => decompressObject(compressed))
|
||||
// Verify basic structure
|
||||
.then(contents => {
|
||||
const result = this.internalVerifyBasicStructure(contents);
|
||||
if (!result.isGood()) {
|
||||
return Promise.reject("verify-failed: " + result.reason);
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
// Check version and migrate if required
|
||||
.then(contents => {
|
||||
if (contents.version > this.getCurrentVersion()) {
|
||||
return Promise.reject("stored-data-is-newer");
|
||||
}
|
||||
if (contents.version < this.getCurrentVersion()) {
|
||||
logger.log("Trying to migrate data object from version", contents.version, "to", this.getCurrentVersion());
|
||||
const migrationResult = this.migrate(contents); // modify in place
|
||||
if (migrationResult.isBad()) {
|
||||
return Promise.reject("migration-failed: " + migrationResult.reason);
|
||||
}
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
// Verify
|
||||
.then(contents => {
|
||||
const verifyResult = this.internalVerifyEntry(contents);
|
||||
if (!verifyResult.result) {
|
||||
logger.error("Read invalid data from", this.filename, "reason:", verifyResult.reason, "contents:", contents);
|
||||
return Promise.reject("invalid-data: " + verifyResult.reason);
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
// Store
|
||||
.then(contents => {
|
||||
this.currentData = contents;
|
||||
logger.log("📄 Read data with version", this.currentData.version, "from", this.filename);
|
||||
return contents;
|
||||
})
|
||||
// Catchall
|
||||
.catch(err => {
|
||||
return Promise.reject("Failed to read " + this.filename + ": " + err);
|
||||
}));
|
||||
return (
|
||||
this.app.storage
|
||||
.readFileAsync(this.filename)
|
||||
|
||||
// Check for errors during read
|
||||
.catch(err => {
|
||||
if (err === FILE_NOT_FOUND) {
|
||||
logger.log("File not found, using default data");
|
||||
// File not found or unreadable, assume default file
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return Promise.reject("file-error: " + err);
|
||||
})
|
||||
|
||||
// Decrypt data (if its encrypted)
|
||||
// @ts-ignore
|
||||
.then(rawData => {
|
||||
if (rawData == null) {
|
||||
// So, the file has not been found, use default data
|
||||
return JSON.stringify(compressObject(this.getDefaultData()));
|
||||
}
|
||||
if (rawData.startsWith(compressionPrefix)) {
|
||||
const decompressed = decompressX64(rawData.substr(compressionPrefix.length));
|
||||
if (!decompressed) {
|
||||
// LZ string decompression failure
|
||||
return Promise.reject("bad-content / decompression-failed");
|
||||
}
|
||||
if (decompressed.length < 40) {
|
||||
// String too short
|
||||
return Promise.reject("bad-content / payload-too-small");
|
||||
}
|
||||
// Compare stored checksum with actual checksum
|
||||
const checksum = decompressed.substring(0, 40);
|
||||
const jsonString = decompressed.substr(40);
|
||||
const desiredChecksum = checksum.startsWith(CRC_PREFIX)
|
||||
? computeCrc(jsonString + salt)
|
||||
: sha1(jsonString + salt);
|
||||
if (desiredChecksum !== checksum) {
|
||||
// Checksum mismatch
|
||||
return Promise.reject(
|
||||
"bad-content / checksum-mismatch: " + desiredChecksum + " vs " + checksum
|
||||
);
|
||||
}
|
||||
return jsonString;
|
||||
} else {
|
||||
if (!G_IS_DEV) {
|
||||
return Promise.reject("bad-content / missing-compression");
|
||||
}
|
||||
}
|
||||
return rawData;
|
||||
})
|
||||
// Parse JSON, this could throw but that's fine
|
||||
.then(res => {
|
||||
try {
|
||||
return JSON.parse(res);
|
||||
} catch (ex) {
|
||||
logger.error(
|
||||
"Failed to parse file content of",
|
||||
this.filename,
|
||||
":",
|
||||
ex,
|
||||
"(content was:",
|
||||
res,
|
||||
")"
|
||||
);
|
||||
throw new Error("invalid-serialized-data");
|
||||
}
|
||||
})
|
||||
// Decompress
|
||||
.then(compressed => decompressObject(compressed))
|
||||
// Verify basic structure
|
||||
.then(contents => {
|
||||
const result = this.internalVerifyBasicStructure(contents);
|
||||
if (!result.isGood()) {
|
||||
return Promise.reject("verify-failed: " + result.reason);
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
// Check version and migrate if required
|
||||
.then(contents => {
|
||||
if (contents.version > this.getCurrentVersion()) {
|
||||
return Promise.reject("stored-data-is-newer");
|
||||
}
|
||||
if (contents.version < this.getCurrentVersion()) {
|
||||
logger.log(
|
||||
"Trying to migrate data object from version",
|
||||
contents.version,
|
||||
"to",
|
||||
this.getCurrentVersion()
|
||||
);
|
||||
const migrationResult = this.migrate(contents); // modify in place
|
||||
if (migrationResult.isBad()) {
|
||||
return Promise.reject("migration-failed: " + migrationResult.reason);
|
||||
}
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
// Verify
|
||||
.then(contents => {
|
||||
const verifyResult = this.internalVerifyEntry(contents);
|
||||
if (!verifyResult.result) {
|
||||
logger.error(
|
||||
"Read invalid data from",
|
||||
this.filename,
|
||||
"reason:",
|
||||
verifyResult.reason,
|
||||
"contents:",
|
||||
contents
|
||||
);
|
||||
return Promise.reject("invalid-data: " + verifyResult.reason);
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
// Store
|
||||
.then(contents => {
|
||||
this.currentData = contents;
|
||||
logger.log("📄 Read data with version", this.currentData.version, "from", this.filename);
|
||||
return contents;
|
||||
})
|
||||
// Catchall
|
||||
.catch(err => {
|
||||
return Promise.reject("Failed to read " + this.filename + ": " + err);
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Deletes the file
|
||||
@ -235,14 +263,18 @@ export class ReadWriteProxy {
|
||||
return ExplainedResult.bad("Data is empty");
|
||||
}
|
||||
if (!Number.isInteger(data.version) || data.version < 0) {
|
||||
return ExplainedResult.bad(`Data has invalid version: ${data.version} (expected ${this.getCurrentVersion()})`);
|
||||
return ExplainedResult.bad(
|
||||
`Data has invalid version: ${data.version} (expected ${this.getCurrentVersion()})`
|
||||
);
|
||||
}
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
/** {} */
|
||||
internalVerifyEntry(data): ExplainedResult {
|
||||
if (data.version !== this.getCurrentVersion()) {
|
||||
return ExplainedResult.bad("Version mismatch, got " + data.version + " and expected " + this.getCurrentVersion());
|
||||
return ExplainedResult.bad(
|
||||
"Version mismatch, got " + data.version + " and expected " + this.getCurrentVersion()
|
||||
);
|
||||
}
|
||||
const verifyStructureError = this.internalVerifyBasicStructure(data);
|
||||
if (!verifyStructureError.isGood()) {
|
||||
|
@ -2,119 +2,109 @@ import { globalConfig } from "./config";
|
||||
import { epsilonCompare, round2Digits } from "./utils";
|
||||
import { Vector } from "./vector";
|
||||
export class Rectangle {
|
||||
public x = x;
|
||||
public y = y;
|
||||
public w = w;
|
||||
public h = h;
|
||||
constructor(public x: number = 0, public y: number = 0, public w: number = 0, public h: number = 0) {}
|
||||
|
||||
constructor(x = 0, y = 0, w = 0, h = 0) {
|
||||
}
|
||||
/**
|
||||
* Creates a rectangle from top right bottom and left offsets
|
||||
*/
|
||||
static fromTRBL(top: number, right: number, bottom: number, left: number) {
|
||||
return new Rectangle(left, top, right - left, bottom - top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new square rectangle
|
||||
*/
|
||||
static fromSquare(x: number, y: number, size: number) {
|
||||
return new Rectangle(x, y, size, size);
|
||||
}
|
||||
static fromTwoPoints(p1: Vector, p2: Vector) {
|
||||
static fromTwoPoints(p1: Vector, p2: Vector) {
|
||||
const left = Math.min(p1.x, p2.x);
|
||||
const top = Math.min(p1.y, p2.y);
|
||||
const right = Math.max(p1.x, p2.x);
|
||||
const bottom = Math.max(p1.y, p2.y);
|
||||
return new Rectangle(left, top, right - left, bottom - top);
|
||||
}
|
||||
static centered(width: number, height: number) {
|
||||
static centered(width: number, height: number) {
|
||||
return new Rectangle(-Math.ceil(width / 2), -Math.ceil(height / 2), width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if a intersects b
|
||||
*/
|
||||
static intersects(a: Rectangle, b: Rectangle) {
|
||||
return a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies this instance
|
||||
* {}
|
||||
*/
|
||||
clone(): Rectangle {
|
||||
return new Rectangle(this.x, this.y, this.w, this.h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this rectangle is empty
|
||||
* {}
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return epsilonCompare(this.w * this.h, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this rectangle is equal to the other while taking an epsilon into account
|
||||
*/
|
||||
equalsEpsilon(other: Rectangle, epsilon: number) {
|
||||
return (epsilonCompare(this.x, other.x, epsilon) &&
|
||||
return (
|
||||
epsilonCompare(this.x, other.x, epsilon) &&
|
||||
epsilonCompare(this.y, other.y, epsilon) &&
|
||||
epsilonCompare(this.w, other.w, epsilon) &&
|
||||
epsilonCompare(this.h, other.h, epsilon));
|
||||
epsilonCompare(this.h, other.h, epsilon)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
|
||||
left(): number {
|
||||
return this.x;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
|
||||
right(): number {
|
||||
return this.x + this.w;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
|
||||
top(): number {
|
||||
return this.y;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
|
||||
bottom(): number {
|
||||
return this.y + this.h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Top, Right, Bottom, Left
|
||||
* {}
|
||||
*/
|
||||
trbl(): [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number
|
||||
] {
|
||||
trbl(): [number, number, number, number] {
|
||||
return [this.y, this.right(), this.bottom(), this.x];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the center of the rect
|
||||
* {}
|
||||
*/
|
||||
getCenter(): Vector {
|
||||
return new Vector(this.x + this.w / 2, this.y + this.h / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the right side of the rect without moving it
|
||||
*/
|
||||
setRight(right: number) {
|
||||
this.w = right - this.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bottom side of the rect without moving it
|
||||
*/
|
||||
setBottom(bottom: number) {
|
||||
this.h = bottom - this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the top side of the rect without scaling it
|
||||
*/
|
||||
@ -123,6 +113,7 @@ export class Rectangle {
|
||||
this.y = top;
|
||||
this.setBottom(bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the left side of the rect without scaling it
|
||||
*/
|
||||
@ -131,6 +122,7 @@ export class Rectangle {
|
||||
this.x = left;
|
||||
this.setRight(right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top left point
|
||||
* {}
|
||||
@ -138,6 +130,7 @@ export class Rectangle {
|
||||
topLeft(): Vector {
|
||||
return new Vector(this.x, this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bottom left point
|
||||
* {}
|
||||
@ -145,6 +138,7 @@ export class Rectangle {
|
||||
bottomRight(): Vector {
|
||||
return new Vector(this.right(), this.bottom());
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the rectangle by the given parameters
|
||||
*/
|
||||
@ -152,6 +146,7 @@ export class Rectangle {
|
||||
this.x += x;
|
||||
this.y += y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the rectangle by the given vector
|
||||
*/
|
||||
@ -159,6 +154,7 @@ export class Rectangle {
|
||||
this.x += vec.x;
|
||||
this.y += vec.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales every parameter (w, h, x, y) by the given factor. Useful to transform from world to
|
||||
* tile space and vice versa
|
||||
@ -166,50 +162,55 @@ export class Rectangle {
|
||||
allScaled(factor: number) {
|
||||
return new Rectangle(this.x * factor, this.y * factor, this.w * factor, this.h * factor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the rectangle in all directions
|
||||
* {} new rectangle
|
||||
* @returns new rectangle
|
||||
*/
|
||||
expandedInAllDirections(amount: number): Rectangle {
|
||||
return new Rectangle(this.x - amount, this.y - amount, this.w + 2 * amount, this.h + 2 * amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the given rectangle is contained
|
||||
* {}
|
||||
*/
|
||||
containsRect(rect: Rectangle): boolean {
|
||||
return (this.x <= rect.right() &&
|
||||
return (
|
||||
this.x <= rect.right() &&
|
||||
rect.x <= this.right() &&
|
||||
this.y <= rect.bottom() &&
|
||||
rect.y <= this.bottom());
|
||||
rect.y <= this.bottom()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this rectangle contains the other rectangle specified by the parameters
|
||||
* {}
|
||||
*/
|
||||
containsRect4Params(x: number, y: number, w: number, h: number): boolean {
|
||||
return this.x <= x + w && x <= this.right() && this.y <= y + h && y <= this.bottom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the rectangle contains the given circle at (x, y) with the radius (radius)
|
||||
* {}
|
||||
*/
|
||||
containsCircle(x: number, y: number, radius: number): boolean {
|
||||
return (this.x <= x + radius &&
|
||||
return (
|
||||
this.x <= x + radius &&
|
||||
x - radius <= this.right() &&
|
||||
this.y <= y + radius &&
|
||||
y - radius <= this.bottom());
|
||||
y - radius <= this.bottom()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the rectangle contains the given point
|
||||
* {}
|
||||
*/
|
||||
containsPoint(x: number, y: number): boolean {
|
||||
return x >= this.x && x < this.right() && y >= this.y && y < this.bottom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shared area with another rectangle, or null if there is no intersection
|
||||
* {}
|
||||
*/
|
||||
getIntersection(rect: Rectangle): Rectangle | null {
|
||||
const left = Math.max(this.x, rect.x);
|
||||
@ -221,6 +222,7 @@ export class Rectangle {
|
||||
}
|
||||
return Rectangle.fromTRBL(top, right, bottom, left);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the rectangle fully intersects the given rectangle
|
||||
*/
|
||||
@ -228,6 +230,7 @@ export class Rectangle {
|
||||
const intersection = this.getIntersection(rect);
|
||||
return intersection && Math.abs(intersection.w * intersection.h - rect.w * rect.h) < 0.001;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the union of this rectangle with another
|
||||
*/
|
||||
@ -247,23 +250,28 @@ export class Rectangle {
|
||||
const bottom = Math.max(this.bottom(), rect.bottom());
|
||||
return Rectangle.fromTRBL(top, right, bottom, left);
|
||||
}
|
||||
|
||||
/**
|
||||
* Good for caching stuff
|
||||
*/
|
||||
toCompareableString() {
|
||||
return (round2Digits(this.x) +
|
||||
return (
|
||||
round2Digits(this.x) +
|
||||
"/" +
|
||||
round2Digits(this.y) +
|
||||
"/" +
|
||||
round2Digits(this.w) +
|
||||
"/" +
|
||||
round2Digits(this.h));
|
||||
round2Digits(this.h)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Good for printing stuff
|
||||
*/
|
||||
toString() {
|
||||
return ("[x:" +
|
||||
return (
|
||||
"[x:" +
|
||||
round2Digits(this.x) +
|
||||
"| y:" +
|
||||
round2Digits(this.y) +
|
||||
@ -271,13 +279,19 @@ export class Rectangle {
|
||||
round2Digits(this.w) +
|
||||
"| h:" +
|
||||
round2Digits(this.h) +
|
||||
"]");
|
||||
"]"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new rectangle in tile space which includes all tiles which are visible in this rect
|
||||
* {}
|
||||
*/
|
||||
toTileCullRectangle(): Rectangle {
|
||||
return new Rectangle(Math.floor(this.x / globalConfig.tileSize), Math.floor(this.y / globalConfig.tileSize), Math.ceil(this.w / globalConfig.tileSize), Math.ceil(this.h / globalConfig.tileSize));
|
||||
return new Rectangle(
|
||||
Math.floor(this.x / globalConfig.tileSize),
|
||||
Math.floor(this.y / globalConfig.tileSize),
|
||||
Math.ceil(this.w / globalConfig.tileSize),
|
||||
Math.ceil(this.h / globalConfig.tileSize)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,42 +4,40 @@ const logger = createLogger("request_channel");
|
||||
// Thrown when a request is aborted
|
||||
export const PROMISE_ABORTED = "promise-aborted";
|
||||
export class RequestChannel {
|
||||
public pendingPromises: Array<Promise> = [];
|
||||
public pendingPromises: Array<Promise<any>> = [];
|
||||
|
||||
constructor() {
|
||||
}
|
||||
/**
|
||||
*
|
||||
* {}
|
||||
*/
|
||||
watch(promise: Promise<any>): Promise<any> {
|
||||
// log(this, "Added new promise:", promise, "(pending =", this.pendingPromises.length, ")");
|
||||
let cancelled = false;
|
||||
const wrappedPromise = new Promise((resolve, reject) => {
|
||||
promise.then(result => {
|
||||
// Remove from pending promises
|
||||
fastArrayDeleteValueIfContained(this.pendingPromises, wrappedPromise);
|
||||
// If not cancelled, resolve promise with same payload
|
||||
if (!cancelled) {
|
||||
resolve.call(this, result);
|
||||
promise.then(
|
||||
result => {
|
||||
// Remove from pending promises
|
||||
fastArrayDeleteValueIfContained(this.pendingPromises, wrappedPromise);
|
||||
|
||||
// If not cancelled, resolve promise with same payload
|
||||
if (!cancelled) {
|
||||
resolve.call(this, result);
|
||||
} else {
|
||||
logger.warn("Not resolving because promise got cancelled");
|
||||
// reject.call(this, PROMISE_ABORTED);
|
||||
}
|
||||
},
|
||||
err => {
|
||||
// Remove from pending promises
|
||||
fastArrayDeleteValueIfContained(this.pendingPromises, wrappedPromise);
|
||||
|
||||
// If not cancelled, reject promise with same payload
|
||||
if (!cancelled) {
|
||||
reject.call(this, err);
|
||||
} else {
|
||||
logger.warn("Not rejecting because promise got cancelled");
|
||||
// reject.call(this, PROMISE_ABORTED);
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.warn("Not resolving because promise got cancelled");
|
||||
// reject.call(this, PROMISE_ABORTED);
|
||||
}
|
||||
}, err => {
|
||||
// Remove from pending promises
|
||||
fastArrayDeleteValueIfContained(this.pendingPromises, wrappedPromise);
|
||||
// If not cancelled, reject promise with same payload
|
||||
if (!cancelled) {
|
||||
reject.call(this, err);
|
||||
}
|
||||
else {
|
||||
logger.warn("Not rejecting because promise got cancelled");
|
||||
// reject.call(this, PROMISE_ABORTED);
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
// Add cancel handler
|
||||
// @ts-ignore
|
||||
wrappedPromise.cancel = function () {
|
||||
@ -48,6 +46,7 @@ export class RequestChannel {
|
||||
this.pendingPromises.push(wrappedPromise);
|
||||
return wrappedPromise;
|
||||
}
|
||||
|
||||
cancelAll() {
|
||||
if (this.pendingPromises.length > 0) {
|
||||
logger.log("Cancel all pending promises (", this.pendingPromises.length, ")");
|
||||
|
@ -7,24 +7,24 @@ import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso";
|
||||
export class RestrictionManager extends ReadWriteProxy {
|
||||
public currentData = this.getDefaultData();
|
||||
|
||||
constructor(app) {
|
||||
constructor(app) {
|
||||
super(app, "restriction-flags.bin");
|
||||
}
|
||||
// -- RW Proxy Impl
|
||||
verify(data: any) {
|
||||
verify(data: any) {
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
|
||||
|
||||
getDefaultData() {
|
||||
return {
|
||||
version: this.getCurrentVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
getCurrentVersion() {
|
||||
return 1;
|
||||
}
|
||||
migrate(data: any) {
|
||||
migrate(data: any) {
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
initialize() {
|
||||
|
@ -16,6 +16,7 @@ function Mash() {
|
||||
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
|
||||
};
|
||||
}
|
||||
|
||||
function makeNewRng(seed: number | string) {
|
||||
// Johannes Baagøe <baagoe@baagoe.com>, 2010
|
||||
var c = 1;
|
||||
@ -23,6 +24,7 @@ function makeNewRng(seed: number | string) {
|
||||
let s0 = mash(" ");
|
||||
let s1 = mash(" ");
|
||||
let s2 = mash(" ");
|
||||
|
||||
s0 -= mash(seed);
|
||||
if (s0 < 0) {
|
||||
s0 += 1;
|
||||
@ -36,49 +38,59 @@ function makeNewRng(seed: number | string) {
|
||||
s2 += 1;
|
||||
}
|
||||
mash = null;
|
||||
|
||||
var random = function () {
|
||||
var t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
|
||||
s0 = s1;
|
||||
s1 = s2;
|
||||
return (s2 = t - (c = t | 0));
|
||||
};
|
||||
|
||||
random.exportState = function () {
|
||||
return [s0, s1, s2, c];
|
||||
};
|
||||
|
||||
random.importState = function (i) {
|
||||
s0 = +i[0] || 0;
|
||||
s1 = +i[1] || 0;
|
||||
s2 = +i[2] || 0;
|
||||
c = +i[3] || 0;
|
||||
};
|
||||
|
||||
return random;
|
||||
}
|
||||
export class RandomNumberGenerator {
|
||||
public internalRng = makeNewRng(seed || Math.random());
|
||||
|
||||
constructor(seed) {
|
||||
export class RandomNumberGenerator {
|
||||
public internalRng: () => number;
|
||||
|
||||
constructor(seed?: number | string) {
|
||||
this.internalRng = makeNewRng(seed || Math.random());
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-seeds the generator
|
||||
*/
|
||||
reseed(seed: number | string) {
|
||||
this.internalRng = makeNewRng(seed || Math.random());
|
||||
}
|
||||
|
||||
/**
|
||||
* {} between 0 and 1
|
||||
* @returns between 0 and 1
|
||||
*/
|
||||
next(): number {
|
||||
return this.internalRng();
|
||||
}
|
||||
|
||||
/**
|
||||
* Random choice of an array
|
||||
*/
|
||||
choice(array: array) {
|
||||
choice(array: any[]) {
|
||||
const index = this.nextIntRange(0, array.length);
|
||||
return array[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* {} Integer in range [min, max[
|
||||
* @returns Integer in range [min, max[]
|
||||
*/
|
||||
nextIntRange(min: number, max: number): number {
|
||||
assert(Number.isFinite(min), "Minimum is no integer");
|
||||
@ -86,13 +98,15 @@ export class RandomNumberGenerator {
|
||||
assert(max > min, "rng: max <= min");
|
||||
return Math.floor(this.next() * (max - min) + min);
|
||||
}
|
||||
|
||||
/**
|
||||
* {} Number in range [min, max[
|
||||
* @returns Number in range [min, max[
|
||||
*/
|
||||
nextRange(min: number, max: number): number {
|
||||
assert(max > min, "rng: max <= min");
|
||||
return this.next() * (max - min) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the seed
|
||||
*/
|
||||
|
@ -1,15 +1,19 @@
|
||||
import { createHash } from "rusha";
|
||||
import crc32 from "crc/crc32";
|
||||
import { decompressX64 } from "./lzstring";
|
||||
|
||||
export function sha1(str) {
|
||||
return createHash().update(str).digest("hex");
|
||||
}
|
||||
|
||||
// Window.location.host
|
||||
export function getNameOfProvider() {
|
||||
return window[decompressX64("DYewxghgLgliB2Q")][decompressX64("BYewzgLgdghgtgUyA")];
|
||||
}
|
||||
|
||||
// Distinguish legacy crc prefixes
|
||||
export const CRC_PREFIX = "crc32".padEnd(32, "-");
|
||||
|
||||
/**
|
||||
* Computes the crc for a given string
|
||||
*/
|
||||
|
@ -1,48 +1,55 @@
|
||||
export const STOP_PROPAGATION = "stop_propagation";
|
||||
export class Signal {
|
||||
public receivers = [];
|
||||
export type STOP_PROPAGATION = typeof STOP_PROPAGATION;
|
||||
|
||||
export class Signal<T extends any[]> {
|
||||
public receivers: {
|
||||
receiver: (...args: T) => STOP_PROPAGATION | void;
|
||||
scope: object;
|
||||
}[] = [];
|
||||
public modifyCount = 0;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
/**
|
||||
* Adds a new signal listener
|
||||
*/
|
||||
add(receiver: function, scope: object = null) {
|
||||
add(receiver: (...args: T) => STOP_PROPAGATION | void, scope: object = null) {
|
||||
assert(receiver, "receiver is null");
|
||||
this.receivers.push({ receiver, scope });
|
||||
++this.modifyCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new signal listener
|
||||
*/
|
||||
addToTop(receiver: function, scope: object = null) {
|
||||
addToTop(receiver: (...args: T) => STOP_PROPAGATION | void, scope: object = null) {
|
||||
assert(receiver, "receiver is null");
|
||||
this.receivers.unshift({ receiver, scope });
|
||||
++this.modifyCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the signal
|
||||
* @param {} payload
|
||||
*/
|
||||
dispatch() {
|
||||
dispatch(...payload: T): void | STOP_PROPAGATION {
|
||||
const modifyState = this.modifyCount;
|
||||
|
||||
const n = this.receivers.length;
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const { receiver, scope } = this.receivers[i];
|
||||
if (receiver.apply(scope, arguments) === STOP_PROPAGATION) {
|
||||
if (receiver.apply(scope, payload) === STOP_PROPAGATION) {
|
||||
return STOP_PROPAGATION;
|
||||
}
|
||||
|
||||
if (modifyState !== this.modifyCount) {
|
||||
// Signal got modified during iteration
|
||||
return STOP_PROPAGATION;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a receiver
|
||||
*/
|
||||
remove(receiver: function) {
|
||||
remove(receiver: (...args: any[]) => any) {
|
||||
let index = null;
|
||||
const n = this.receivers.length;
|
||||
for (let i = 0; i < n; ++i) {
|
||||
@ -55,6 +62,7 @@ export class Signal {
|
||||
this.receivers.splice(index, 1);
|
||||
++this.modifyCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all receivers
|
||||
*/
|
||||
|
@ -1,40 +1,46 @@
|
||||
import { createLogger } from "./logging";
|
||||
|
||||
const logger = createLogger("singleton_factory");
|
||||
// simple factory pattern
|
||||
export class SingletonFactory {
|
||||
public id = id;
|
||||
public entries = [];
|
||||
public idToEntry = {};
|
||||
export class SingletonFactory<T extends { getId(): string }> {
|
||||
public entries: T[] = [];
|
||||
public idToEntry: {
|
||||
[id: string]: T;
|
||||
} = {};
|
||||
|
||||
constructor(public id?: string) {}
|
||||
|
||||
constructor(id) {
|
||||
}
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
register(classHandle) {
|
||||
|
||||
register(classHandle: Class<T>) {
|
||||
// First, construct instance
|
||||
const instance = new classHandle();
|
||||
|
||||
// Extract id
|
||||
const id = instance.getId();
|
||||
assert(id, "Factory: Invalid id for class " + classHandle.name + ": " + id);
|
||||
|
||||
// Check duplicates
|
||||
assert(!this.idToEntry[id], "Duplicate factory entry for " + id);
|
||||
|
||||
// Insert
|
||||
this.entries.push(instance);
|
||||
this.idToEntry[id] = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given id is registered
|
||||
* {}
|
||||
*/
|
||||
hasId(id: string): boolean {
|
||||
return !!this.idToEntry[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an instance by a given id
|
||||
* {}
|
||||
*/
|
||||
findById(id: string): object {
|
||||
findById(id: string): T {
|
||||
const entry = this.idToEntry[id];
|
||||
if (!entry) {
|
||||
logger.error("Object with id", id, "is not registered!");
|
||||
@ -43,12 +49,11 @@ export class SingletonFactory {
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
/**
|
||||
|
||||
/**
|
||||
* Finds an instance by its constructor (The class handle)
|
||||
* {}
|
||||
*/
|
||||
findByClass(classHandle: object): object {
|
||||
findByClass(classHandle: Class<T>): T {
|
||||
for (let i = 0; i < this.entries.length; ++i) {
|
||||
if (this.entries[i] instanceof classHandle) {
|
||||
return this.entries[i];
|
||||
@ -57,23 +62,23 @@ export class SingletonFactory {
|
||||
assert(false, "Factory: Object not found by classHandle (classid: " + classHandle.name + ")");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all entries
|
||||
* {}
|
||||
*/
|
||||
getEntries(): Array<object> {
|
||||
getEntries(): Array<T> {
|
||||
return this.entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all registered ids
|
||||
* {}
|
||||
*/
|
||||
getAllIds(): Array<string> {
|
||||
return Object.keys(this.idToEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns amount of stored entries
|
||||
* {}
|
||||
*/
|
||||
getNumEntries(): number {
|
||||
return this.entries.length;
|
||||
|
@ -1,181 +1,288 @@
|
||||
import { DrawParameters } from "./draw_parameters";
|
||||
import { Rectangle } from "./rectangle";
|
||||
import { round3Digits } from "./utils";
|
||||
|
||||
export const ORIGINAL_SPRITE_SCALE = "0.75";
|
||||
export const FULL_CLIP_RECT = new Rectangle(0, 0, 1, 1);
|
||||
|
||||
const EXTRUDE = 0.1;
|
||||
export class BaseSprite {
|
||||
export abstract class BaseSprite {
|
||||
/**
|
||||
* Returns the raw handle
|
||||
* {}
|
||||
* @abstract
|
||||
*/
|
||||
getRawTexture(): HTMLImageElement | HTMLCanvasElement {
|
||||
abstract;
|
||||
return null;
|
||||
}
|
||||
abstract getRawTexture(): HTMLImageElement | HTMLCanvasElement;
|
||||
|
||||
/**
|
||||
* Draws the sprite
|
||||
*/
|
||||
draw(context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) {
|
||||
// eslint-disable-line no-unused-vars
|
||||
abstract;
|
||||
}
|
||||
abstract draw(context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Position of a sprite within an atlas
|
||||
*/
|
||||
export class SpriteAtlasLink {
|
||||
public packedX = packedX;
|
||||
public packedY = packedY;
|
||||
public packedW = packedW;
|
||||
public packedH = packedH;
|
||||
public packOffsetX = packOffsetX;
|
||||
public packOffsetY = packOffsetY;
|
||||
public atlas = atlas;
|
||||
public w = w;
|
||||
public h = h;
|
||||
public packedX: number;
|
||||
public packedY: number;
|
||||
public packedW: number;
|
||||
public packedH: number;
|
||||
public packOffsetX: number;
|
||||
public packOffsetY: number;
|
||||
public atlas: HTMLImageElement | HTMLCanvasElement;
|
||||
public w: number;
|
||||
public h: number;
|
||||
|
||||
constructor({ w, h, packedX, packedY, packOffsetX, packOffsetY, packedW, packedH, atlas }) {
|
||||
constructor({
|
||||
w,
|
||||
h,
|
||||
packedX,
|
||||
packedY,
|
||||
packOffsetX,
|
||||
packOffsetY,
|
||||
packedW,
|
||||
packedH,
|
||||
atlas,
|
||||
}: {
|
||||
packedX: number;
|
||||
packedY: number;
|
||||
packedW: number;
|
||||
packedH: number;
|
||||
packOffsetX: number;
|
||||
packOffsetY: number;
|
||||
atlas: HTMLImageElement | HTMLCanvasElement;
|
||||
w: number;
|
||||
h: number;
|
||||
}) {
|
||||
this.packedX = packedX;
|
||||
this.packedY = packedY;
|
||||
this.packedW = packedW;
|
||||
this.packedH = packedH;
|
||||
this.packOffsetX = packOffsetX;
|
||||
this.packOffsetY = packOffsetY;
|
||||
this.atlas = atlas;
|
||||
this.w = w;
|
||||
this.h = h;
|
||||
}
|
||||
}
|
||||
|
||||
export class AtlasSprite extends BaseSprite {
|
||||
public linksByResolution: {
|
||||
[idx: string]: SpriteAtlasLink;
|
||||
} = {};
|
||||
public spriteName = spriteName;
|
||||
|
||||
public frozen = false;
|
||||
|
||||
constructor(spriteName = "sprite") {
|
||||
constructor(public spriteName: string = "sprite") {
|
||||
super();
|
||||
}
|
||||
|
||||
getRawTexture() {
|
||||
return this.linksByResolution[ORIGINAL_SPRITE_SCALE].atlas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the sprite onto a regular context using no contexts
|
||||
* @see {BaseSprite.draw}
|
||||
*/
|
||||
draw(context, x, y, w, h) {
|
||||
draw(context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) {
|
||||
if (G_IS_DEV) {
|
||||
assert(context instanceof CanvasRenderingContext2D, "Not a valid context");
|
||||
}
|
||||
|
||||
const link = this.linksByResolution[ORIGINAL_SPRITE_SCALE];
|
||||
|
||||
if (!link) {
|
||||
throw new Error("draw: Link for " +
|
||||
this.spriteName +
|
||||
" not known: " +
|
||||
ORIGINAL_SPRITE_SCALE +
|
||||
" (having " +
|
||||
Object.keys(this.linksByResolution) +
|
||||
")");
|
||||
throw new Error(
|
||||
"draw: Link for " +
|
||||
this.spriteName +
|
||||
" not known: " +
|
||||
ORIGINAL_SPRITE_SCALE +
|
||||
" (having " +
|
||||
Object.keys(this.linksByResolution) +
|
||||
")"
|
||||
);
|
||||
}
|
||||
|
||||
const width = w || link.w;
|
||||
const height = h || link.h;
|
||||
|
||||
const scaleW = width / link.w;
|
||||
const scaleH = height / link.h;
|
||||
context.drawImage(link.atlas, link.packedX, link.packedY, link.packedW, link.packedH, x + link.packOffsetX * scaleW, y + link.packOffsetY * scaleH, link.packedW * scaleW, link.packedH * scaleH);
|
||||
|
||||
context.drawImage(
|
||||
link.atlas,
|
||||
link.packedX,
|
||||
link.packedY,
|
||||
link.packedW,
|
||||
link.packedH,
|
||||
x + link.packOffsetX * scaleW,
|
||||
y + link.packOffsetY * scaleH,
|
||||
link.packedW * scaleW,
|
||||
link.packedH * scaleH
|
||||
);
|
||||
}
|
||||
drawCachedCentered(parameters: DrawParameters, x: number, y: number, size: number, clipping: boolean= = true) {
|
||||
|
||||
drawCachedCentered(
|
||||
parameters: DrawParameters,
|
||||
x: number,
|
||||
y: number,
|
||||
size: number,
|
||||
clipping: boolean = true
|
||||
) {
|
||||
this.drawCached(parameters, x - size / 2, y - size / 2, size, size, clipping);
|
||||
}
|
||||
drawCentered(context: CanvasRenderingContext2D, x: number, y: number, size: number) {
|
||||
|
||||
drawCentered(context: CanvasRenderingContext2D, x: number, y: number, size: number) {
|
||||
this.draw(context, x - size / 2, y - size / 2, size, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the sprite
|
||||
*/
|
||||
drawCached(parameters: DrawParameters, x: number, y: number, w: number = null, h: number = null, clipping: boolean= = true) {
|
||||
drawCached(
|
||||
parameters: DrawParameters,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number = null,
|
||||
h: number = null,
|
||||
clipping: boolean = true
|
||||
) {
|
||||
if (G_IS_DEV) {
|
||||
assert(parameters instanceof DrawParameters, "Not a valid context");
|
||||
assert(!!w && w > 0, "Not a valid width:" + w);
|
||||
assert(!!h && h > 0, "Not a valid height:" + h);
|
||||
}
|
||||
|
||||
const visibleRect = parameters.visibleRect;
|
||||
|
||||
const scale = parameters.desiredAtlasScale;
|
||||
const link = this.linksByResolution[scale];
|
||||
|
||||
if (!link) {
|
||||
throw new Error("drawCached: Link for " +
|
||||
this.spriteName +
|
||||
" at scale " +
|
||||
scale +
|
||||
" not known (having " +
|
||||
Object.keys(this.linksByResolution) +
|
||||
")");
|
||||
throw new Error(
|
||||
"drawCached: Link for " +
|
||||
this.spriteName +
|
||||
" at scale " +
|
||||
scale +
|
||||
" not known (having " +
|
||||
Object.keys(this.linksByResolution) +
|
||||
")"
|
||||
);
|
||||
}
|
||||
|
||||
const scaleW = w / link.w;
|
||||
const scaleH = h / link.h;
|
||||
|
||||
let destX = x + link.packOffsetX * scaleW;
|
||||
let destY = y + link.packOffsetY * scaleH;
|
||||
let destW = link.packedW * scaleW;
|
||||
let destH = link.packedH * scaleH;
|
||||
|
||||
let srcX = link.packedX;
|
||||
let srcY = link.packedY;
|
||||
let srcW = link.packedW;
|
||||
let srcH = link.packedH;
|
||||
|
||||
let intersection = null;
|
||||
|
||||
if (clipping) {
|
||||
const rect = new Rectangle(destX, destY, destW, destH);
|
||||
intersection = rect.getIntersection(visibleRect);
|
||||
if (!intersection) {
|
||||
return;
|
||||
}
|
||||
|
||||
srcX += (intersection.x - destX) / scaleW;
|
||||
srcY += (intersection.y - destY) / scaleH;
|
||||
|
||||
srcW *= intersection.w / destW;
|
||||
srcH *= intersection.h / destH;
|
||||
|
||||
destX = intersection.x;
|
||||
destY = intersection.y;
|
||||
|
||||
destW = intersection.w;
|
||||
destH = intersection.h;
|
||||
}
|
||||
parameters.context.drawImage(link.atlas,
|
||||
// atlas src pos
|
||||
srcX, srcY,
|
||||
// atlas src size
|
||||
srcW, srcH,
|
||||
// dest pos and size
|
||||
destX - EXTRUDE, destY - EXTRUDE, destW + 2 * EXTRUDE, destH + 2 * EXTRUDE);
|
||||
|
||||
parameters.context.drawImage(
|
||||
link.atlas,
|
||||
// atlas src pos
|
||||
srcX,
|
||||
srcY,
|
||||
// atlas src size
|
||||
srcW,
|
||||
srcH,
|
||||
// dest pos and size
|
||||
destX - EXTRUDE,
|
||||
destY - EXTRUDE,
|
||||
destW + 2 * EXTRUDE,
|
||||
destH + 2 * EXTRUDE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a subset of the sprite. Does NO culling
|
||||
*/
|
||||
drawCachedWithClipRect(parameters: DrawParameters, x: number, y: number, w: number = null, h: number = null, clipRect: Rectangle= = FULL_CLIP_RECT) {
|
||||
drawCachedWithClipRect(
|
||||
parameters: DrawParameters,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number = null,
|
||||
h: number = null,
|
||||
clipRect: Rectangle = FULL_CLIP_RECT
|
||||
) {
|
||||
if (G_IS_DEV) {
|
||||
assert(parameters instanceof DrawParameters, "Not a valid context");
|
||||
assert(!!w && w > 0, "Not a valid width:" + w);
|
||||
assert(!!h && h > 0, "Not a valid height:" + h);
|
||||
assert(clipRect, "No clip rect given!");
|
||||
}
|
||||
|
||||
const scale = parameters.desiredAtlasScale;
|
||||
const link = this.linksByResolution[scale];
|
||||
|
||||
if (!link) {
|
||||
throw new Error("drawCachedWithClipRect: Link for " +
|
||||
this.spriteName +
|
||||
" at scale " +
|
||||
scale +
|
||||
" not known (having " +
|
||||
Object.keys(this.linksByResolution) +
|
||||
")");
|
||||
throw new Error(
|
||||
"drawCachedWithClipRect: Link for " +
|
||||
this.spriteName +
|
||||
" at scale " +
|
||||
scale +
|
||||
" not known (having " +
|
||||
Object.keys(this.linksByResolution) +
|
||||
")"
|
||||
);
|
||||
}
|
||||
|
||||
const scaleW = w / link.w;
|
||||
const scaleH = h / link.h;
|
||||
|
||||
let destX = x + link.packOffsetX * scaleW + clipRect.x * w;
|
||||
let destY = y + link.packOffsetY * scaleH + clipRect.y * h;
|
||||
let destW = link.packedW * scaleW * clipRect.w;
|
||||
let destH = link.packedH * scaleH * clipRect.h;
|
||||
|
||||
let srcX = link.packedX + clipRect.x * link.packedW;
|
||||
let srcY = link.packedY + clipRect.y * link.packedH;
|
||||
let srcW = link.packedW * clipRect.w;
|
||||
let srcH = link.packedH * clipRect.h;
|
||||
parameters.context.drawImage(link.atlas,
|
||||
// atlas src pos
|
||||
srcX, srcY,
|
||||
// atlas src siize
|
||||
srcW, srcH,
|
||||
// dest pos and size
|
||||
destX - EXTRUDE, destY - EXTRUDE, destW + 2 * EXTRUDE, destH + 2 * EXTRUDE);
|
||||
|
||||
parameters.context.drawImage(
|
||||
link.atlas,
|
||||
// atlas src pos
|
||||
srcX,
|
||||
srcY,
|
||||
// atlas src siize
|
||||
srcW,
|
||||
srcH,
|
||||
// dest pos and size
|
||||
destX - EXTRUDE,
|
||||
destY - EXTRUDE,
|
||||
destW + 2 * EXTRUDE,
|
||||
destH + 2 * EXTRUDE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders into an html element
|
||||
*/
|
||||
@ -183,44 +290,56 @@ export class AtlasSprite extends BaseSprite {
|
||||
element.style.position = "relative";
|
||||
element.innerHTML = this.getAsHTML(w, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the html to render as icon
|
||||
*/
|
||||
getAsHTML(w: number, h: number) {
|
||||
const link = this.linksByResolution["0.5"];
|
||||
if (!link) {
|
||||
throw new Error("getAsHTML: Link for " +
|
||||
this.spriteName +
|
||||
" at scale 0.5" +
|
||||
" not known (having " +
|
||||
Object.keys(this.linksByResolution) +
|
||||
")");
|
||||
throw new Error(
|
||||
"getAsHTML: Link for " +
|
||||
this.spriteName +
|
||||
" at scale 0.5" +
|
||||
" not known (having " +
|
||||
Object.keys(this.linksByResolution) +
|
||||
")"
|
||||
);
|
||||
}
|
||||
|
||||
// Find out how much we have to scale it so that it fits
|
||||
const scaleX = w / link.w;
|
||||
const scaleY = h / link.h;
|
||||
|
||||
// Find out how big the scaled atlas is
|
||||
const atlasW = link.atlas.width * scaleX;
|
||||
const atlasH = link.atlas.height * scaleY;
|
||||
|
||||
// @ts-ignore
|
||||
const srcSafe = link.atlas.src.replaceAll("\\", "/");
|
||||
|
||||
// Find out how big we render the sprite
|
||||
const widthAbsolute = scaleX * link.packedW;
|
||||
const heightAbsolute = scaleY * link.packedH;
|
||||
|
||||
// Compute the position in the relative container
|
||||
const leftRelative = (link.packOffsetX * scaleX) / w;
|
||||
const topRelative = (link.packOffsetY * scaleY) / h;
|
||||
const widthRelative = widthAbsolute / w;
|
||||
const heightRelative = heightAbsolute / h;
|
||||
|
||||
// Scale the atlas relative to the width and height of the element
|
||||
const bgW = atlasW / widthAbsolute;
|
||||
const bgH = atlasH / heightAbsolute;
|
||||
|
||||
// Figure out what the position of the atlas is
|
||||
const bgX = link.packedX * scaleX;
|
||||
const bgY = link.packedY * scaleY;
|
||||
|
||||
// Fuck you, whoever thought its a good idea to make background-position work like it does now
|
||||
const bgXRelative = -bgX / (widthAbsolute - atlasW);
|
||||
const bgYRelative = -bgY / (heightAbsolute - atlasH);
|
||||
|
||||
return `
|
||||
<span class="spritesheetImage" style="
|
||||
background-image: url('${srcSafe}');
|
||||
@ -229,23 +348,23 @@ export class AtlasSprite extends BaseSprite {
|
||||
width: ${round3Digits(widthRelative * 100.0)}%;
|
||||
height: ${round3Digits(heightRelative * 100.0)}%;
|
||||
background-repeat: repeat;
|
||||
background-position: ${round3Digits(bgXRelative * 100.0)}% ${round3Digits(bgYRelative * 100.0)}%;
|
||||
background-position: ${round3Digits(bgXRelative * 100.0)}% ${round3Digits(
|
||||
bgYRelative * 100.0
|
||||
)}%;
|
||||
background-size: ${round3Digits(bgW * 100.0)}% ${round3Digits(bgH * 100.0)}%;
|
||||
"></span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
export class RegularSprite extends BaseSprite {
|
||||
public w = w;
|
||||
public h = h;
|
||||
public sprite = sprite;
|
||||
|
||||
constructor(sprite, w, h) {
|
||||
constructor(public sprite: HTMLCanvasElement | HTMLImageElement, public w: number, public h: number) {
|
||||
super();
|
||||
}
|
||||
|
||||
getRawTexture() {
|
||||
return this.sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing
|
||||
* images into buffers
|
||||
@ -258,6 +377,7 @@ export class RegularSprite extends BaseSprite {
|
||||
assert(h !== undefined, "No height given");
|
||||
context.drawImage(this.sprite, x, y, w, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing
|
||||
* images into buffers
|
||||
|
@ -3,15 +3,31 @@ import { Entity } from "../game/entity";
|
||||
import { globalConfig } from "./config";
|
||||
import { createLogger } from "./logging";
|
||||
import { Rectangle } from "./rectangle";
|
||||
|
||||
import type { GameRoot } from "../game/root";
|
||||
|
||||
const logger = createLogger("stale_areas");
|
||||
|
||||
export class StaleAreaDetector {
|
||||
public root = root;
|
||||
public name = name;
|
||||
public recomputeMethod = recomputeMethod;
|
||||
public root: GameRoot;
|
||||
public name: string;
|
||||
public recomputeMethod: (rect: Rectangle) => void;
|
||||
public staleArea: Rectangle = null;
|
||||
|
||||
constructor({ root, name, recomputeMethod }) {
|
||||
constructor({
|
||||
root,
|
||||
name,
|
||||
recomputeMethod,
|
||||
}: {
|
||||
root: GameRoot;
|
||||
name: string;
|
||||
recomputeMethod: (rect: Rectangle) => void;
|
||||
}) {
|
||||
this.root = root;
|
||||
this.name = name;
|
||||
this.recomputeMethod = recomputeMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the given area
|
||||
*/
|
||||
@ -19,17 +35,18 @@ export class StaleAreaDetector {
|
||||
// logger.log(this.name, "invalidated", area.toString());
|
||||
if (this.staleArea) {
|
||||
this.staleArea = this.staleArea.getUnion(area);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.staleArea = area.clone();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes this detector recompute the area of an entity whenever
|
||||
* it changes in any way
|
||||
*/
|
||||
recomputeOnComponentsChanged(components: Array<typeof Component>, tilesAround: number) {
|
||||
const componentIds = components.map(component => component.getId());
|
||||
|
||||
/**
|
||||
* Internal checker method
|
||||
*/
|
||||
@ -37,22 +54,28 @@ export class StaleAreaDetector {
|
||||
if (!this.root.gameInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for all components
|
||||
for (let i = 0; i < componentIds.length; ++i) {
|
||||
if (entity.components[componentIds[i]]) {
|
||||
// Entity is relevant, compute affected area
|
||||
const area = entity.components.StaticMapEntity.getTileSpaceBounds().expandedInAllDirections(tilesAround);
|
||||
const area =
|
||||
entity.components.StaticMapEntity.getTileSpaceBounds().expandedInAllDirections(
|
||||
tilesAround
|
||||
);
|
||||
this.invalidate(area);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.root.signals.entityAdded.add(checker);
|
||||
this.root.signals.entityChanged.add(checker);
|
||||
this.root.signals.entityComponentRemoved.add(checker);
|
||||
this.root.signals.entityGotNewComponent.add(checker);
|
||||
this.root.signals.entityDestroyed.add(checker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the stale area
|
||||
*/
|
||||
|
@ -1,27 +1,27 @@
|
||||
/* typehints:start*/
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end*/
|
||||
|
||||
import { GameState } from "./game_state";
|
||||
import { createLogger } from "./logging";
|
||||
import { waitNextFrame, removeAllChildren } from "./utils";
|
||||
import { MOD_SIGNALS } from "../mods/mod_signals";
|
||||
|
||||
const logger = createLogger("state_manager");
|
||||
|
||||
/**
|
||||
* This is the main state machine which drives the game states.
|
||||
*/
|
||||
export class StateManager {
|
||||
public app = app;
|
||||
public currentState: GameState = null;
|
||||
public stateClasses: {
|
||||
[idx: string]: new () => GameState;
|
||||
} = {};
|
||||
|
||||
constructor(app) {
|
||||
}
|
||||
constructor(public app: Application) {}
|
||||
|
||||
/**
|
||||
* Registers a new state class, should be a GameState derived class
|
||||
*/
|
||||
register(stateClass: object) {
|
||||
register(stateClass: { new (): GameState }) {
|
||||
// Create a dummy to retrieve the key
|
||||
const dummy = new stateClass();
|
||||
assert(dummy instanceof GameState, "Not a state!");
|
||||
@ -29,6 +29,7 @@ export class StateManager {
|
||||
assert(!this.stateClasses[key], `State '${key}' is already registered!`);
|
||||
this.stateClasses[key] = stateClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new state or returns the instance from the cache
|
||||
*/
|
||||
@ -38,20 +39,23 @@ export class StateManager {
|
||||
}
|
||||
assert(false, `State '${key}' is not known!`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves to a given state
|
||||
*/
|
||||
moveToState(key: string, payload = {}) {
|
||||
moveToState(key: string, payload: object = {}) {
|
||||
if (window.APP_ERROR_OCCURED) {
|
||||
console.warn("Skipping state transition because of application crash");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentState) {
|
||||
if (key === this.currentState.getKey()) {
|
||||
logger.error(`State '${key}' is already active!`);
|
||||
return false;
|
||||
}
|
||||
this.currentState.internalLeaveCallback();
|
||||
|
||||
// Remove all references
|
||||
for (const stateKey in this.currentState) {
|
||||
if (this.currentState.hasOwnProperty(stateKey)) {
|
||||
@ -60,42 +64,56 @@ export class StateManager {
|
||||
}
|
||||
this.currentState = null;
|
||||
}
|
||||
|
||||
this.currentState = this.constructState(key);
|
||||
this.currentState.internalRegisterCallback(this, this.app);
|
||||
|
||||
// Clean up old elements
|
||||
if (this.currentState.getRemovePreviousContent()) {
|
||||
removeAllChildren(document.body);
|
||||
}
|
||||
|
||||
document.body.className = "gameState " + (this.currentState.getHasFadeIn() ? "" : "arrived");
|
||||
document.body.id = "state_" + key;
|
||||
|
||||
if (this.currentState.getRemovePreviousContent()) {
|
||||
document.body.innerHTML = this.currentState.internalGetFullHtml();
|
||||
}
|
||||
|
||||
const dialogParent = document.createElement("div");
|
||||
dialogParent.classList.add("modalDialogParent");
|
||||
document.body.appendChild(dialogParent);
|
||||
try {
|
||||
this.currentState.internalEnterCallback(payload);
|
||||
}
|
||||
catch (ex) {
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
this.app.sound.playThemeMusic(this.currentState.getThemeMusic());
|
||||
|
||||
this.currentState.onResized(this.app.screenWidth, this.app.screenHeight);
|
||||
|
||||
this.app.analytics.trackStateEnter(key);
|
||||
window.history.pushState({
|
||||
key,
|
||||
}, key);
|
||||
|
||||
window.history.pushState(
|
||||
{
|
||||
key,
|
||||
},
|
||||
key
|
||||
);
|
||||
|
||||
MOD_SIGNALS.stateEntered.dispatch(this.currentState);
|
||||
|
||||
waitNextFrame().then(() => {
|
||||
document.body.classList.add("arrived");
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state
|
||||
* {}
|
||||
*/
|
||||
getCurrentState(): GameState {
|
||||
return this.currentState;
|
||||
|
@ -1,23 +1,34 @@
|
||||
import { Application } from "../application";
|
||||
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
||||
import { T } from "../translations";
|
||||
import { openStandaloneLink } from "./config";
|
||||
|
||||
export let WEB_STEAM_SSO_AUTHENTICATED = false;
|
||||
export async function authorizeViaSSOToken(app, dialogs) {
|
||||
|
||||
export async function authorizeViaSSOToken(app: Application, dialogs: HUDModalDialogs) {
|
||||
if (G_IS_STANDALONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.location.search.includes("sso_logout_silent")) {
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
window.location.replace("/");
|
||||
return new Promise(() => null);
|
||||
}
|
||||
|
||||
if (window.location.search.includes("sso_logout")) {
|
||||
const { ok } = dialogs.showWarning(T.dialogs.steamSsoError.title, T.dialogs.steamSsoError.desc);
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
ok.add(() => window.location.replace("/"));
|
||||
return new Promise(() => null);
|
||||
}
|
||||
|
||||
if (window.location.search.includes("steam_sso_no_ownership")) {
|
||||
const { ok, getStandalone } = dialogs.showWarning(T.dialogs.steamSsoNoOwnership.title, T.dialogs.steamSsoNoOwnership.desc, ["ok", "getStandalone:good"]);
|
||||
const { ok, getStandalone } = dialogs.showWarning(
|
||||
T.dialogs.steamSsoNoOwnership.title,
|
||||
T.dialogs.steamSsoNoOwnership.desc,
|
||||
["ok", "getStandalone:good"]
|
||||
);
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
getStandalone.add(() => {
|
||||
openStandaloneLink(app, "sso_ownership");
|
||||
@ -26,20 +37,24 @@ export async function authorizeViaSSOToken(app, dialogs) {
|
||||
ok.add(() => window.location.replace("/"));
|
||||
return new Promise(() => null);
|
||||
}
|
||||
|
||||
const token = window.localStorage.getItem("steam_sso_auth_token");
|
||||
if (!token) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const apiUrl = app.clientApi.getEndpoint();
|
||||
console.warn("Authorizing via token:", token);
|
||||
|
||||
const verify = async () => {
|
||||
const token = window.localStorage.getItem("steam_sso_auth_token");
|
||||
if (!token) {
|
||||
window.location.replace("?sso_logout");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await Promise.race([
|
||||
const response = (await Promise.race([
|
||||
fetch(apiUrl + "/v1/sso/refresh", {
|
||||
method: "POST",
|
||||
body: token,
|
||||
@ -50,7 +65,8 @@ export async function authorizeViaSSOToken(app, dialogs) {
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(() => reject("timeout exceeded"), 20000);
|
||||
}),
|
||||
]);
|
||||
])) as Response;
|
||||
|
||||
const responseText = await response.json();
|
||||
if (!responseText.token) {
|
||||
console.warn("Failed to register");
|
||||
@ -58,17 +74,18 @@ export async function authorizeViaSSOToken(app, dialogs) {
|
||||
window.location.replace("?sso_logout");
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem("steam_sso_auth_token", responseText.token);
|
||||
app.clientApi.token = responseText.token;
|
||||
WEB_STEAM_SSO_AUTHENTICATED = true;
|
||||
}
|
||||
catch (ex) {
|
||||
} catch (ex) {
|
||||
console.warn("Auth failure", ex);
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
window.location.replace("/");
|
||||
return new Promise(() => null);
|
||||
}
|
||||
};
|
||||
|
||||
await verify();
|
||||
setInterval(verify, 120000);
|
||||
}
|
||||
|
@ -2,15 +2,22 @@ import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
||||
import { GameState } from "./game_state";
|
||||
import { T } from "../translations";
|
||||
/**
|
||||
* Baseclass for all game states which are structured similary: A header with back button + some
|
||||
* Baseclass for all game states which are structured similarly: A header with back button + some
|
||||
* scrollable content.
|
||||
*/
|
||||
export class TextualGameState extends GameState {
|
||||
public backToStateId: string;
|
||||
public backToStatePayload: object;
|
||||
|
||||
public containerElement: HTMLDivElement;
|
||||
public headerElement: HTMLDivElement;
|
||||
public dialogs: HUDModalDialogs;
|
||||
|
||||
///// INTERFACE ////
|
||||
|
||||
/**
|
||||
* Should return the states inner html. If not overriden, will create a scrollable container
|
||||
* Should return the states inner html. If not overridden, will create a scrollable container
|
||||
* with the content of getMainContentHTML()
|
||||
* {}
|
||||
*/
|
||||
getInnerHTML(): string {
|
||||
return `
|
||||
@ -19,21 +26,24 @@ export class TextualGameState extends GameState {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return the states HTML content.
|
||||
*/
|
||||
getMainContentHTML() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return the title of the game state. If null, no title and back button will
|
||||
* get created
|
||||
* {}
|
||||
*/
|
||||
getStateHeaderTitle(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/////////////
|
||||
|
||||
/**
|
||||
* Back button handler, can be overridden. Per default it goes back to the main menu,
|
||||
* or if coming from the game it moves back to the game again.
|
||||
@ -41,17 +51,18 @@ export class TextualGameState extends GameState {
|
||||
onBackButton() {
|
||||
if (this.backToStateId) {
|
||||
this.moveToState(this.backToStateId, this.backToStatePayload);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.moveToState(this.getDefaultPreviousState());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default state to go back to
|
||||
*/
|
||||
getDefaultPreviousState() {
|
||||
return "MainMenuState";
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes to a new state, telling him to go back to this state later
|
||||
*/
|
||||
@ -64,6 +75,7 @@ export class TextualGameState extends GameState {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all click detectors, except the one on the back button. Useful when regenerating
|
||||
* content.
|
||||
@ -79,6 +91,7 @@ export class TextualGameState extends GameState {
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the GameState implementation to provide our own html
|
||||
*/
|
||||
@ -87,10 +100,11 @@ export class TextualGameState extends GameState {
|
||||
if (this.getStateHeaderTitle()) {
|
||||
headerHtml = `
|
||||
<div class="headerBar">
|
||||
|
||||
|
||||
<h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
${headerHtml}
|
||||
<div class="container">
|
||||
@ -99,7 +113,9 @@ export class TextualGameState extends GameState {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
//// INTERNALS /////
|
||||
|
||||
/**
|
||||
* Overrides the GameState leave callback to cleanup stuff
|
||||
*/
|
||||
@ -107,6 +123,7 @@ export class TextualGameState extends GameState {
|
||||
super.internalLeaveCallback();
|
||||
this.dialogs.cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the GameState enter callback to setup required stuff
|
||||
*/
|
||||
@ -116,18 +133,23 @@ export class TextualGameState extends GameState {
|
||||
this.backToStateId = payload.backToStateId;
|
||||
this.backToStatePayload = payload.backToStatePayload;
|
||||
}
|
||||
|
||||
this.htmlElement.classList.add("textualState");
|
||||
if (this.getStateHeaderTitle()) {
|
||||
this.htmlElement.classList.add("hasTitle");
|
||||
}
|
||||
|
||||
this.containerElement = this.htmlElement.querySelector(".widthKeeper .container");
|
||||
this.headerElement = this.htmlElement.querySelector(".headerBar > h1");
|
||||
|
||||
if (this.headerElement) {
|
||||
this.trackClicks(this.headerElement, this.onBackButton);
|
||||
}
|
||||
|
||||
this.dialogs = new HUDModalDialogs(null, this.app);
|
||||
const dialogsElement = document.body.querySelector(".modalDialogParent");
|
||||
this.dialogs.initializeToElement(dialogsElement);
|
||||
|
||||
this.onEnter(payload);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
export class TrackedState {
|
||||
public lastSeenValue = null;
|
||||
export class TrackedState<T> {
|
||||
public callback: (value: T) => void;
|
||||
public callbackScope: object;
|
||||
|
||||
constructor(callbackMethod = null, callbackScope = null) {
|
||||
public lastSeenValue: T = null;
|
||||
|
||||
constructor(callbackMethod: (value: T) => void = null, callbackScope: any = null) {
|
||||
if (callbackMethod) {
|
||||
this.callback = callbackMethod;
|
||||
if (callbackScope) {
|
||||
@ -9,7 +12,8 @@ export class TrackedState {
|
||||
}
|
||||
}
|
||||
}
|
||||
set(value, changeHandler = null, changeScope = null) {
|
||||
|
||||
set(value: T, changeHandler: (value: T) => void = null, changeScope: object = null) {
|
||||
if (value !== this.lastSeenValue) {
|
||||
// Copy value since the changeHandler call could actually modify our lastSeenValue
|
||||
const valueCopy = value;
|
||||
@ -17,23 +21,22 @@ export class TrackedState {
|
||||
if (changeHandler) {
|
||||
if (changeScope) {
|
||||
changeHandler.call(changeScope, valueCopy);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
changeHandler(valueCopy);
|
||||
}
|
||||
}
|
||||
else if (this.callback) {
|
||||
} else if (this.callback) {
|
||||
this.callback(value);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
assert(false, "No callback specified");
|
||||
}
|
||||
}
|
||||
}
|
||||
setSilent(value) {
|
||||
|
||||
setSilent(value: T) {
|
||||
this.lastSeenValue = value;
|
||||
}
|
||||
get() {
|
||||
|
||||
get(): T {
|
||||
return this.lastSeenValue;
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,23 @@
|
||||
import { T } from "../translations";
|
||||
import { rando } from "@nastyox/rando.js";
|
||||
import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso";
|
||||
|
||||
const bigNumberSuffixTranslationKeys = ["thousands", "millions", "billions", "trillions"];
|
||||
|
||||
/**
|
||||
* Returns a platform name
|
||||
* {}
|
||||
*/
|
||||
export function getPlatformName(): "android" | "browser" | "ios" | "standalone" | "unknown" {
|
||||
if (G_IS_STANDALONE) {
|
||||
return "standalone";
|
||||
}
|
||||
else if (G_IS_BROWSER) {
|
||||
} else if (G_IS_BROWSER) {
|
||||
return "browser";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a new 2D array with undefined contents
|
||||
* {}
|
||||
*/
|
||||
export function make2DUndefinedArray(w: number, h: number): Array<Array<any>> {
|
||||
const result = new Array(w);
|
||||
@ -26,36 +26,39 @@ export function make2DUndefinedArray(w: number, h: number): Array<Array<any>> {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new map (an empty object without any props)
|
||||
*/
|
||||
export function newEmptyMap() {
|
||||
export function newEmptyMap(): object {
|
||||
return Object.create(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random integer in the range [start,end]
|
||||
*/
|
||||
export function randomInt(start: number, end: number) {
|
||||
return rando(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access an object in a very annoying way, used for obsfuscation.
|
||||
*/
|
||||
export function accessNestedPropertyReverse(obj: any, keys: Array<string>) {
|
||||
export function accessNestedPropertyReverse(obj: any, keys: Array<string>): any {
|
||||
let result = obj;
|
||||
for (let i = keys.length - 1; i >= 0; --i) {
|
||||
result = result[keys[i]];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses a random entry of an array
|
||||
* @template T
|
||||
* {}
|
||||
*/
|
||||
export function randomChoice(arr: T[]): T {
|
||||
export function randomChoice<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes from an array by swapping with the last element
|
||||
*/
|
||||
@ -69,9 +72,11 @@ export function fastArrayDelete(array: Array<any>, index: number) {
|
||||
const last = array[array.length - 1];
|
||||
array[index] = last;
|
||||
}
|
||||
|
||||
// Finally remove the last element
|
||||
array.length -= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes from an array by swapping with the last element. Searches
|
||||
* for the value in the array first
|
||||
@ -87,6 +92,7 @@ export function fastArrayDeleteValue(array: Array<any>, value: any) {
|
||||
}
|
||||
return fastArrayDelete(array, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see fastArrayDeleteValue
|
||||
*/
|
||||
@ -100,6 +106,7 @@ export function fastArrayDeleteValueIfContained(array: Array<any>, value: any) {
|
||||
}
|
||||
return fastArrayDelete(array, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes from an array at the given index
|
||||
*/
|
||||
@ -109,6 +116,7 @@ export function arrayDelete(array: Array<any>, index: number) {
|
||||
}
|
||||
array.splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given value from an array
|
||||
*/
|
||||
@ -123,19 +131,21 @@ export function arrayDeleteValue(array: Array<any>, value: any) {
|
||||
}
|
||||
return arrayDelete(array, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two floats for epsilon equality
|
||||
* {}
|
||||
*/
|
||||
export function epsilonCompare(a: number, b: number, epsilon = 1e-5): boolean {
|
||||
return Math.abs(a - b) < epsilon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates two numbers
|
||||
*/
|
||||
export function lerp(a: number, b: number, x: number) {
|
||||
return a * (1 - x) + b * x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a value which is nice to display, e.g. 15669 -> 15000. Also handles fractional stuff
|
||||
*/
|
||||
@ -149,25 +159,20 @@ export function findNiceValue(num: number) {
|
||||
let roundAmount = 1;
|
||||
if (num > 50000) {
|
||||
roundAmount = 10000;
|
||||
}
|
||||
else if (num > 20000) {
|
||||
} else if (num > 20000) {
|
||||
roundAmount = 5000;
|
||||
}
|
||||
else if (num > 5000) {
|
||||
} else if (num > 5000) {
|
||||
roundAmount = 1000;
|
||||
}
|
||||
else if (num > 2000) {
|
||||
} else if (num > 2000) {
|
||||
roundAmount = 500;
|
||||
}
|
||||
else if (num > 1000) {
|
||||
} else if (num > 1000) {
|
||||
roundAmount = 100;
|
||||
}
|
||||
else if (num > 100) {
|
||||
} else if (num > 100) {
|
||||
roundAmount = 20;
|
||||
}
|
||||
else if (num > 20) {
|
||||
} else if (num > 20) {
|
||||
roundAmount = 5;
|
||||
}
|
||||
|
||||
const niceValue = Math.floor(num / roundAmount) * roundAmount;
|
||||
if (num >= 10) {
|
||||
return Math.round(niceValue);
|
||||
@ -175,8 +180,10 @@ export function findNiceValue(num: number) {
|
||||
if (num >= 1) {
|
||||
return Math.round(niceValue * 10) / 10;
|
||||
}
|
||||
|
||||
return Math.round(niceValue * 100) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a nice integer value
|
||||
* @see findNiceValue
|
||||
@ -184,16 +191,18 @@ export function findNiceValue(num: number) {
|
||||
export function findNiceIntegerValue(num: number) {
|
||||
return Math.ceil(findNiceValue(num));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a big number
|
||||
* {}
|
||||
*/
|
||||
export function formatBigNumber(num: number, separator: string= = T.global.decimalSeparator): string {
|
||||
export function formatBigNumber(num: number, separator: string = T.global.decimalSeparator): string {
|
||||
const sign = num < 0 ? "-" : "";
|
||||
num = Math.abs(num);
|
||||
|
||||
if (num > 1e54) {
|
||||
return sign + T.global.infinite;
|
||||
}
|
||||
|
||||
if (num < 10 && !Number.isInteger(num)) {
|
||||
return sign + num.toFixed(2);
|
||||
}
|
||||
@ -201,10 +210,10 @@ export function formatBigNumber(num: number, separator: string= = T.global.decim
|
||||
return sign + num.toFixed(1);
|
||||
}
|
||||
num = Math.floor(num);
|
||||
|
||||
if (num < 1000) {
|
||||
return sign + "" + num;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
let leadingDigits = num;
|
||||
let suffix = "";
|
||||
for (let suffixIndex = 0; suffixIndex < bigNumberSuffixTranslationKeys.length; ++suffixIndex) {
|
||||
@ -222,11 +231,11 @@ export function formatBigNumber(num: number, separator: string= = T.global.decim
|
||||
return sign + leadingDigitsNoTrailingDecimal + suffix;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a big number, but does not add any suffix and instead uses its full representation
|
||||
* {}
|
||||
*/
|
||||
export function formatBigNumberFull(num: number, divider: string= = T.global.thousandsDivider): string {
|
||||
export function formatBigNumberFull(num: number, divider: string = T.global.thousandsDivider): string {
|
||||
if (num < 1000) {
|
||||
return num + "";
|
||||
}
|
||||
@ -240,11 +249,12 @@ export function formatBigNumberFull(num: number, divider: string= = T.global.tho
|
||||
rest = Math.floor(rest / 1000);
|
||||
}
|
||||
out = rest + divider + out;
|
||||
|
||||
return out.substring(0, out.length - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits two frames so the ui is updated
|
||||
* {}
|
||||
*/
|
||||
export function waitNextFrame(): Promise<void> {
|
||||
return new Promise(function (resolve) {
|
||||
@ -255,44 +265,46 @@ export function waitNextFrame(): Promise<void> {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds 1 digit
|
||||
* {}
|
||||
*/
|
||||
export function round1Digit(n: number): number {
|
||||
return Math.floor(n * 10.0) / 10.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds 2 digits
|
||||
* {}
|
||||
*/
|
||||
export function round2Digits(n: number): number {
|
||||
return Math.floor(n * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds 3 digits
|
||||
* {}
|
||||
*/
|
||||
export function round3Digits(n: number): number {
|
||||
return Math.floor(n * 1000.0) / 1000.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds 4 digits
|
||||
* {}
|
||||
*/
|
||||
export function round4Digits(n: number): number {
|
||||
return Math.floor(n * 10000.0) / 10000.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps a value between [min, max]
|
||||
*/
|
||||
export function clamp(v: number, minimum: number= = 0, maximum: number= = 1) {
|
||||
export function clamp(v: number, minimum: number = 0, maximum: number = 1) {
|
||||
return Math.max(minimum, Math.min(maximum, v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a new div element
|
||||
*/
|
||||
export function makeDivElement(id: string= = null, classes: Array<string>= = [], innerHTML: string= = "") {
|
||||
export function makeDivElement(id: string = null, classes: Array<string> = [], innerHTML: string = "") {
|
||||
const div = document.createElement("div");
|
||||
if (id) {
|
||||
div.id = id;
|
||||
@ -303,18 +315,25 @@ export function makeDivElement(id: string= = null, classes: Array<string>= = [],
|
||||
div.innerHTML = innerHTML;
|
||||
return div;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a new div
|
||||
*/
|
||||
export function makeDiv(parent: Element, id: string= = null, classes: Array<string>= = [], innerHTML: string= = "") {
|
||||
export function makeDiv(
|
||||
parent: Element,
|
||||
id: string = null,
|
||||
classes: Array<string> = [],
|
||||
innerHTML: string = ""
|
||||
) {
|
||||
const div = makeDivElement(id, classes, innerHTML);
|
||||
parent.appendChild(div);
|
||||
return div;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a new button element
|
||||
*/
|
||||
export function makeButtonElement(classes: Array<string>= = [], innerHTML: string= = "") {
|
||||
export function makeButtonElement(classes: Array<string> = [], innerHTML: string = "") {
|
||||
const element = document.createElement("button");
|
||||
for (let i = 0; i < classes.length; ++i) {
|
||||
element.classList.add(classes[i]);
|
||||
@ -323,14 +342,16 @@ export function makeButtonElement(classes: Array<string>= = [], innerHTML: strin
|
||||
element.innerHTML = innerHTML;
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a new button
|
||||
*/
|
||||
export function makeButton(parent: Element, classes: Array<string>= = [], innerHTML: string= = "") {
|
||||
export function makeButton(parent: Element, classes: Array<string> = [], innerHTML: string = "") {
|
||||
const element = makeButtonElement(classes, innerHTML);
|
||||
parent.appendChild(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all children of the given element
|
||||
*/
|
||||
@ -341,6 +362,7 @@ export function removeAllChildren(elem: Element) {
|
||||
range.deleteContents();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the game supports this browser
|
||||
*/
|
||||
@ -352,9 +374,11 @@ export function isSupportedBrowser() {
|
||||
// and new IE Edge outputs to true now for window.chrome
|
||||
// and if not iOS Chrome check
|
||||
// so use the below updated condition
|
||||
|
||||
if (G_IS_STANDALONE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
var isChromium = window.chrome;
|
||||
var winNav = window.navigator;
|
||||
@ -362,93 +386,102 @@ export function isSupportedBrowser() {
|
||||
// @ts-ignore
|
||||
var isIEedge = winNav.userAgent.indexOf("Edge") > -1;
|
||||
var isIOSChrome = winNav.userAgent.match("CriOS");
|
||||
|
||||
if (isIOSChrome) {
|
||||
// is Google Chrome on IOS
|
||||
return false;
|
||||
}
|
||||
else if (isChromium !== null &&
|
||||
} else if (
|
||||
isChromium !== null &&
|
||||
typeof isChromium !== "undefined" &&
|
||||
vendorName === "Google Inc." &&
|
||||
isIEedge === false) {
|
||||
isIEedge === false
|
||||
) {
|
||||
// is Google Chrome
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// not Google Chrome
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an amount of seconds into something like "5s ago"
|
||||
* {}
|
||||
*/
|
||||
export function formatSecondsToTimeAgo(secs: number): string {
|
||||
const seconds = Math.floor(secs);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (seconds < 60) {
|
||||
if (seconds === 1) {
|
||||
return T.global.time.oneSecondAgo;
|
||||
}
|
||||
return T.global.time.xSecondsAgo.replace("<x>", "" + seconds);
|
||||
}
|
||||
else if (minutes < 60) {
|
||||
} else if (minutes < 60) {
|
||||
if (minutes === 1) {
|
||||
return T.global.time.oneMinuteAgo;
|
||||
}
|
||||
return T.global.time.xMinutesAgo.replace("<x>", "" + minutes);
|
||||
}
|
||||
else if (hours < 24) {
|
||||
} else if (hours < 24) {
|
||||
if (hours === 1) {
|
||||
return T.global.time.oneHourAgo;
|
||||
}
|
||||
return T.global.time.xHoursAgo.replace("<x>", "" + hours);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (days === 1) {
|
||||
return T.global.time.oneDayAgo;
|
||||
}
|
||||
return T.global.time.xDaysAgo.replace("<x>", "" + days);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats seconds into a readable string like "5h 23m"
|
||||
* {}
|
||||
*/
|
||||
export function formatSeconds(secs: number): string {
|
||||
const trans = T.global.time;
|
||||
secs = Math.ceil(secs);
|
||||
if (secs < 60) {
|
||||
return trans.secondsShort.replace("<seconds>", "" + secs);
|
||||
}
|
||||
else if (secs < 60 * 60) {
|
||||
} else if (secs < 60 * 60) {
|
||||
const minutes = Math.floor(secs / 60);
|
||||
const seconds = secs % 60;
|
||||
return trans.minutesAndSecondsShort
|
||||
.replace("<seconds>", "" + seconds)
|
||||
.replace("<minutes>", "" + minutes);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const hours = Math.floor(secs / 3600);
|
||||
const minutes = Math.floor(secs / 60) % 60;
|
||||
return trans.hoursAndMinutesShort.replace("<minutes>", "" + minutes).replace("<hours>", "" + hours);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number like 2.51 to "2.5"
|
||||
*/
|
||||
export function round1DigitLocalized(speed: number, separator: string= = T.global.decimalSeparator) {
|
||||
export function round1DigitLocalized(speed: number, separator: string = T.global.decimalSeparator) {
|
||||
return round1Digit(speed).toString().replace(".", separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number like 2.51 to "2.51 items / s"
|
||||
*/
|
||||
export function formatItemsPerSecond(speed: number, double: boolean= = false, separator: string= = T.global.decimalSeparator) {
|
||||
return ((speed === 1.0
|
||||
? T.ingame.buildingPlacement.infoTexts.oneItemPerSecond
|
||||
: T.ingame.buildingPlacement.infoTexts.itemsPerSecond.replace("<x>", round2Digits(speed).toString().replace(".", separator))) + (double ? " " + T.ingame.buildingPlacement.infoTexts.itemsPerSecondDouble : ""));
|
||||
export function formatItemsPerSecond(
|
||||
speed: number,
|
||||
double: boolean = false,
|
||||
separator: string = T.global.decimalSeparator
|
||||
) {
|
||||
return (
|
||||
(speed === 1.0
|
||||
? T.ingame.buildingPlacement.infoTexts.oneItemPerSecond
|
||||
: T.ingame.buildingPlacement.infoTexts.itemsPerSecond.replace(
|
||||
"<x>",
|
||||
round2Digits(speed).toString().replace(".", separator)
|
||||
)) + (double ? " " + T.ingame.buildingPlacement.infoTexts.itemsPerSecondDouble : "")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates a flat 3x3 matrix clockwise
|
||||
* Entries:
|
||||
@ -475,11 +508,11 @@ export function rotateFlatMatrix3x3(flatMatrix: Array<number>) {
|
||||
flatMatrix[2],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates rotated variants of the matrix
|
||||
* {}
|
||||
*/
|
||||
export function generateMatrixRotations(originalMatrix: Array<number>): Object<number, Array<number>> {
|
||||
export function generateMatrixRotations(originalMatrix: Array<number>): { [idx: number]: Array<number> } {
|
||||
const result = {
|
||||
0: originalMatrix,
|
||||
};
|
||||
@ -492,9 +525,15 @@ export function generateMatrixRotations(originalMatrix: Array<number>): Object<n
|
||||
return result;
|
||||
}
|
||||
|
||||
export type DirectionalObject = {
|
||||
top: any;
|
||||
right: any;
|
||||
bottom: any;
|
||||
left: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* Rotates a directional object
|
||||
* {}
|
||||
*/
|
||||
export function rotateDirectionalObject(obj: DirectionalObject, rotation): DirectionalObject {
|
||||
const queue = [obj.top, obj.right, obj.bottom, obj.left];
|
||||
@ -502,6 +541,7 @@ export function rotateDirectionalObject(obj: DirectionalObject, rotation): Direc
|
||||
rotation -= 90;
|
||||
queue.push(queue.shift());
|
||||
}
|
||||
|
||||
return {
|
||||
top: queue[0],
|
||||
right: queue[1],
|
||||
@ -509,19 +549,21 @@ export function rotateDirectionalObject(obj: DirectionalObject, rotation): Direc
|
||||
left: queue[3],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Modulo which works for negative numbers
|
||||
*/
|
||||
export function safeModulo(n: number, m: number) {
|
||||
return ((n % m) + m) % m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a smooth pulse between 0 and 1
|
||||
* {}
|
||||
*/
|
||||
export function smoothPulse(time: number): number {
|
||||
return Math.sin(time * 4) * 0.5 + 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills in a <link> tag
|
||||
*/
|
||||
@ -530,6 +572,7 @@ export function fillInLinkIntoTranslation(translation: string, link: string) {
|
||||
.replace("<link>", "<a href='" + link + "' target='_blank'>")
|
||||
.replace("</link>", "</a>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a file download
|
||||
*/
|
||||
@ -537,11 +580,14 @@ export function generateFileDownload(filename: string, text: string) {
|
||||
var element = document.createElement("a");
|
||||
element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
|
||||
element.setAttribute("download", filename);
|
||||
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a file chooser
|
||||
*/
|
||||
@ -549,25 +595,26 @@ export function startFileChoose(acceptedType: string = ".bin") {
|
||||
var input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = acceptedType;
|
||||
|
||||
return new Promise(resolve => {
|
||||
input.onchange = _ => resolve(input.files[0]);
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
const MAX_ROMAN_NUMBER = 49;
|
||||
const romanLiteralsCache = ["0"];
|
||||
/**
|
||||
*
|
||||
* {}
|
||||
*/
|
||||
|
||||
export function getRomanNumber(number: number): string {
|
||||
number = Math.max(0, Math.round(number));
|
||||
if (romanLiteralsCache[number]) {
|
||||
return romanLiteralsCache[number];
|
||||
}
|
||||
|
||||
if (number > MAX_ROMAN_NUMBER) {
|
||||
return String(number);
|
||||
}
|
||||
|
||||
function formatDigit(digit, unit, quintuple, decuple) {
|
||||
switch (digit) {
|
||||
case 0:
|
||||
@ -587,22 +634,29 @@ export function getRomanNumber(number: number): string {
|
||||
return quintuple + formatDigit(digit - 5, unit, quintuple, decuple);
|
||||
}
|
||||
}
|
||||
|
||||
let thousands = Math.floor(number / 1000);
|
||||
let thousandsPart = "";
|
||||
while (thousands > 0) {
|
||||
thousandsPart += "M";
|
||||
thousands -= 1;
|
||||
}
|
||||
|
||||
const hundreds = Math.floor((number % 1000) / 100);
|
||||
const hundredsPart = formatDigit(hundreds, "C", "D", "M");
|
||||
|
||||
const tens = Math.floor((number % 100) / 10);
|
||||
const tensPart = formatDigit(tens, "X", "L", "C");
|
||||
|
||||
const units = number % 10;
|
||||
const unitsPart = formatDigit(units, "I", "V", "X");
|
||||
|
||||
const formatted = thousandsPart + hundredsPart + tensPart + unitsPart;
|
||||
|
||||
romanLiteralsCache[number] = formatted;
|
||||
return formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate logo sprite path
|
||||
*/
|
||||
@ -615,10 +669,11 @@ export function getLogoSprite() {
|
||||
}
|
||||
return "logo.png";
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects a promise after X ms
|
||||
*/
|
||||
export function timeoutPromise(promise: Promise, timeout = 30000) {
|
||||
export function timeoutPromise(promise: Promise<any>, timeout: number = 30000) {
|
||||
return Promise.race([
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(() => reject("timeout of " + timeout + " ms exceeded"), timeout);
|
||||
|
@ -1,324 +1,315 @@
|
||||
import { globalConfig } from "./config";
|
||||
import { safeModulo } from "./utils";
|
||||
|
||||
const tileSize = globalConfig.tileSize;
|
||||
const halfTileSize = globalConfig.halfTileSize;
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const enumDirection = {
|
||||
top: "top",
|
||||
right: "right",
|
||||
bottom: "bottom",
|
||||
left: "left",
|
||||
};
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
|
||||
export enum enumDirection {
|
||||
top = "top",
|
||||
right = "right",
|
||||
bottom = "bottom",
|
||||
left = "left",
|
||||
}
|
||||
|
||||
export const enumInvertedDirections = {
|
||||
[enumDirection.top]: enumDirection.bottom,
|
||||
[enumDirection.right]: enumDirection.left,
|
||||
[enumDirection.bottom]: enumDirection.top,
|
||||
[enumDirection.left]: enumDirection.right,
|
||||
};
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
|
||||
export const enumDirectionToAngle = {
|
||||
[enumDirection.top]: 0,
|
||||
[enumDirection.right]: 90,
|
||||
[enumDirection.bottom]: 180,
|
||||
[enumDirection.left]: 270,
|
||||
};
|
||||
/**
|
||||
* @enum {enumDirection}
|
||||
*/
|
||||
|
||||
export const enumAngleToDirection = {
|
||||
0: enumDirection.top,
|
||||
90: enumDirection.right,
|
||||
180: enumDirection.bottom,
|
||||
270: enumDirection.left,
|
||||
};
|
||||
|
||||
export const arrayAllDirections: Array<enumDirection> = [
|
||||
enumDirection.top,
|
||||
enumDirection.right,
|
||||
enumDirection.bottom,
|
||||
enumDirection.left,
|
||||
];
|
||||
export class Vector {
|
||||
public x = x || 0;
|
||||
public y = y || 0;
|
||||
|
||||
constructor(x, y) {
|
||||
}
|
||||
export class Vector {
|
||||
constructor(public x: number = 0, public y: number = 0) {}
|
||||
|
||||
/**
|
||||
* return a copy of the vector
|
||||
* {}
|
||||
*/
|
||||
copy(): Vector {
|
||||
return new Vector(this.x, this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a vector and return a new vector
|
||||
* {}
|
||||
*/
|
||||
add(other: Vector): Vector {
|
||||
return new Vector(this.x + other.x, this.y + other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a vector
|
||||
* {}
|
||||
*/
|
||||
addInplace(other: Vector): Vector {
|
||||
this.x += other.x;
|
||||
this.y += other.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Substracts a vector and return a new vector
|
||||
* {}
|
||||
*/
|
||||
sub(other: Vector): Vector {
|
||||
return new Vector(this.x - other.x, this.y - other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subs a vector
|
||||
* {}
|
||||
*/
|
||||
subInplace(other: Vector): Vector {
|
||||
this.x -= other.x;
|
||||
this.y -= other.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies with a vector and return a new vector
|
||||
* {}
|
||||
*/
|
||||
mul(other: Vector): Vector {
|
||||
return new Vector(this.x * other.x, this.y * other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two scalars and return a new vector
|
||||
* {}
|
||||
*/
|
||||
addScalars(x: number, y: number): Vector {
|
||||
return new Vector(this.x + x, this.y + y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Substracts a scalar and return a new vector
|
||||
* {}
|
||||
*/
|
||||
subScalar(f: number): Vector {
|
||||
return new Vector(this.x - f, this.y - f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Substracts two scalars and return a new vector
|
||||
* {}
|
||||
*/
|
||||
subScalars(x: number, y: number): Vector {
|
||||
return new Vector(this.x - x, this.y - y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the euclidian length
|
||||
* {}
|
||||
*/
|
||||
length(): number {
|
||||
return Math.hypot(this.x, this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the square length
|
||||
* {}
|
||||
*/
|
||||
lengthSquare(): number {
|
||||
return this.x * this.x + this.y * this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Divides both components by a scalar and return a new vector
|
||||
* {}
|
||||
*/
|
||||
divideScalar(f: number): Vector {
|
||||
return new Vector(this.x / f, this.y / f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Divides both components by the given scalars and return a new vector
|
||||
* {}
|
||||
*/
|
||||
divideScalars(a: number, b: number): Vector {
|
||||
return new Vector(this.x / a, this.y / b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Divides both components by a scalar
|
||||
* {}
|
||||
*/
|
||||
divideScalarInplace(f: number): Vector {
|
||||
this.x /= f;
|
||||
this.y /= f;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies both components with a scalar and return a new vector
|
||||
* {}
|
||||
*/
|
||||
multiplyScalar(f: number): Vector {
|
||||
return new Vector(this.x * f, this.y * f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies both components with two scalars and returns a new vector
|
||||
* {}
|
||||
*/
|
||||
multiplyScalars(a: number, b: number): Vector {
|
||||
return new Vector(this.x * a, this.y * b);
|
||||
}
|
||||
|
||||
/**
|
||||
* For both components, compute the maximum of each component and the given scalar, and return a new vector.
|
||||
* For example:
|
||||
* - new Vector(-1, 5).maxScalar(0) -> Vector(0, 5)
|
||||
* {}
|
||||
*/
|
||||
maxScalar(f: number): Vector {
|
||||
return new Vector(Math.max(f, this.x), Math.max(f, this.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a scalar to both components and return a new vector
|
||||
* {}
|
||||
*/
|
||||
addScalar(f: number): Vector {
|
||||
return new Vector(this.x + f, this.y + f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the component wise minimum and return a new vector
|
||||
* {}
|
||||
*/
|
||||
min(v: Vector): Vector {
|
||||
return new Vector(Math.min(v.x, this.x), Math.min(v.y, this.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the component wise maximum and return a new vector
|
||||
* {}
|
||||
*/
|
||||
max(v: Vector): Vector {
|
||||
return new Vector(Math.max(v.x, this.x), Math.max(v.y, this.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the component wise absolute
|
||||
* {}
|
||||
*/
|
||||
abs(): Vector {
|
||||
return new Vector(Math.abs(this.x), Math.abs(this.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the scalar product
|
||||
* {}
|
||||
*/
|
||||
dot(v: Vector): number {
|
||||
return this.x * v.x + this.y * v.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the distance to a given vector
|
||||
* {}
|
||||
*/
|
||||
distance(v: Vector): number {
|
||||
return Math.hypot(this.x - v.x, this.y - v.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the square distance to a given vectort
|
||||
* {}
|
||||
*/
|
||||
distanceSquare(v: Vector): number {
|
||||
const dx = this.x - v.x;
|
||||
const dy = this.y - v.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns x % f, y % f
|
||||
* {} new vector
|
||||
*/
|
||||
modScalar(f: number): Vector {
|
||||
return new Vector(safeModulo(this.x, f), safeModulo(this.y, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and returns the center between both points
|
||||
* {}
|
||||
*/
|
||||
centerPoint(v: Vector): Vector {
|
||||
const cx = this.x + v.x;
|
||||
const cy = this.y + v.y;
|
||||
return new Vector(cx / 2, cy / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes componentwise floor and returns a new vector
|
||||
* {}
|
||||
*/
|
||||
floor(): Vector {
|
||||
return new Vector(Math.floor(this.x), Math.floor(this.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes componentwise ceil and returns a new vector
|
||||
* {}
|
||||
*/
|
||||
ceil(): Vector {
|
||||
return new Vector(Math.ceil(this.x), Math.ceil(this.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes componentwise round and return a new vector
|
||||
* {}
|
||||
*/
|
||||
round(): Vector {
|
||||
return new Vector(Math.round(this.x), Math.round(this.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this vector from world to tile space and return a new vector
|
||||
* {}
|
||||
*/
|
||||
toTileSpace(): Vector {
|
||||
return new Vector(Math.floor(this.x / tileSize), Math.floor(this.y / tileSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this vector from world to street space and return a new vector
|
||||
* {}
|
||||
*/
|
||||
toStreetSpace(): Vector {
|
||||
return new Vector(Math.floor(this.x / halfTileSize + 0.25), Math.floor(this.y / halfTileSize + 0.25));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this vector to world space and return a new vector
|
||||
* {}
|
||||
*/
|
||||
toWorldSpace(): Vector {
|
||||
return new Vector(this.x * tileSize, this.y * tileSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this vector to world space and return a new vector
|
||||
* {}
|
||||
*/
|
||||
toWorldSpaceCenterOfTile(): Vector {
|
||||
return new Vector(this.x * tileSize + halfTileSize, this.y * tileSize + halfTileSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the top left tile position of this vector
|
||||
* {}
|
||||
*/
|
||||
snapWorldToTile(): Vector {
|
||||
return new Vector(Math.floor(this.x / tileSize) * tileSize, Math.floor(this.y / tileSize) * tileSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the vector, dividing by the length(), and return a new vector
|
||||
* {}
|
||||
*/
|
||||
normalize(): Vector {
|
||||
const len = Math.max(1e-5, Math.hypot(this.x, this.y));
|
||||
return new Vector(this.x / len, this.y / len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the vector, dividing by the length(), and return a new vector
|
||||
* {}
|
||||
*/
|
||||
normalizeIfGreaterOne(): Vector {
|
||||
const len = Math.max(1, Math.hypot(this.x, this.y));
|
||||
return new Vector(this.x / len, this.y / len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized vector to the other point
|
||||
* {}
|
||||
*/
|
||||
normalizedDirection(v: Vector): Vector {
|
||||
const dx = v.x - this.x;
|
||||
@ -326,52 +317,55 @@ export class Vector {
|
||||
const len = Math.max(1e-5, Math.hypot(dx, dy));
|
||||
return new Vector(dx / len, dy / len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a perpendicular vector
|
||||
* {}
|
||||
*/
|
||||
findPerpendicular(): Vector {
|
||||
return new Vector(-this.y, this.x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unnormalized direction to the other point
|
||||
* {}
|
||||
*/
|
||||
direction(v: Vector): Vector {
|
||||
return new Vector(v.x - this.x, v.y - this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the vector
|
||||
* {}
|
||||
*/
|
||||
toString(): string {
|
||||
return this.x + "," + this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares both vectors for exact equality. Does not do an epsilon compare
|
||||
* {}
|
||||
*/
|
||||
equals(v: Vector): Boolean {
|
||||
return this.x === v.x && this.y === v.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates this vector
|
||||
* {} new vector
|
||||
* @returns new vector
|
||||
*/
|
||||
rotated(angle: number): Vector {
|
||||
const sin = Math.sin(angle);
|
||||
const cos = Math.cos(angle);
|
||||
return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates this vector
|
||||
* {} this vector
|
||||
* @returns this vector
|
||||
*/
|
||||
rotateInplaceFastMultipleOf90(angle: number): Vector {
|
||||
// const sin = Math.sin(angle);
|
||||
// const cos = Math.cos(angle);
|
||||
// let sin = 0, cos = 1;
|
||||
assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle);
|
||||
|
||||
switch (angle) {
|
||||
case 0:
|
||||
case 360: {
|
||||
@ -380,6 +374,7 @@ export class Vector {
|
||||
case 90: {
|
||||
// sin = 1;
|
||||
// cos = 0;
|
||||
|
||||
const x = this.x;
|
||||
this.x = -this.y;
|
||||
this.y = x;
|
||||
@ -388,6 +383,7 @@ export class Vector {
|
||||
case 180: {
|
||||
// sin = 0
|
||||
// cos = -1
|
||||
|
||||
this.x = -this.x;
|
||||
this.y = -this.y;
|
||||
return this;
|
||||
@ -395,6 +391,7 @@ export class Vector {
|
||||
case 270: {
|
||||
// sin = -1
|
||||
// cos = 0
|
||||
|
||||
const x = this.x;
|
||||
this.x = this.y;
|
||||
this.y = -x;
|
||||
@ -407,12 +404,14 @@ export class Vector {
|
||||
}
|
||||
// return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates this vector
|
||||
* {} new vector
|
||||
* @returns new vector
|
||||
*/
|
||||
rotateFastMultipleOf90(angle: number): Vector {
|
||||
assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle);
|
||||
|
||||
switch (angle) {
|
||||
case 360:
|
||||
case 0: {
|
||||
@ -433,9 +432,9 @@ export class Vector {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to rotate a direction
|
||||
* {}
|
||||
*/
|
||||
static transformDirectionFromMultipleOf90(direction: enumDirection, angle: number): enumDirection {
|
||||
if (angle === 0 || angle === 360) {
|
||||
@ -500,74 +499,72 @@ export class Vector {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares both vectors for epsilon equality
|
||||
* {}
|
||||
*/
|
||||
equalsEpsilon(v: Vector, epsilon = 1e-5): Boolean {
|
||||
return Math.abs(this.x - v.x) < 1e-5 && Math.abs(this.y - v.y) < epsilon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the angle
|
||||
* {} 0 .. 2 PI
|
||||
* @returns 0 .. 2 PI
|
||||
*/
|
||||
angle(): number {
|
||||
return Math.atan2(this.y, this.x) + Math.PI / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the vector to a string
|
||||
* {}
|
||||
*/
|
||||
serializeTile(): string {
|
||||
return String.fromCharCode(33 + this.x) + String.fromCharCode(33 + this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simple representation of the vector
|
||||
*/
|
||||
serializeSimple() {
|
||||
return { x: this.x, y: this.y };
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
|
||||
serializeTileToInt(): number {
|
||||
return this.x + this.y * 256;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* {}
|
||||
*/
|
||||
|
||||
static deserializeTileFromInt(i: number): Vector {
|
||||
const x = i % 256;
|
||||
const y = Math.floor(i / 256);
|
||||
return new Vector(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a vector from a string
|
||||
* {}
|
||||
*/
|
||||
static deserializeTile(s: string): Vector {
|
||||
return new Vector(s.charCodeAt(0) - 33, s.charCodeAt(1) - 33);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a vector from a serialized json object
|
||||
* {}
|
||||
*/
|
||||
static fromSerializedObject(obj: object): Vector {
|
||||
static fromSerializedObject(obj: any): Vector {
|
||||
if (obj) {
|
||||
return new Vector(obj.x || 0, obj.y || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates two vectors, for a = 0, returns v1 and for a = 1 return v2, otherwise interpolate
|
||||
*/
|
||||
export function mixVector(v1: Vector, v2: Vector, a: number) {
|
||||
return new Vector(v1.x * (1 - a) + v2.x * a, v1.y * (1 - a) + v2.y * a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping from string direction to actual vector
|
||||
* @enum {Vector}
|
||||
*/
|
||||
export const enumDirectionToVector = {
|
||||
top: new Vector(0, -1),
|
||||
|
Loading…
Reference in New Issue
Block a user