1
0
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:
Bagel03 2022-11-18 22:53:49 -05:00
parent 4dd57c1605
commit c399e0d7f1
43 changed files with 2183 additions and 1152 deletions

View File

@ -1,16 +1,21 @@
import { Signal } from "./signal"; import { Signal } from "./signal";
// @ts-ignore // @ts-ignore
import BackgroundAnimationFrameEmitterWorker from "../webworkers/background_animation_frame_emittter.worker"; import BackgroundAnimationFrameEmitterWorker from "../webworkers/background_animation_frame_emittter.worker";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
const logger = createLogger("animation_frame"); const logger = createLogger("animation_frame");
const maxDtMs = 1000; const maxDtMs = 1000;
const resetDtMs = 16; const resetDtMs = 16;
export class AnimationFrame { export class AnimationFrame {
public frameEmitted = new Signal(); public frameEmitted = new Signal<[dt: number]>();
public bgFrameEmitted = new Signal(); public bgFrameEmitted = new Signal<[dt: number]>();
public lastTime = performance.now(); public lastTime = performance.now();
public bgLastTime = performance.now(); public bgLastTime = performance.now();
public boundMethod = this.handleAnimationFrame.bind(this); public boundMethod = this.handleAnimationFrame.bind(this);
public backgroundWorker = new BackgroundAnimationFrameEmitterWorker(); public backgroundWorker = new BackgroundAnimationFrameEmitterWorker();
constructor() { constructor() {
@ -19,28 +24,35 @@ export class AnimationFrame {
}); });
this.backgroundWorker.addEventListener("message", this.handleBackgroundTick.bind(this)); this.backgroundWorker.addEventListener("message", this.handleBackgroundTick.bind(this));
} }
handleBackgroundTick() { handleBackgroundTick() {
const time = performance.now(); const time = performance.now();
let dt = time - this.bgLastTime; let dt = time - this.bgLastTime;
if (dt > maxDtMs) { if (dt > maxDtMs) {
dt = resetDtMs; dt = resetDtMs;
} }
this.bgFrameEmitted.dispatch(dt); this.bgFrameEmitted.dispatch(dt);
this.bgLastTime = time; this.bgLastTime = time;
} }
start() { start() {
assertAlways(window.requestAnimationFrame, "requestAnimationFrame is not supported!"); assertAlways(window.requestAnimationFrame, "requestAnimationFrame is not supported!");
this.handleAnimationFrame(); this.handleAnimationFrame(0);
} }
handleAnimationFrame(time) {
handleAnimationFrame(time: number) {
let dt = time - this.lastTime; let dt = time - this.lastTime;
if (dt > maxDtMs) { if (dt > maxDtMs) {
dt = resetDtMs; dt = resetDtMs;
} }
try { try {
this.frameEmitted.dispatch(dt); this.frameEmitted.dispatch(dt);
} } catch (ex) {
catch (ex) {
console.error(ex); console.error(ex);
} }
this.lastTime = time; this.lastTime = time;

View File

@ -1,13 +1,15 @@
import { createLogger } from "./logging"; import { createLogger } from "./logging";
const logger = createLogger("assert"); const logger = createLogger("assert");
let assertionErrorShown = false; let assertionErrorShown = false;
function initAssert() { function initAssert() {
/** /**
* Expects a given condition to be true * Expects a given condition to be true
* @param {} failureMessage
*/ */
// @ts-ignore // @ts-ignore
window.assert = function (condition: Boolean, ...failureMessage: ...String) { window.assert = function (condition: boolean, ...failureMessage: string[]) {
if (!condition) { if (!condition) {
logger.error("assertion failed:", ...failureMessage); logger.error("assertion failed:", ...failureMessage);
if (!assertionErrorShown) { if (!assertionErrorShown) {
@ -18,4 +20,5 @@ function initAssert() {
} }
}; };
} }
initAssert(); initAssert();

View File

@ -1,20 +1,24 @@
// @ts-ignore // @ts-ignore
import CompressionWorker from "../webworkers/compression.worker"; import CompressionWorker from "../webworkers/compression.worker";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { round2Digits } from "./utils"; import { round2Digits } from "./utils";
const logger = createLogger("async_compression"); const logger = createLogger("async_compression");
export let compressionPrefix = String.fromCodePoint(1); export let compressionPrefix = String.fromCodePoint(1);
function checkCryptPrefix(prefix) {
function checkCryptPrefix(prefix: string) {
try { try {
window.localStorage.setItem("prefix_test", prefix); window.localStorage.setItem("prefix_test", prefix);
window.localStorage.removeItem("prefix_test"); window.localStorage.removeItem("prefix_test");
return true; return true;
} } catch (ex) {
catch (ex) {
logger.warn("Prefix '" + prefix + "' not available"); logger.warn("Prefix '" + prefix + "' not available");
return false; return false;
} }
} }
if (!checkCryptPrefix(compressionPrefix)) { if (!checkCryptPrefix(compressionPrefix)) {
logger.warn("Switching to basic prefix"); logger.warn("Switching to basic prefix");
compressionPrefix = " "; compressionPrefix = " ";
@ -22,15 +26,18 @@ if (!checkCryptPrefix(compressionPrefix)) {
logger.warn("Prefix not available, ls seems to be unavailable"); logger.warn("Prefix not available, ls seems to be unavailable");
} }
} }
export type JobEntry = { export type JobEntry = {
errorHandler: function(: void):void; errorHandler: (err: any) => void;
resolver: function(: void):void; resolver: (res: any) => void;
startTime: number; startTime: number;
}; };
class AsynCompression { class AsynCompression {
public worker = new CompressionWorker(); public worker = new CompressionWorker();
public currentJobId = 1000; public currentJobId = 1000;
public currentJobs: { public currentJobs: {
[idx: number]: JobEntry; [idx: number]: JobEntry;
} = {}; } = {};
@ -43,12 +50,22 @@ class AsynCompression {
logger.error("Failed to resolve job result, job id", jobId, "is not known"); logger.error("Failed to resolve job result, job id", jobId, "is not known");
return; return;
} }
const duration = performance.now() - jobData.startTime; 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; const resolver = jobData.resolver;
delete this.currentJobs[jobId]; delete this.currentJobs[jobId];
resolver(result); resolver(result);
}); });
this.worker.addEventListener("error", err => { this.worker.addEventListener("error", err => {
logger.error("Got error from webworker:", err, "aborting all jobs"); logger.error("Got error from webworker:", err, "aborting all jobs");
const failureCalls = []; const failureCalls = [];
@ -61,6 +78,7 @@ class AsynCompression {
} }
}); });
} }
/** /**
* Compresses any object * Compresses any object
*/ */
@ -71,9 +89,9 @@ class AsynCompression {
compressionPrefix, compressionPrefix,
}); });
} }
/** /**
* Queues a new job * Queues a new job
* {}
*/ */
internalQueueJob(job: string, data: any): Promise<any> { internalQueueJob(job: string, data: any): Promise<any> {
const jobId = ++this.currentJobId; const jobId = ++this.currentJobId;
@ -87,9 +105,11 @@ class AsynCompression {
resolver: resolve, resolver: resolve,
startTime: performance.now(), startTime: performance.now(),
}; };
logger.log("Posting job", job, "/", jobId); logger.log("Posting job", job, "/", jobId);
this.worker.postMessage({ jobId, job, data }); this.worker.postMessage({ jobId, job, data });
}); });
} }
} }
export const asyncCompressor = new AsynCompression(); export const asyncCompressor = new AsynCompression();

View File

@ -1,4 +1,3 @@
export type Size = { export type Size = {
w: number; w: number;
h: number; h: number;
@ -24,20 +23,29 @@ export type AtlasMeta = {
smartupdate: string; smartupdate: string;
}; };
export type SourceData = { export type SourceData = {
frames: Object<string, SpriteDefinition>; frames: {
[idx: string]: SpriteDefinition;
};
meta: AtlasMeta; meta: AtlasMeta;
}; };
export class AtlasDefinition { export class AtlasDefinition {
public meta = meta; public meta: AtlasMeta;
public sourceData = frames; public sourceData: {
public sourceFileName = meta.image; [idx: string]: SpriteDefinition;
};
public sourceFileName: string;
constructor({ frames, meta }) { constructor({ frames, meta }: SourceData) {
this.meta = meta;
this.sourceData = frames;
this.sourceFileName = meta.image;
} }
getFullSourcePath() { getFullSourcePath() {
return this.sourceFileName; return this.sourceFileName;
} }
} }
export const atlasFiles: AtlasDefinition[] = require export const atlasFiles: AtlasDefinition[] = require
// @ts-ignore // @ts-ignore
.context("../../../res_built/atlas/", false, /.*\.json/i) .context("../../../res_built/atlas/", false, /.*\.json/i)

View File

@ -1,6 +1,6 @@
/* typehints:start */
import type { Application } from "../application"; import type { Application } from "../application";
/* typehints:end */ import type { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { initSpriteCache } from "../game/meta_building_registry"; import { initSpriteCache } from "../game/meta_building_registry";
import { MUSIC, SOUNDS } from "../platform/sound"; import { MUSIC, SOUNDS } from "../platform/sound";
import { T } from "../translations"; import { T } from "../translations";
@ -10,14 +10,24 @@ import { Loader } from "./loader";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { Signal } from "./signal"; import { Signal } from "./signal";
import { clamp, getLogoSprite, timeoutPromise } from "./utils"; import { clamp, getLogoSprite, timeoutPromise } from "./utils";
const logger = createLogger("background_loader"); 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()], sprites: [getLogoSprite()],
sounds: [SOUNDS.uiClick, SOUNDS.uiError, SOUNDS.dialogError, SOUNDS.dialogOk], sounds: [SOUNDS.uiClick, SOUNDS.uiError, SOUNDS.dialogError, SOUNDS.dialogOk],
atlas: [], atlas: [],
css: [], css: [],
}; };
const INGAME_ASSETS = {
const INGAME_ASSETS: Assets = {
sprites: [], sprites: [],
sounds: [ sounds: [
...Array.from(Object.values(MUSIC)), ...Array.from(Object.values(MUSIC)),
@ -26,32 +36,36 @@ const INGAME_ASSETS = {
atlas: atlasFiles, atlas: atlasFiles,
css: ["async-resources.css"], css: ["async-resources.css"],
}; };
if (G_IS_STANDALONE) { if (G_IS_STANDALONE) {
MAIN_MENU_ASSETS.sounds = [...Array.from(Object.values(MUSIC)), ...Array.from(Object.values(SOUNDS))]; MAIN_MENU_ASSETS.sounds = [...Array.from(Object.values(MUSIC)), ...Array.from(Object.values(SOUNDS))];
INGAME_ASSETS.sounds = []; INGAME_ASSETS.sounds = [];
} }
const LOADER_TIMEOUT_PER_RESOURCE = 180000; const LOADER_TIMEOUT_PER_RESOURCE = 180000;
// Cloudflare does not send content-length headers with brotli compression, // Cloudflare does not send content-length headers with brotli compression,
// so store the actual (compressed) file sizes so we can show a progress bar. // so store the actual (compressed) file sizes so we can show a progress bar.
const HARDCODED_FILE_SIZES = { const HARDCODED_FILE_SIZES = {
"async-resources.css": 2216145, "async-resources.css": 2216145,
}; };
export class BackgroundResourcesLoader { export class BackgroundResourcesLoader {
public app = app;
public mainMenuPromise = null; public mainMenuPromise = null;
public ingamePromise = null; public ingamePromise = null;
public resourceStateChangedSignal = new Signal(); public resourceStateChangedSignal = new Signal<[{ progress: number }]>();
constructor(app) { constructor(public app) {}
}
getMainMenuPromise() { getMainMenuPromise(): Promise<void> {
if (this.mainMenuPromise) { if (this.mainMenuPromise) {
return this.mainMenuPromise; return this.mainMenuPromise;
} }
logger.log("⏰ Loading main menu assets"); logger.log("⏰ Loading main menu assets");
return (this.mainMenuPromise = this.loadAssets(MAIN_MENU_ASSETS)); return (this.mainMenuPromise = this.loadAssets(MAIN_MENU_ASSETS));
} }
getIngamePromise() {
getIngamePromise(): Promise<void> {
if (this.ingamePromise) { if (this.ingamePromise) {
return this.ingamePromise; return this.ingamePromise;
} }
@ -59,46 +73,77 @@ export class BackgroundResourcesLoader {
const promise = this.loadAssets(INGAME_ASSETS).then(() => initSpriteCache()); const promise = this.loadAssets(INGAME_ASSETS).then(() => initSpriteCache());
return (this.ingamePromise = promise); return (this.ingamePromise = promise);
} }
async loadAssets({ sprites, sounds, atlas, css }: {
async loadAssets({
sprites,
sounds,
atlas,
css,
}: {
sprites: string[]; sprites: string[];
sounds: string[]; sounds: string[];
atlas: AtlasDefinition[]; atlas: AtlasDefinition[];
css: string[]; css: string[];
}) { }) {
let promiseFunctions: ((progressHandler: (progress: number) => void) => Promise<void>)[] = []; let promiseFunctions: ((progressHandler: (progress: number) => void) => Promise<void>)[] = [];
// CSS // CSS
for (let i = 0; i < css.length; ++i) { for (let i = 0; i < css.length; ++i) {
promiseFunctions.push(progress => timeoutPromise(this.internalPreloadCss(css[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch(err => { promiseFunctions.push(progress =>
logger.error("Failed to load css:", css[i], err); timeoutPromise(this.internalPreloadCss(css[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch(
throw new Error("HUD Stylesheet " + css[i] + " failed to load: " + err); err => {
})); logger.error("Failed to load css:", css[i], err);
throw new Error("HUD Stylesheet " + css[i] + " failed to load: " + err);
}
)
);
} }
// ATLAS FILES // ATLAS FILES
for (let i = 0; i < atlas.length; ++i) { for (let i = 0; i < atlas.length; ++i) {
promiseFunctions.push(progress => timeoutPromise(Loader.preloadAtlas(atlas[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch(err => { promiseFunctions.push(progress =>
logger.error("Failed to load atlas:", atlas[i].sourceFileName, err); timeoutPromise(Loader.preloadAtlas(atlas[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch(
throw new Error("Atlas " + atlas[i].sourceFileName + " failed to load: " + err); err => {
})); logger.error("Failed to load atlas:", atlas[i].sourceFileName, err);
throw new Error("Atlas " + atlas[i].sourceFileName + " failed to load: " + err);
}
)
);
} }
// HUD Sprites // HUD Sprites
for (let i = 0; i < sprites.length; ++i) { for (let i = 0; i < sprites.length; ++i) {
promiseFunctions.push(progress => timeoutPromise(Loader.preloadCSSSprite(sprites[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch(err => { promiseFunctions.push(progress =>
logger.error("Failed to load css sprite:", sprites[i], err); timeoutPromise(
throw new Error("HUD Sprite " + sprites[i] + " failed to load: " + err); 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 // SFX & Music
for (let i = 0; i < sounds.length; ++i) { for (let i = 0; i < sounds.length; ++i) {
promiseFunctions.push(progress => timeoutPromise(this.app.sound.loadSound(sounds[i]), LOADER_TIMEOUT_PER_RESOURCE).catch(err => { promiseFunctions.push(progress =>
logger.warn("Failed to load sound, will not be available:", sounds[i], err); 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 originalAmount = promiseFunctions.length;
const start = performance.now(); const start = performance.now();
logger.log("⏰ Preloading", originalAmount, "assets"); logger.log("⏰ Preloading", originalAmount, "assets");
let progress = 0; let progress = 0;
this.resourceStateChangedSignal.dispatch({ progress }); this.resourceStateChangedSignal.dispatch({ progress });
let promises = []; let promises = [];
for (let i = 0; i < promiseFunctions.length; i++) { for (let i = 0; i < promiseFunctions.length; i++) {
let lastIndividualProgress = 0; let lastIndividualProgress = 0;
const progressHandler = individualProgress => { const progressHandler = individualProgress => {
@ -107,49 +152,64 @@ export class BackgroundResourcesLoader {
progress += delta / originalAmount; progress += delta / originalAmount;
this.resourceStateChangedSignal.dispatch({ progress }); this.resourceStateChangedSignal.dispatch({ progress });
}; };
promises.push(promiseFunctions[i](progressHandler).then(() => { promises.push(
progressHandler(1); promiseFunctions[i](progressHandler).then(() => {
})); progressHandler(1);
})
);
} }
await Promise.all(promises); await Promise.all(promises);
logger.log("⏰ Preloaded assets in", Math.round(performance.now() - start), "ms"); 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 * 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) { if (G_IS_STANDALONE) {
dialogs 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()); .retry.add(() => window.location.reload());
} } else {
else {
dialogs 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>`) + .showWarning(
"<br>" + T.dialogs.resourceLoadFailed.title,
err, ["retry"]) 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()); .retry.add(() => window.location.reload());
} }
} }
preloadWithProgress(src, progressHandler) {
preloadWithProgress(src: string, progressHandler: (percent: number) => void): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
let notifiedNotComputable = false; let notifiedNotComputable = false;
const fullUrl = cachebust(src); const fullUrl = cachebust(src);
xhr.open("GET", fullUrl, true); xhr.open("GET", fullUrl, true);
xhr.responseType = "arraybuffer"; xhr.responseType = "arraybuffer";
xhr.onprogress = function (ev) { xhr.onprogress = function (ev) {
if (ev.lengthComputable) { if (ev.lengthComputable) {
progressHandler(ev.loaded / ev.total); progressHandler(ev.loaded / ev.total);
} } else {
else {
if (window.location.search.includes("alwaysLogFileSize")) { if (window.location.search.includes("alwaysLogFileSize")) {
console.warn("Progress:", src, ev.loaded); console.warn("Progress:", src, ev.loaded);
} }
if (HARDCODED_FILE_SIZES[src]) { if (HARDCODED_FILE_SIZES[src]) {
progressHandler(clamp(ev.loaded / HARDCODED_FILE_SIZES[src])); progressHandler(clamp(ev.loaded / HARDCODED_FILE_SIZES[src]));
} } else {
else {
if (!notifiedNotComputable) { if (!notifiedNotComputable) {
notifiedNotComputable = true; notifiedNotComputable = true;
console.warn("Progress not computable:", src, ev.loaded); console.warn("Progress not computable:", src, ev.loaded);
@ -158,14 +218,15 @@ export class BackgroundResourcesLoader {
} }
} }
}; };
xhr.onloadend = function () { xhr.onloadend = function () {
if (!xhr.status.toString().match(/^2/)) { if (!xhr.status.toString().match(/^2/)) {
reject(fullUrl + ": " + xhr.status + " " + xhr.statusText); reject(fullUrl + ": " + xhr.status + " " + xhr.statusText);
} } else {
else {
if (!notifiedNotComputable) { if (!notifiedNotComputable) {
progressHandler(1); progressHandler(1);
} }
const options = {}; const options = {};
const headers = xhr.getAllResponseHeaders(); const headers = xhr.getAllResponseHeaders();
const contentType = headers.match(/^Content-Type:\s*(.*?)$/im); const contentType = headers.match(/^Content-Type:\s*(.*?)$/im);
@ -179,7 +240,8 @@ export class BackgroundResourcesLoader {
xhr.send(); xhr.send();
}); });
} }
internalPreloadCss(src, progressHandler) {
internalPreloadCss(src: string, progressHandler: (percent: number) => void) {
return this.preloadWithProgress(src, progressHandler).then(blobSrc => { return this.preloadWithProgress(src, progressHandler).then(blobSrc => {
var styleElement = document.createElement("link"); var styleElement = document.createElement("link");
styleElement.href = blobSrc; styleElement.href = blobSrc;

View File

@ -2,6 +2,7 @@ import { GameRoot } from "../game/root";
import { clearBufferBacklog, freeCanvas, getBufferStats, makeOffscreenBuffer } from "./buffer_utils"; import { clearBufferBacklog, freeCanvas, getBufferStats, makeOffscreenBuffer } from "./buffer_utils";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { round1Digit } from "./utils"; import { round1Digit } from "./utils";
export type CacheEntry = { export type CacheEntry = {
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D; context: CanvasRenderingContext2D;
@ -9,16 +10,18 @@ export type CacheEntry = {
}; };
const logger = createLogger("buffers"); const logger = createLogger("buffers");
const bufferGcDurationSeconds = 0.5; const bufferGcDurationSeconds = 0.5;
export class BufferMaintainer { export class BufferMaintainer {
public root = root;
public cache: Map<string, Map<string, CacheEntry>> = new Map(); public cache: Map<string, Map<string, CacheEntry>> = new Map();
public iterationIndex = 1; public iterationIndex = 1;
public lastIteration = 0; public lastIteration = 0;
constructor(root) { constructor(public root) {
this.root.signals.gameFrameStarted.add(this.update, this); this.root.signals.gameFrameStarted.add(this.update, this);
} }
/** /**
* Returns the buffer stats * Returns the buffer stats
*/ */
@ -30,14 +33,18 @@ export class BufferMaintainer {
}; };
this.cache.forEach((subCache, key) => { this.cache.forEach((subCache, key) => {
++stats.rootKeys; ++stats.rootKeys;
subCache.forEach((cacheEntry, subKey) => { subCache.forEach((cacheEntry, subKey) => {
++stats.subKeys; ++stats.subKeys;
const canvas = cacheEntry.canvas; const canvas = cacheEntry.canvas;
stats.vramBytes += canvas.width * canvas.height * 4; stats.vramBytes += canvas.width * canvas.height * 4;
}); });
}); });
return stats; return stats;
} }
/** /**
* Goes to the next buffer iteration, clearing all buffers which were not used * Goes to the next buffer iteration, clearing all buffers which were not used
* for a few iterations * for a few iterations
@ -46,28 +53,34 @@ export class BufferMaintainer {
let totalKeys = 0; let totalKeys = 0;
let deletedKeys = 0; let deletedKeys = 0;
const minIteration = this.iterationIndex; const minIteration = this.iterationIndex;
this.cache.forEach((subCache, key) => { this.cache.forEach((subCache, key) => {
let unusedSubKeys = []; let unusedSubKeys = [];
// Filter sub cache // Filter sub cache
subCache.forEach((cacheEntry, subKey) => { subCache.forEach((cacheEntry, subKey) => {
if (cacheEntry.lastUse < minIteration || if (
cacheEntry.lastUse < minIteration ||
// @ts-ignore // @ts-ignore
cacheEntry.canvas._contextLost) { cacheEntry.canvas._contextLost
) {
unusedSubKeys.push(subKey); unusedSubKeys.push(subKey);
freeCanvas(cacheEntry.canvas); freeCanvas(cacheEntry.canvas);
++deletedKeys; ++deletedKeys;
} } else {
else {
++totalKeys; ++totalKeys;
} }
}); });
// Delete unused sub keys // Delete unused sub keys
for (let i = 0; i < unusedSubKeys.length; ++i) { for (let i = 0; i < unusedSubKeys.length; ++i) {
subCache.delete(unusedSubKeys[i]); subCache.delete(unusedSubKeys[i]);
} }
}); });
// Make sure our backlog never gets too big // Make sure our backlog never gets too big
clearBufferBacklog(); clearBufferBacklog();
// if (G_IS_DEV) { // if (G_IS_DEV) {
// const bufferStats = getBufferStats(); // const bufferStats = getBufferStats();
// const mbUsed = round1Digit(bufferStats.vramUsage / (1024 * 1024)); // const mbUsed = round1Digit(bufferStats.vramUsage / (1024 * 1024));
@ -89,8 +102,10 @@ export class BufferMaintainer {
// "MB" // "MB"
// ); // );
// } // }
++this.iterationIndex; ++this.iterationIndex;
} }
update() { update() {
const now = this.root.time.realtimeNow(); const now = this.root.time.realtimeNow();
if (now - this.lastIteration > bufferGcDurationSeconds) { if (now - this.lastIteration > bufferGcDurationSeconds) {
@ -98,18 +113,30 @@ export class BufferMaintainer {
this.garbargeCollect(); this.garbargeCollect();
} }
} }
/**
* {} getForKey({
* key,
*/ subKey,
getForKey({ key, subKey, w, h, dpi, redrawMethod, additionalParams }: { w,
h,
dpi,
redrawMethod,
additionalParams = {},
}: {
key: string; key: string;
subKey: string; subKey: string;
w: number; w: number;
h: number; h: number;
dpi: number; dpi: number;
redrawMethod: function(: void, : void, : void, : void, : void, : void):void; redrawMethod: (
additionalParams: object=; canvas: HTMLCanvasElement,
context: CanvasRenderingContext2D,
w: number,
h: number,
dpi: number,
addParams?: object
) => void;
additionalParams: object;
}): HTMLCanvasElement { }): HTMLCanvasElement {
// First, create parent key // First, create parent key
let parent = this.cache.get(key); let parent = this.cache.get(key);
@ -117,21 +144,26 @@ export class BufferMaintainer {
parent = new Map(); parent = new Map();
this.cache.set(key, parent); this.cache.set(key, parent);
} }
// Now search for sub key // Now search for sub key
const cacheHit = parent.get(subKey); const cacheHit = parent.get(subKey);
if (cacheHit) { if (cacheHit) {
cacheHit.lastUse = this.iterationIndex; cacheHit.lastUse = this.iterationIndex;
return cacheHit.canvas; return cacheHit.canvas;
} }
// Need to generate new buffer // Need to generate new buffer
const effectiveWidth = w * dpi; const effectiveWidth = w * dpi;
const effectiveHeight = h * dpi; const effectiveHeight = h * dpi;
const [canvas, context] = makeOffscreenBuffer(effectiveWidth, effectiveHeight, { const [canvas, context] = makeOffscreenBuffer(effectiveWidth, effectiveHeight, {
reusable: true, reusable: true,
label: "buffer-" + key + "/" + subKey, label: "buffer-" + key + "/" + subKey,
smooth: true, smooth: true,
}); });
redrawMethod(canvas, context, w, h, dpi, additionalParams); redrawMethod(canvas, context, w, h, dpi, additionalParams);
parent.set(subKey, { parent.set(subKey, {
canvas, canvas,
context, context,
@ -139,14 +171,8 @@ export class BufferMaintainer {
}); });
return canvas; return canvas;
} }
/**
* {} getForKeyOrNullNoUpdate({ key, subKey }: { key: string; subKey: string }): HTMLCanvasElement | null {
*
*/
getForKeyOrNullNoUpdate({ key, subKey }: {
key: string;
subKey: string;
}): ?HTMLCanvasElement {
let parent = this.cache.get(key); let parent = this.cache.get(key);
if (!parent) { if (!parent) {
return null; return null;

View File

@ -1,16 +1,20 @@
import { globalConfig } from "./config"; import { globalConfig } from "./config";
import { fastArrayDelete } from "./utils"; import { fastArrayDelete } from "./utils";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
const logger = createLogger("buffer_utils"); const logger = createLogger("buffer_utils");
/** /**
* Enables images smoothing on a context * Enables images smoothing on a context
*/ */
export function enableImageSmoothing(context: CanvasRenderingContext2D) { export function enableImageSmoothing(context: CanvasRenderingContext2D) {
context.imageSmoothingEnabled = true; context.imageSmoothingEnabled = true;
context.webkitImageSmoothingEnabled = true; context.webkitImageSmoothingEnabled = true;
// @ts-ignore // @ts-ignore
context.imageSmoothingQuality = globalConfig.smoothing.quality; context.imageSmoothingQuality = globalConfig.smoothing.quality;
} }
/** /**
* Disables image smoothing on a context * Disables image smoothing on a context
*/ */
@ -18,16 +22,19 @@ export function disableImageSmoothing(context: CanvasRenderingContext2D) {
context.imageSmoothingEnabled = false; context.imageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false; context.webkitImageSmoothingEnabled = false;
} }
export type CanvasCacheEntry = { export type CanvasCacheEntry = {
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D; context: CanvasRenderingContext2D;
}; };
const registeredCanvas: Array<CanvasCacheEntry> = []; const registeredCanvas: Array<CanvasCacheEntry> = [];
/** /**
* Buckets for each width * height combination * Buckets for each width * height combination
*/ */
const freeCanvasBuckets: Map<number, Array<CanvasCacheEntry>> = new Map(); const freeCanvasBuckets: Map<number, Array<CanvasCacheEntry>> = new Map();
/** /**
* Track statistics * Track statistics
*/ */
@ -38,12 +45,14 @@ const stats = {
numReused: 0, numReused: 0,
numCreated: 0, numCreated: 0,
}; };
export function getBufferVramUsageBytes(canvas: HTMLCanvasElement) { export function getBufferVramUsageBytes(canvas: HTMLCanvasElement) {
assert(canvas, "no canvas given"); assert(canvas, "no canvas given");
assert(Number.isFinite(canvas.width), "bad canvas width: " + canvas.width); assert(Number.isFinite(canvas.width), "bad canvas width: " + canvas.width);
assert(Number.isFinite(canvas.height), "bad canvas height" + canvas.height); assert(Number.isFinite(canvas.height), "bad canvas height" + canvas.height);
return canvas.width * canvas.height * 4; return canvas.width * canvas.height * 4;
} }
/** /**
* Returns stats on the allocated buffers * Returns stats on the allocated buffers
*/ */
@ -52,12 +61,14 @@ export function getBufferStats() {
freeCanvasBuckets.forEach(bucket => { freeCanvasBuckets.forEach(bucket => {
numBuffersFree += bucket.length; numBuffersFree += bucket.length;
}); });
return { return {
...stats, ...stats,
backlogKeys: freeCanvasBuckets.size, backlogKeys: freeCanvasBuckets.size,
backlogSize: numBuffersFree, backlogSize: numBuffersFree,
}; };
} }
/** /**
* Clears the backlog buffers if they grew too much * Clears the backlog buffers if they grew too much
*/ */
@ -72,14 +83,15 @@ export function clearBufferBacklog() {
} }
}); });
} }
/** /**
* Creates a new offscreen buffer * Creates a new offscreen buffer
* {}
*/ */
export function makeOffscreenBuffer(w: Number, h: Number, { smooth = true, reusable = true, label = "buffer" }): [ export function makeOffscreenBuffer(
HTMLCanvasElement, w: number,
CanvasRenderingContext2D h: number,
] { { smooth = true, reusable = true, label = "buffer" }
): [HTMLCanvasElement, CanvasRenderingContext2D] {
assert(w > 0 && h > 0, "W or H < 0"); assert(w > 0 && h > 0, "W or H < 0");
if (w % 1 !== 0 || h % 1 !== 0) { if (w % 1 !== 0 || h % 1 !== 0) {
// console.warn("Subpixel offscreen buffer size:", w, h); // 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); w = Math.max(1, w);
h = Math.max(1, h); h = Math.max(1, h);
} }
const recommendedSize = 1024 * 1024; const recommendedSize = 1024 * 1024;
if (w * h > recommendedSize) { if (w * h > recommendedSize) {
logger.warn("Creating huge buffer:", w, "x", h, "with label", label); logger.warn("Creating huge buffer:", w, "x", h, "with label", label);
} }
w = Math.floor(w); w = Math.floor(w);
h = Math.floor(h); h = Math.floor(h);
let canvas = null; let canvas = null;
let context = null; let context = null;
// Ok, search in cache first // Ok, search in cache first
const bucket = freeCanvasBuckets.get(w * h) || []; const bucket = freeCanvasBuckets.get(w * h) || [];
for (let i = 0; i < bucket.length; ++i) { for (let i = 0; i < bucket.length; ++i) {
const { canvas: useableCanvas, context: useableContext } = bucket[i]; const { canvas: useableCanvas, context: useableContext } = bucket[i];
if (useableCanvas.width === w && useableCanvas.height === h) { if (useableCanvas.width === w && useableCanvas.height === h) {
// Ok we found one // Ok we found one
canvas = useableCanvas; canvas = useableCanvas;
context = useableContext; context = useableContext;
// Restore past state // Restore past state
context.restore(); context.restore();
context.save(); context.save();
context.clearRect(0, 0, canvas.width, canvas.height); context.clearRect(0, 0, canvas.width, canvas.height);
delete canvas.style.width; delete canvas.style.width;
delete canvas.style.height; delete canvas.style.height;
stats.numReused++; stats.numReused++;
stats.backlogVramUsage -= getBufferVramUsageBytes(canvas); stats.backlogVramUsage -= getBufferVramUsageBytes(canvas);
fastArrayDelete(bucket, i); fastArrayDelete(bucket, i);
break; break;
} }
} }
// None found , create new one // None found , create new one
if (!canvas) { if (!canvas) {
canvas = document.createElement("canvas"); canvas = document.createElement("canvas");
context = canvas.getContext("2d" /*, { alpha } */); context = canvas.getContext("2d" /*, { alpha } */);
stats.numCreated++; stats.numCreated++;
canvas.width = w; canvas.width = w;
canvas.height = h; canvas.height = h;
// Initial state // Initial state
context.save(); context.save();
canvas.addEventListener("webglcontextlost", () => { canvas.addEventListener("webglcontextlost", () => {
console.warn("canvas::webglcontextlost", canvas); console.warn("canvas::webglcontextlost", canvas);
// @ts-ignore // @ts-ignore
@ -139,35 +164,42 @@ export function makeOffscreenBuffer(w: Number, h: Number, { smooth = true, reusa
} }
// @ts-ignore // @ts-ignore
canvas._contextLost = false; canvas._contextLost = false;
// @ts-ignore // @ts-ignore
canvas.label = label; canvas.label = label;
if (smooth) { if (smooth) {
enableImageSmoothing(context); enableImageSmoothing(context);
} } else {
else {
disableImageSmoothing(context); disableImageSmoothing(context);
} }
if (reusable) { if (reusable) {
registerCanvas(canvas, context); registerCanvas(canvas, context);
} }
return [canvas, context]; return [canvas, context];
} }
/** /**
* Frees a canvas * Frees a canvas
*/ */
export function registerCanvas(canvas: HTMLCanvasElement, context) { export function registerCanvas(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D) {
registeredCanvas.push({ canvas, context }); registeredCanvas.push({ canvas, context });
stats.bufferCount += 1; stats.bufferCount += 1;
const bytesUsed = getBufferVramUsageBytes(canvas); const bytesUsed = getBufferVramUsageBytes(canvas);
stats.vramUsage += bytesUsed; stats.vramUsage += bytesUsed;
} }
/** /**
* Frees a canvas * Frees a canvas
*/ */
export function freeCanvas(canvas: HTMLCanvasElement) { export function freeCanvas(canvas: HTMLCanvasElement) {
assert(canvas, "Canvas is empty"); assert(canvas, "Canvas is empty");
let index = -1; let index = -1;
let data = null; let data = null;
for (let i = 0; i < registeredCanvas.length; ++i) { for (let i = 0; i < registeredCanvas.length; ++i) {
if (registeredCanvas[i].canvas === canvas) { if (registeredCanvas[i].canvas === canvas) {
index = i; index = i;
@ -175,20 +207,23 @@ export function freeCanvas(canvas: HTMLCanvasElement) {
break; break;
} }
} }
if (index < 0) { if (index < 0) {
logger.error("Tried to free unregistered canvas of size", canvas.width, canvas.height); logger.error("Tried to free unregistered canvas of size", canvas.width, canvas.height);
return; return;
} }
fastArrayDelete(registeredCanvas, index); fastArrayDelete(registeredCanvas, index);
const key = canvas.width * canvas.height; const key = canvas.width * canvas.height;
const bucket = freeCanvasBuckets.get(key); const bucket = freeCanvasBuckets.get(key);
if (bucket) { if (bucket) {
bucket.push(data); bucket.push(data);
} } else {
else {
freeCanvasBuckets.set(key, [data]); freeCanvasBuckets.set(key, [data]);
} }
stats.bufferCount -= 1; stats.bufferCount -= 1;
const bytesUsed = getBufferVramUsageBytes(canvas); const bytesUsed = getBufferVramUsageBytes(canvas);
stats.vramUsage -= bytesUsed; stats.vramUsage -= bytesUsed;
stats.backlogVramUsage += bytesUsed; stats.backlogVramUsage += bytesUsed;

View File

@ -5,19 +5,26 @@ import { Vector } from "./vector";
import { IS_MOBILE, SUPPORT_TOUCH } from "./config"; import { IS_MOBILE, SUPPORT_TOUCH } from "./config";
import { SOUNDS } from "../platform/sound"; import { SOUNDS } from "../platform/sound";
import { GLOBAL_APP } from "./globals"; import { GLOBAL_APP } from "./globals";
const logger = createLogger("click_detector"); const logger = createLogger("click_detector");
export const MAX_MOVE_DISTANCE_PX = IS_MOBILE ? 20 : 80; export const MAX_MOVE_DISTANCE_PX = IS_MOBILE ? 20 : 80;
// For debugging // For debugging
const registerClickDetectors = G_IS_DEV && true; const registerClickDetectors = G_IS_DEV && true;
if (registerClickDetectors) { if (registerClickDetectors) {
window.activeClickDetectors = []; window.activeClickDetectors = [];
} }
// Store active click detectors so we can cancel them // Store active click detectors so we can cancel them
const ongoingClickDetectors: Array<ClickDetector> = []; const ongoingClickDetectors: Array<ClickDetector> = [];
// Store when the last touch event was registered, to avoid accepting a touch *and* a click event // Store when the last touch event was registered, to avoid accepting a touch *and* a click event
export let clickDetectorGlobals = { export let clickDetectorGlobals = {
lastTouchTime: -1000, lastTouchTime: -1000,
}; };
export type ClickDetectorConstructorArgs = { export type ClickDetectorConstructorArgs = {
consumeEvents?: boolean; consumeEvents?: boolean;
preventDefault?: boolean; preventDefault?: boolean;
@ -32,30 +39,67 @@ export type ClickDetectorConstructorArgs = {
// Detects clicks // Detects clicks
export class ClickDetector { export class ClickDetector {
public clickDownPosition = null; public clickDownPosition = null;
public consumeEvents = consumeEvents; public consumeEvents: boolean;
public preventDefault = preventDefault; public preventDefault: boolean;
public applyCssClass = applyCssClass; public applyCssClass: string;
public captureTouchmove = captureTouchmove; public captureTouchmove: boolean;
public targetOnly = targetOnly; public targetOnly: boolean;
public clickSound = clickSound; public clickSound: string;
public maxDistance = maxDistance; public maxDistance: number;
public preventClick = preventClick; public preventClick: boolean;
public click = new Signal();
public rightClick = new Signal(); // Bound Methods
public touchstart = new Signal(); public handlerTouchStart = this.internalOnPointerDown.bind(this);
public touchmove = new Signal(); public handlerTouchEnd = this.internalOnPointerEnd.bind(this);
public touchend = new Signal(); public handlerTouchMove = this.internalOnPointerMove.bind(this);
public touchcancel = new Signal(); public handlerTouchCancel = this.internalOnTouchCancel.bind(this);
public touchstartSimple = new Signal(); public handlerPreventClick = this.internalPreventClick.bind(this);
public touchmoveSimple = new Signal();
public touchendSimple = new Signal(); // Signals
public clickStartTime = null; 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; 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!"); 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 * Cleans up all event listeners of this detector
*/ */
@ -65,20 +109,22 @@ export class ClickDetector {
const index = window.activeClickDetectors.indexOf(this); const index = window.activeClickDetectors.indexOf(this);
if (index < 0) { if (index < 0) {
logger.error("Click detector cleanup but is not active"); logger.error("Click detector cleanup but is not active");
} } else {
else {
window.activeClickDetectors.splice(index, 1); window.activeClickDetectors.splice(index, 1);
} }
} }
const options = this.internalGetEventListenerOptions(); const options = this.internalGetEventListenerOptions();
if (SUPPORT_TOUCH) { if (SUPPORT_TOUCH) {
this.element.removeEventListener("touchstart", this.handlerTouchStart, options); this.element.removeEventListener("touchstart", this.handlerTouchStart, options);
this.element.removeEventListener("touchend", this.handlerTouchEnd, options); this.element.removeEventListener("touchend", this.handlerTouchEnd, options);
this.element.removeEventListener("touchcancel", this.handlerTouchCancel, options); this.element.removeEventListener("touchcancel", this.handlerTouchCancel, options);
} }
this.element.removeEventListener("mouseup", this.handlerTouchStart, options); this.element.removeEventListener("mouseup", this.handlerTouchStart, options);
this.element.removeEventListener("mousedown", this.handlerTouchEnd, options); this.element.removeEventListener("mousedown", this.handlerTouchEnd, options);
this.element.removeEventListener("mouseout", this.handlerTouchCancel, options); this.element.removeEventListener("mouseout", this.handlerTouchCancel, options);
if (this.captureTouchmove) { if (this.captureTouchmove) {
if (SUPPORT_TOUCH) { if (SUPPORT_TOUCH) {
this.element.removeEventListener("touchmove", this.handlerTouchMove, options); this.element.removeEventListener("touchmove", this.handlerTouchMove, options);
@ -88,19 +134,23 @@ export class ClickDetector {
if (this.preventClick) { if (this.preventClick) {
this.element.removeEventListener("click", this.handlerPreventClick, options); this.element.removeEventListener("click", this.handlerPreventClick, options);
} }
this.click.removeAll(); this.click.removeAll();
this.touchstart.removeAll(); this.touchstart.removeAll();
this.touchmove.removeAll(); this.touchmove.removeAll();
this.touchend.removeAll(); this.touchend.removeAll();
this.touchcancel.removeAll(); this.touchcancel.removeAll();
this.element = null; this.element = null;
} }
} }
// INTERNAL METHODS // INTERNAL METHODS
internalPreventClick(event: Event) {
internalPreventClick(event: Event) {
window.focus(); window.focus();
event.preventDefault(); event.preventDefault();
} }
/** /**
* Internal method to get the options to pass to an event listener * Internal method to get the options to pass to an event listener
*/ */
@ -110,17 +160,14 @@ export class ClickDetector {
passive: !this.preventDefault, passive: !this.preventDefault,
}; };
} }
/** /**
* Binds the click detector to an element * Binds the click detector to an element
*/ */
internalBindTo(element: HTMLElement) { internalBindTo(element: HTMLElement) {
const options = this.internalGetEventListenerOptions(); 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) { if (this.preventClick) {
this.handlerPreventClick = this.internalPreventClick.bind(this);
element.addEventListener("click", this.handlerPreventClick, options); element.addEventListener("click", this.handlerPreventClick, options);
} }
if (SUPPORT_TOUCH) { if (SUPPORT_TOUCH) {
@ -142,12 +189,14 @@ export class ClickDetector {
} }
this.element = element; this.element = element;
} }
/** /**
* Returns if the bound element is currently in the DOM. * Returns if the bound element is currently in the DOM.
*/ */
internalIsDomElementAttached() { internalIsDomElementAttached() {
return this.element && document.documentElement.contains(this.element); return this.element && document.documentElement.contains(this.element);
} }
/** /**
* Checks if the given event is relevant for this detector * Checks if the given event is relevant for this detector
*/ */
@ -156,52 +205,70 @@ export class ClickDetector {
// Already cleaned up // Already cleaned up
return false; return false;
} }
if (this.targetOnly && event.target !== this.element) { if (this.targetOnly && event.target !== this.element) {
// Clicked a child element // Clicked a child element
return false; return false;
} }
// Stop any propagation and defaults if configured // Stop any propagation and defaults if configured
if (this.consumeEvents && event.cancelable) { if (this.consumeEvents && event.cancelable) {
event.stopPropagation(); event.stopPropagation();
} }
if (this.preventDefault && event.cancelable) { if (this.preventDefault && event.cancelable) {
event.preventDefault(); event.preventDefault();
} }
if (window.TouchEvent && event instanceof TouchEvent) { if (window.TouchEvent && event instanceof TouchEvent) {
clickDetectorGlobals.lastTouchTime = performance.now(); clickDetectorGlobals.lastTouchTime = performance.now();
// console.log("Got touches", event.targetTouches.length, "vs", expectedRemainingTouches); // console.log("Got touches", event.targetTouches.length, "vs", expectedRemainingTouches);
if (event.targetTouches.length !== expectedRemainingTouches) { if (event.targetTouches.length !== expectedRemainingTouches) {
return false; return false;
} }
} }
if (event instanceof MouseEvent) { if (event instanceof MouseEvent) {
if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) { if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) {
return false; return false;
} }
} }
return true; return true;
} }
/** /**
* Extracts the mous position from an event * Extracts the mouse position from an event
* {} The client space position * @param event The client space position
*/ */
static extractPointerPosition(event: TouchEvent | MouseEvent): Vector { static extractPointerPosition(event: TouchEvent | MouseEvent): Vector {
if (window.TouchEvent && event instanceof TouchEvent) { if (window.TouchEvent && event instanceof TouchEvent) {
if (event.changedTouches.length !== 1) { 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); return new Vector(0, 0);
} }
const touch = event.changedTouches[0]; const touch = event.changedTouches[0];
return new Vector(touch.clientX, touch.clientY); return new Vector(touch.clientX, touch.clientY);
} }
if (event instanceof MouseEvent) { if (event instanceof MouseEvent) {
return new Vector(event.clientX, event.clientY); return new Vector(event.clientX, event.clientY);
} }
assertAlways(false, "Got unknown event: " + event); assertAlways(false, "Got unknown event: " + event);
return new Vector(0, 0); return new Vector(0, 0);
} }
/** /**
* Cacnels all ongoing events on this detector * Cancels all ongoing events on this detector
*/ */
cancelOngoingEvents() { cancelOngoingEvents() {
if (this.applyCssClass && this.element) { if (this.applyCssClass && this.element) {
@ -212,16 +279,19 @@ export class ClickDetector {
this.cancelled = true; this.cancelled = true;
fastArrayDeleteValueIfContained(ongoingClickDetectors, this); fastArrayDeleteValueIfContained(ongoingClickDetectors, this);
} }
/** /**
* Internal pointer down handler * Internal pointer down handler
*/ */
internalOnPointerDown(event: TouchEvent | MouseEvent) { internalOnPointerDown(event: TouchEvent | MouseEvent) {
window.focus(); window.focus();
if (!this.internalEventPreHandler(event, 1)) { if (!this.internalEventPreHandler(event, 1)) {
return false; return false;
} }
const position = this.constructor as typeof ClickDetector).extractPointerPosition(event); const position = (this.constructor as typeof ClickDetector).extractPointerPosition(event);
if (event instanceof MouseEvent) { if (event instanceof MouseEvent) {
const isRightClick = event.button === 2; const isRightClick = event.button === 2;
if (isRightClick) { if (isRightClick) {
@ -232,33 +302,40 @@ export class ClickDetector {
return; return;
} }
} }
if (this.clickDownPosition) { if (this.clickDownPosition) {
logger.warn("Ignoring double click"); logger.warn("Ignoring double click");
return false; return false;
} }
this.cancelled = false; this.cancelled = false;
this.touchstart.dispatch(event); this.touchstart.dispatch(event);
// Store where the touch started // Store where the touch started
this.clickDownPosition = position; this.clickDownPosition = position;
this.clickStartTime = performance.now(); this.clickStartTime = performance.now();
this.touchstartSimple.dispatch(this.clickDownPosition.x, this.clickDownPosition.y); this.touchstartSimple.dispatch(this.clickDownPosition.x, this.clickDownPosition.y);
// If we are not currently within a click, register it // If we are not currently within a click, register it
if (ongoingClickDetectors.indexOf(this) < 0) { if (ongoingClickDetectors.indexOf(this) < 0) {
ongoingClickDetectors.push(this); ongoingClickDetectors.push(this);
} } else {
else {
logger.warn("Click detector got pointer down of active pointer twice"); logger.warn("Click detector got pointer down of active pointer twice");
} }
// If we should apply any classes, do this now // If we should apply any classes, do this now
if (this.applyCssClass) { if (this.applyCssClass) {
this.element.classList.add(this.applyCssClass); this.element.classList.add(this.applyCssClass);
} }
// If we should play any sound, do this // If we should play any sound, do this
if (this.clickSound) { if (this.clickSound) {
GLOBAL_APP.sound.playUiSound(this.clickSound); GLOBAL_APP.sound.playUiSound(this.clickSound);
} }
return false; return false;
} }
/** /**
* Internal pointer move handler * Internal pointer move handler
*/ */
@ -267,61 +344,68 @@ export class ClickDetector {
return false; return false;
} }
this.touchmove.dispatch(event); 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); this.touchmoveSimple.dispatch(pos.x, pos.y);
return false; return false;
} }
/** /**
* Internal pointer end handler * Internal pointer end handler
*/ */
internalOnPointerEnd(event: TouchEvent | MouseEvent) { internalOnPointerEnd(event: TouchEvent | MouseEvent) {
window.focus(); window.focus();
if (!this.internalEventPreHandler(event, 0)) { if (!this.internalEventPreHandler(event, 0)) {
return false; return false;
} }
if (this.cancelled) { if (this.cancelled) {
// warn(this, "Not dispatching touchend on cancelled listener"); // warn(this, "Not dispatching touchend on cancelled listener");
return false; return false;
} }
if (event instanceof MouseEvent) { if (event instanceof MouseEvent) {
const isRightClick = event.button === 2; const isRightClick = event.button === 2;
if (isRightClick) { if (isRightClick) {
return; return;
} }
} }
const index = ongoingClickDetectors.indexOf(this); const index = ongoingClickDetectors.indexOf(this);
if (index < 0) { if (index < 0) {
logger.warn("Got pointer end but click detector is not in pressed state"); logger.warn("Got pointer end but click detector is not in pressed state");
} } else {
else {
fastArrayDelete(ongoingClickDetectors, index); fastArrayDelete(ongoingClickDetectors, index);
} }
let dispatchClick = false; let dispatchClick = false;
let dispatchClickPos = null; let dispatchClickPos = null;
// Check for correct down position, otherwise must have pinched or so // Check for correct down position, otherwise must have pinched or so
if (this.clickDownPosition) { 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); const distance = pos.distance(this.clickDownPosition);
if (!IS_MOBILE || distance <= this.maxDistance) { if (!IS_MOBILE || distance <= this.maxDistance) {
dispatchClick = true; dispatchClick = true;
dispatchClickPos = pos; dispatchClickPos = pos;
} } else {
else {
console.warn("[ClickDetector] Touch does not count as click:", "(was", distance, ")"); console.warn("[ClickDetector] Touch does not count as click:", "(was", distance, ")");
} }
} }
this.clickDownPosition = null; this.clickDownPosition = null;
this.clickStartTime = null; this.clickStartTime = null;
if (this.applyCssClass) { if (this.applyCssClass) {
this.element.classList.remove(this.applyCssClass); this.element.classList.remove(this.applyCssClass);
} }
// Dispatch in the end to avoid the element getting invalidated // Dispatch in the end to avoid the element getting invalidated
// Also make sure that the element is still in the dom // Also make sure that the element is still in the dom
if (this.internalIsDomElementAttached()) { if (this.internalIsDomElementAttached()) {
this.touchend.dispatch(event); this.touchend.dispatch(event);
this.touchendSimple.dispatch(); this.touchendSimple.dispatch();
if (dispatchClick) { if (dispatchClick) {
const detectors = ongoingClickDetectors.slice(); const detectors = ongoingClickDetectors.slice();
for (let i = 0; i < detectors.length; ++i) { for (let i = 0; i < detectors.length; ++i) {
@ -332,6 +416,7 @@ export class ClickDetector {
} }
return false; return false;
} }
/** /**
* Internal touch cancel handler * Internal touch cancel handler
*/ */
@ -339,10 +424,12 @@ export class ClickDetector {
if (!this.internalEventPreHandler(event, 0)) { if (!this.internalEventPreHandler(event, 0)) {
return false; return false;
} }
if (this.cancelled) { if (this.cancelled) {
// warn(this, "Not dispatching touchcancel on cancelled listener"); // warn(this, "Not dispatching touchcancel on cancelled listener");
return false; return false;
} }
this.cancelOngoingEvents(); this.cancelOngoingEvents();
this.touchcancel.dispatch(event); this.touchcancel.dispatch(event);
this.touchendSimple.dispatch(event); this.touchendSimple.dispatch(event);

View File

@ -1,126 +1,126 @@
export default { export default {
// You can set any debug options here! // You can set any debug options here!
/* dev:start */ /* dev:start */
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Quickly enters the game and skips the main menu - good for fast iterating // Quickly enters the game and skips the main menu - good for fast iterating
// fastGameEnter: true, // fastGameEnter: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Skips any delays like transitions between states and such // Skips any delays like transitions between states and such
// noArtificialDelays: true, // noArtificialDelays: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Disables writing of savegames, useful for testing the same savegame over and over // Disables writing of savegames, useful for testing the same savegame over and over
// disableSavegameWrite: true, // disableSavegameWrite: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Shows bounds of all entities // Shows bounds of all entities
// showEntityBounds: true, // showEntityBounds: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Shows arrows for every ejector / acceptor // Shows arrows for every ejector / acceptor
// showAcceptorEjectors: true, // showAcceptorEjectors: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Disables the music (Overrides any setting, can cause weird behaviour) // Disables the music (Overrides any setting, can cause weird behaviour)
// disableMusic: true, // disableMusic: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Do not render static map entities (=most buildings) // Do not render static map entities (=most buildings)
// doNotRenderStatics: true, // doNotRenderStatics: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Allow to zoom freely without limits // Allow to zoom freely without limits
// disableZoomLimits: true, // disableZoomLimits: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// All rewards can be unlocked by passing just 1 of any shape // All rewards can be unlocked by passing just 1 of any shape
// rewardsInstant: true, // rewardsInstant: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Unlocks all buildings // Unlocks all buildings
// allBuildingsUnlocked: true, // allBuildingsUnlocked: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Disables cost of blueprints // Disables cost of blueprints
// blueprintsNoCost: true, // blueprintsNoCost: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Disables cost of upgrades // Disables cost of upgrades
// upgradesNoCost: true, // upgradesNoCost: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Disables the dialog when completing a level // Disables the dialog when completing a level
// disableUnlockDialog: true, // disableUnlockDialog: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Disables the simulation - This effectively pauses the game. // Disables the simulation - This effectively pauses the game.
// disableLogicTicks: true, // disableLogicTicks: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Test the rendering if everything is clipped out properly // Test the rendering if everything is clipped out properly
// testClipping: true, // testClipping: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Allows to render slower, useful for recording at half speed to avoid stuttering // Allows to render slower, useful for recording at half speed to avoid stuttering
// framePausesBetweenTicks: 250, // framePausesBetweenTicks: 250,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Replace all translations with emojis to see which texts are translateable // Replace all translations with emojis to see which texts are translateable
// testTranslations: true, // testTranslations: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Enables an inspector which shows information about the entity below the cursor // Enables an inspector which shows information about the entity below the cursor
// enableEntityInspector: true, // enableEntityInspector: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Enables ads in the local build (normally they are deactivated there) // Enables ads in the local build (normally they are deactivated there)
// testAds: true, // testAds: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Allows unlocked achievements to be logged to console in the local build // Allows unlocked achievements to be logged to console in the local build
// testAchievements: true, // testAchievements: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Enables use of (some) existing flags within the puzzle mode context // Enables use of (some) existing flags within the puzzle mode context
// testPuzzleMode: true, // testPuzzleMode: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Disables the automatic switch to an overview when zooming out // Disables the automatic switch to an overview when zooming out
// disableMapOverview: true, // disableMapOverview: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Disables the notification when there are new entries in the changelog since last played // Disables the notification when there are new entries in the changelog since last played
// disableUpgradeNotification: true, // disableUpgradeNotification: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Makes belts almost infinitely fast // Makes belts almost infinitely fast
// instantBelts: true, // instantBelts: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Makes item processors almost infinitely fast // Makes item processors almost infinitely fast
// instantProcessors: true, // instantProcessors: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Makes miners almost infinitely fast // Makes miners almost infinitely fast
// instantMiners: true, // instantMiners: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// When using fastGameEnter, controls whether a new game is started or the last one is resumed // When using fastGameEnter, controls whether a new game is started or the last one is resumed
// resumeGameOnFastEnter: true, // resumeGameOnFastEnter: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Special option used to render the trailer // Special option used to render the trailer
// renderForTrailer: true, // renderForTrailer: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Whether to render changes // Whether to render changes
// renderChanges: true, // renderChanges: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Whether to render belt paths // Whether to render belt paths
// renderBeltPaths: true, // renderBeltPaths: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Whether to check belt paths // Whether to check belt paths
// checkBeltPaths: true, // checkBeltPaths: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Whether to items / s instead of items / m in stats // Whether to items / s instead of items / m in stats
// detailedStatistics: true, // detailedStatistics: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Shows detailed information about which atlas is used // Shows detailed information about which atlas is used
// showAtlasInfo: true, // showAtlasInfo: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Renders the rotation of all wires // Renders the rotation of all wires
// renderWireRotations: true, // renderWireRotations: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Renders information about wire networks // Renders information about wire networks
// renderWireNetworkInfos: true, // renderWireNetworkInfos: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Disables ejector animations and processing // Disables ejector animations and processing
// disableEjectorProcessing: true, // disableEjectorProcessing: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Allows manual ticking // Allows manual ticking
// manualTickOnly: true, // manualTickOnly: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Disables slow asserts, useful for debugging performance // Disables slow asserts, useful for debugging performance
// disableSlowAsserts: true, // disableSlowAsserts: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Allows to load a mod from an external source for developing it // Allows to load a mod from an external source for developing it
// externalModUrl: "http://localhost:3005/combined.js", // externalModUrl: "http://localhost:3005/combined.js",
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Visualizes the shape grouping on belts // Visualizes the shape grouping on belts
// showShapeGrouping: true // showShapeGrouping: true
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
/* dev:end */ /* dev:end */
}; };

View File

@ -1,21 +1,26 @@
/* typehints:start */
import type { Application } from "../application"; import type { Application } from "../application";
/* typehints:end */
export const IS_DEBUG = G_IS_DEV && export const IS_DEBUG =
G_IS_DEV &&
typeof window !== "undefined" && typeof window !== "undefined" &&
window.location.port === "3005" && window.location.port === "3005" &&
(window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) && (window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) &&
window.location.search.indexOf("nodebug") < 0; window.location.search.indexOf("nodebug") < 0;
export const SUPPORT_TOUCH = false; export const SUPPORT_TOUCH = false;
const smoothCanvas = true; const smoothCanvas = true;
export const THIRDPARTY_URLS = { export const THIRDPARTY_URLS = {
discord: "https://discord.gg/HN7EVzV", discord: "https://discord.gg/HN7EVzV",
github: "https://github.com/tobspr-games/shapez.io", github: "https://github.com/tobspr-games/shapez.io",
reddit: "https://www.reddit.com/r/shapezio", reddit: "https://www.reddit.com/r/shapezio",
shapeViewer: "https://viewer.shapez.io", shapeViewer: "https://viewer.shapez.io",
twitter: "https://twitter.com/tobspr", twitter: "https://twitter.com/tobspr",
patreon: "https://www.patreon.com/tobsprgames", patreon: "https://www.patreon.com/tobsprgames",
privacyPolicy: "https://tobspr.io/privacy.html", privacyPolicy: "https://tobspr.io/privacy.html",
standaloneCampaignLink: "https://get.shapez.io/bundle/$campaign", standaloneCampaignLink: "https://get.shapez.io/bundle/$campaign",
puzzleDlcStorePage: "https://get.shapez.io/mm_puzzle_dlc?target=dlc", puzzleDlcStorePage: "https://get.shapez.io/mm_puzzle_dlc?target=dlc",
levelTutorialVideos: { levelTutorialVideos: {
@ -23,52 +28,69 @@ export const THIRDPARTY_URLS = {
25: "https://www.youtube.com/watch?v=7OCV1g40Iew&", 25: "https://www.youtube.com/watch?v=7OCV1g40Iew&",
26: "https://www.youtube.com/watch?v=gfm6dS1dCoY", 26: "https://www.youtube.com/watch?v=gfm6dS1dCoY",
}, },
modBrowser: "https://shapez.mod.io/", modBrowser: "https://shapez.mod.io/",
}; };
export function openStandaloneLink(app: Application, campaign: string) { export function openStandaloneLink(app: Application, campaign: string) {
const discount = globalConfig.currentDiscount > 0 ? "_discount" + globalConfig.currentDiscount : ""; const discount = globalConfig.currentDiscount > 0 ? "_discount" + globalConfig.currentDiscount : "";
const event = campaign + discount; const event = campaign + discount;
app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneCampaignLink.replace("$campaign", event)); app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneCampaignLink.replace("$campaign", event));
app.gameAnalytics.noteMinor("g.stdlink." + event); app.gameAnalytics.noteMinor("g.stdlink." + event);
} }
export const globalConfig = { export const globalConfig = {
// Size of a single tile in Pixels. // Size of a single tile in Pixels.
// NOTICE: Update webpack.production.config too! // NOTICE: Update webpack.production.config too!
tileSize: 32, tileSize: 32,
halfTileSize: 16, halfTileSize: 16,
// Which dpi the assets have // Which dpi the assets have
assetsDpi: 192 / 32, assetsDpi: 192 / 32,
assetsSharpness: 1.5, assetsSharpness: 1.5,
shapesSharpness: 1.3, shapesSharpness: 1.3,
// Achievements // Achievements
achievementSliceDuration: 10, achievementSliceDuration: 10,
// Production analytics // Production analytics
statisticsGraphDpi: 2.5, statisticsGraphDpi: 2.5,
statisticsGraphSlices: 100, statisticsGraphSlices: 100,
analyticsSliceDurationSeconds: G_IS_DEV ? 1 : 10, analyticsSliceDurationSeconds: G_IS_DEV ? 1 : 10,
minimumTickRate: 25, minimumTickRate: 25,
maximumTickRate: 500, maximumTickRate: 500,
// Map // Map
mapChunkSize: 16, mapChunkSize: 16,
chunkAggregateSize: 4, chunkAggregateSize: 4,
mapChunkOverviewMinZoom: 0.9, mapChunkOverviewMinZoom: 0.9,
mapChunkWorldSize: null, mapChunkWorldSize: null,
maxBeltShapeBundleSize: 20, maxBeltShapeBundleSize: 20,
// Belt speeds // Belt speeds
// NOTICE: Update webpack.production.config too! // NOTICE: Update webpack.production.config too!
beltSpeedItemsPerSecond: 2, beltSpeedItemsPerSecond: 2,
minerSpeedItemsPerSecond: 0, minerSpeedItemsPerSecond: 0,
defaultItemDiameter: 20, defaultItemDiameter: 20,
itemSpacingOnBelts: 0.63, itemSpacingOnBelts: 0.63,
wiresSpeedItemsPerSecond: 6, wiresSpeedItemsPerSecond: 6,
undergroundBeltMaxTilesByTier: [5, 9], undergroundBeltMaxTilesByTier: [5, 9],
readerAnalyzeIntervalSeconds: 10, readerAnalyzeIntervalSeconds: 10,
goalAcceptorItemsRequired: 12, goalAcceptorItemsRequired: 12,
goalAcceptorsPerProducer: 5, goalAcceptorsPerProducer: 5,
puzzleModeSpeed: 3, puzzleModeSpeed: 3,
puzzleMinBoundsSize: 2, puzzleMinBoundsSize: 2,
puzzleMaxBoundsSize: 20, puzzleMaxBoundsSize: 20,
puzzleValidationDurationSeconds: 30, puzzleValidationDurationSeconds: 30,
buildingSpeeds: { buildingSpeeds: {
cutter: 1 / 4, cutter: 1 / 4,
cutterQuad: 1 / 4, cutterQuad: 1 / 4,
@ -81,39 +103,53 @@ export const globalConfig = {
mixer: 1 / 5, mixer: 1 / 5,
stacker: 1 / 8, stacker: 1 / 8,
}, },
// Zooming // Zooming
initialZoom: 1.9, initialZoom: 1.9,
minZoomLevel: 0.1, minZoomLevel: 0.1,
maxZoomLevel: 3, maxZoomLevel: 3,
// Global game speed // Global game speed
gameSpeed: 1, gameSpeed: 1,
warmupTimeSecondsFast: 0.25, warmupTimeSecondsFast: 0.25,
warmupTimeSecondsRegular: 0.25, warmupTimeSecondsRegular: 0.25,
smoothing: { smoothing: {
smoothMainCanvas: smoothCanvas && true, smoothMainCanvas: smoothCanvas && true,
quality: "low", // Low is CRUCIAL for mobile performance! quality: "low", // Low is CRUCIAL for mobile performance!
}, },
rendering: {}, rendering: {},
debug: require("./config.local").default, debug: require("./config.local").default,
currentDiscount: 0, currentDiscount: 0,
// Secret vars // Secret vars
info: { info: {
// Binary file salt // Binary file salt
file: "Ec'])@^+*9zMevK3uMV4432x9%iK'=", file: "Ec'])@^+*9zMevK3uMV4432x9%iK'=",
// Savegame salt // Savegame salt
sgSalt: "}95Q3%8/.837Lqym_BJx%q7)pAHJbF", sgSalt: "}95Q3%8/.837Lqym_BJx%q7)pAHJbF",
// Analytics key // Analytics key
analyticsApiKey: "baf6a50f0cc7dfdec5a0e21c88a1c69a4b34bc4a", analyticsApiKey: "baf6a50f0cc7dfdec5a0e21c88a1c69a4b34bc4a",
}, },
}; };
export const IS_MOBILE = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); export const IS_MOBILE = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
// Automatic calculations // Automatic calculations
globalConfig.minerSpeedItemsPerSecond = globalConfig.beltSpeedItemsPerSecond / 5; globalConfig.minerSpeedItemsPerSecond = globalConfig.beltSpeedItemsPerSecond / 5;
globalConfig.mapChunkWorldSize = globalConfig.mapChunkSize * globalConfig.tileSize; globalConfig.mapChunkWorldSize = globalConfig.mapChunkSize * globalConfig.tileSize;
// Dynamic calculations // Dynamic calculations
if (globalConfig.debug.disableMapOverview) { if (globalConfig.debug.disableMapOverview) {
globalConfig.mapChunkOverviewMinZoom = 0; globalConfig.mapChunkOverviewMinZoom = 0;
} }
// Stuff for making the trailer // Stuff for making the trailer
if (G_IS_DEV && globalConfig.debug.renderForTrailer) { if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
globalConfig.debug.framePausesBetweenTicks = 32; globalConfig.debug.framePausesBetweenTicks = 32;
@ -124,9 +160,11 @@ if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
globalConfig.debug.disableSavegameWrite = true; globalConfig.debug.disableSavegameWrite = true;
// globalConfig.beltSpeedItemsPerSecond *= 2; // globalConfig.beltSpeedItemsPerSecond *= 2;
} }
if (globalConfig.debug.fastGameEnter) { if (globalConfig.debug.fastGameEnter) {
globalConfig.debug.noArtificialDelays = true; globalConfig.debug.noArtificialDelays = true;
} }
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) { if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
globalConfig.warmupTimeSecondsFast = 0; globalConfig.warmupTimeSecondsFast = 0;
globalConfig.warmupTimeSecondsRegular = 0; globalConfig.warmupTimeSecondsRegular = 0;

View File

@ -1,61 +1,64 @@
import { globalConfig } from "./config"; import { globalConfig } from "./config";
import { round1Digit, round2Digits } from "./utils"; import { round1Digit, round2Digits } from "./utils";
/** /**
* Returns the current dpi * Returns the current dpi
* {}
*/ */
export function getDeviceDPI(): number { export function getDeviceDPI(): number {
return window.devicePixelRatio || 1; return window.devicePixelRatio || 1;
} }
/** /**
* * @param dpi Smoothed dpi
* {} Smoothed dpi
*/ */
export function smoothenDpi(dpi: number): number { export function smoothenDpi(dpi: number): number {
if (dpi < 0.05) { if (dpi < 0.05) {
return 0.05; return 0.05;
} } else if (dpi < 0.2) {
else if (dpi < 0.2) {
return round2Digits(Math.round(dpi / 0.04) * 0.04); 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); 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); return round1Digit(Math.round(dpi / 0.5) * 0.5);
} } else {
else {
return 4; return 4;
} }
} }
// Initial dpi // Initial dpi
// setDPIMultiplicator(1); // setDPIMultiplicator(1);
/** /**
* Prepares a context for hihg dpi rendering * Prepares a context for high dpi rendering
*/ */
export function prepareHighDPIContext(context: CanvasRenderingContext2D, smooth = true) { export function prepareHighDPIContext(context: CanvasRenderingContext2D, smooth = true) {
const dpi = getDeviceDPI(); const dpi = getDeviceDPI();
context.scale(dpi, dpi); context.scale(dpi, dpi);
if (smooth) { if (smooth) {
context.imageSmoothingEnabled = true; context.imageSmoothingEnabled = true;
context.webkitImageSmoothingEnabled = true; context.webkitImageSmoothingEnabled = true;
// @ts-ignore // @ts-ignore
context.imageSmoothingQuality = globalConfig.smoothing.quality; context.imageSmoothingQuality = globalConfig.smoothing.quality;
} } else {
else {
context.imageSmoothingEnabled = false; context.imageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false; context.webkitImageSmoothingEnabled = false;
} }
} }
/** /**
* Resizes a high dpi canvas * Resizes a high dpi canvas
*/ */
export function resizeHighDPICanvas(canvas: HTMLCanvasElement, w: number, h: number, smooth = true) { export function resizeHighDPICanvas(canvas: HTMLCanvasElement, w: number, h: number, smooth = true) {
const dpi = getDeviceDPI(); const dpi = getDeviceDPI();
const wNumber = Math.floor(w); const wNumber = Math.floor(w);
const hNumber = Math.floor(h); const hNumber = Math.floor(h);
const targetW = Math.floor(wNumber * dpi); const targetW = Math.floor(wNumber * dpi);
const targetH = Math.floor(hNumber * dpi); const targetH = Math.floor(hNumber * dpi);
if (targetW !== canvas.width || targetH !== canvas.height) { if (targetW !== canvas.width || targetH !== canvas.height) {
// console.log("Resize Canvas from", canvas.width, canvas.height, "to", targetW, targetH) // console.log("Resize Canvas from", canvas.width, canvas.height, "to", targetW, targetH)
canvas.width = targetW; canvas.width = targetW;
@ -65,6 +68,7 @@ export function resizeHighDPICanvas(canvas: HTMLCanvasElement, w: number, h: num
prepareHighDPIContext(canvas.getContext("2d"), smooth); prepareHighDPIContext(canvas.getContext("2d"), smooth);
} }
} }
/** /**
* Resizes a canvas * 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); // console.log("Resizing canvas to", actualW, "x", actualH);
} }
} }
/** /**
* Resizes a canvas and makes sure its cleared * 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 actualW = Math.ceil(w);
const actualH = Math.ceil(h); const actualH = Math.ceil(h);
if (actualW !== canvas.width || actualH !== canvas.height) { 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.width = actualW + "px";
canvas.style.height = actualH + "px"; canvas.style.height = actualH + "px";
// console.log("Resizing canvas to", actualW, "x", actualH); // console.log("Resizing canvas to", actualW, "x", actualH);
} } else {
else {
context.clearRect(0, 0, actualW, actualH); context.clearRect(0, 0, actualW, actualH);
} }
} }

View File

@ -1,14 +1,20 @@
import { globalConfig } from "./config"; import { globalConfig } from "./config";
export type GameRoot = import("../game/root").GameRoot; export type GameRoot = import("../game/root").GameRoot;
export type Rectangle = import("./rectangle").Rectangle; export type Rectangle = import("./rectangle").Rectangle;
export class DrawParameters { export class DrawParameters {
public context: CanvasRenderingContext2D = context; public context: CanvasRenderingContext2D;
public visibleRect: Rectangle = visibleRect; public visibleRect: Rectangle;
public desiredAtlasScale: string = desiredAtlasScale; public desiredAtlasScale: string;
public zoomLevel: number = zoomLevel; public zoomLevel: number;
public root: GameRoot = root; public root: GameRoot;
constructor({ context, visibleRect, desiredAtlasScale, zoomLevel, root }) { constructor({ context, visibleRect, desiredAtlasScale, zoomLevel, root }) {
this.context = context;
this.visibleRect = visibleRect;
this.desiredAtlasScale = desiredAtlasScale;
this.zoomLevel = zoomLevel;
this.root = root;
} }
} }

View File

@ -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 { globalConfig } from "./config";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { Rectangle } from "./rectangle"; import { Rectangle } from "./rectangle";
const logger = createLogger("draw_utils"); const logger = createLogger("draw_utils");
export function initDrawUtils() { export function initDrawUtils() {
CanvasRenderingContext2D.prototype.beginRoundedRect = function (x, y, w, h, r) { CanvasRenderingContext2D.prototype.beginRoundedRect = function (x, y, w, h, r) {
this.beginPath(); this.beginPath();
if (r < 0.05) { if (r < 0.05) {
this.rect(x, y, w, h); this.rect(x, y, w, h);
return; return;
} }
if (w < 2 * r) { if (w < 2 * r) {
r = w / 2; r = w / 2;
} }
if (h < 2 * r) { if (h < 2 * r) {
r = h / 2; r = h / 2;
} }
this.moveTo(x + r, y); this.moveTo(x + r, y);
this.arcTo(x + w, y, x + w, y + h, r); this.arcTo(x + w, y, x + w, y + h, r);
this.arcTo(x + w, y + h, x, 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 + h, x, y, r);
this.arcTo(x, y, x + w, y, r); this.arcTo(x, y, x + w, y, r);
}; };
CanvasRenderingContext2D.prototype.beginCircle = function (x, y, r) { CanvasRenderingContext2D.prototype.beginCircle = function (x, y, r) {
this.beginPath(); this.beginPath();
if (r < 0.05) { if (r < 0.05) {
this.rect(x, y, 1, 1); this.rect(x, y, 1, 1);
return; return;
} }
this.arc(x, y, r, 0, 2.0 * Math.PI); 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; parameters: DrawParameters;
sprite: AtlasSprite; sprite: AtlasSprite;
x: number; x: number;
y: number; y: number;
angle: number; angle: number;
size: number; size: number;
offsetX: number=; offsetX: number;
offsetY: number=; offsetY: number;
}) { }) {
if (angle === 0) { if (angle === 0) {
sprite.drawCachedCentered(parameters, x + offsetX, y + offsetY, size); sprite.drawCachedCentered(parameters, x + offsetX, y + offsetY, size);
return; return;
} }
parameters.context.translate(x, y); parameters.context.translate(x, y);
parameters.context.rotate(angle); parameters.context.rotate(angle);
sprite.drawCachedCentered(parameters, offsetX, offsetY, size, false); sprite.drawCachedCentered(parameters, offsetX, offsetY, size, false);
parameters.context.rotate(-angle); parameters.context.rotate(-angle);
parameters.context.translate(-x, -y); parameters.context.translate(-x, -y);
} }
let warningsShown = 0; let warningsShown = 0;
/** /**
* Draws a sprite with clipping * 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; parameters: DrawParameters;
sprite: HTMLCanvasElement; sprite: HTMLCanvasElement;
x: number; x: number;
@ -72,7 +103,11 @@ export function drawSpriteClipped({ parameters, sprite, x, y, w, h, originalW, o
if (!intersection) { if (!intersection) {
// Clipped // Clipped
if (++warningsShown % 200 === 1) { 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) { if (G_IS_DEV && globalConfig.debug.testClipping) {
parameters.context.fillStyle = "yellow"; parameters.context.fillStyle = "yellow";
@ -80,9 +115,19 @@ export function drawSpriteClipped({ parameters, sprite, x, y, w, h, originalW, o
} }
return; return;
} }
parameters.context.drawImage(sprite,
// src pos and size parameters.context.drawImage(
((intersection.x - x) / w) * originalW, ((intersection.y - y) / h) * originalH, (originalW * intersection.w) / w, (originalH * intersection.h) / h, sprite,
// dest pos and size // src pos and size
intersection.x, intersection.y, intersection.w, intersection.h); ((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
);
} }

View File

@ -1,28 +1,36 @@
export class ExplainedResult { export class ExplainedResult {
public result: boolean = result; public result: boolean;
public reason: string = reason; 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 // Copy additional props
for (const key in additionalProps) { for (const key in additionalProps) {
this[key] = additionalProps[key]; this[key] = additionalProps[key];
} }
} }
isGood() { isGood() {
return !!this.result; return !!this.result;
} }
isBad() { isBad() {
return !this.result; return !this.result;
} }
static good() { static good() {
return new ExplainedResult(true); return new ExplainedResult(true);
} }
static bad(reason, additionalProps) {
static bad(reason?: string, additionalProps?: object) {
return new ExplainedResult(false, reason, additionalProps); return new ExplainedResult(false, reason, additionalProps);
} }
static requireAll(...args) {
static requireAll(...args: (() => ExplainedResult)[]) {
for (let i = 0; i < args.length; ++i) { for (let i = 0; i < args.length; ++i) {
const subResult = args[i].call(); const subResult = args[i].call(undefined);
if (!subResult.isGood()) { if (!subResult.isGood()) {
return subResult; return subResult;
} }

View File

@ -1,18 +1,20 @@
import { createLogger } from "./logging"; import { createLogger } from "./logging";
const logger = createLogger("factory"); const logger = createLogger("factory");
// simple factory pattern // simple factory pattern
export class Factory { export class Factory<T extends { getId: () => string }> {
public id = id; public entries: T[] = [];
public entries = []; public entryIds: string[] = [];
public entryIds = [];
public idToEntry = {}; public idToEntry = {};
constructor(id) { constructor(public id?: string) {}
}
getId() { getId() {
return this.id; return this.id;
} }
register(entry) {
register(entry: T) {
// Extract id // Extract id
const id = entry.getId(); const id = entry.getId();
assert(id, "Factory: Invalid id for class: " + entry); assert(id, "Factory: Invalid id for class: " + entry);
@ -23,18 +25,18 @@ export class Factory {
this.entryIds.push(id); this.entryIds.push(id);
this.idToEntry[id] = entry; this.idToEntry[id] = entry;
} }
/** /**
* Checks if a given id is registered * Checks if a given id is registered
* {}
*/ */
hasId(id: string): boolean { hasId(id: string): boolean {
return !!this.idToEntry[id]; return !!this.idToEntry[id];
} }
/** /**
* Finds an instance by a given id * Finds an instance by a given id
* {}
*/ */
findById(id: string): object { findById(id: string): T {
const entry = this.idToEntry[id]; const entry = this.idToEntry[id];
if (!entry) { if (!entry) {
logger.error("Object with id", id, "is not registered on factory", this.id, "!"); logger.error("Object with id", id, "is not registered on factory", this.id, "!");
@ -43,23 +45,23 @@ export class Factory {
} }
return entry; return entry;
} }
/** /**
* Returns all entries * Returns all entries
* {}
*/ */
getEntries(): Array<object> { getEntries(): Array<T> {
return this.entries; return this.entries;
} }
/** /**
* Returns all registered ids * Returns all registered ids
* {}
*/ */
getAllIds(): Array<string> { getAllIds(): Array<string> {
return this.entryIds; return this.entryIds;
} }
/** /**
* Returns amount of stored entries * Returns amount of stored entries
* {}
*/ */
getNumEntries(): number { getNumEntries(): number {
return this.entries.length; return this.entries.length;

View File

@ -1,48 +1,53 @@
/* typehints:start */
import type { Application } from "../application"; import type { Application } from "../application";
import type { StateManager } from "./state_manager"; import type { StateManager } from "./state_manager";
/* typehints:end */
import { globalConfig } from "./config"; import { globalConfig } from "./config";
import { ClickDetector } from "./click_detector"; import { ClickDetector, ClickDetectorConstructorArgs } from "./click_detector";
import { logSection, createLogger } from "./logging"; import { logSection, createLogger } from "./logging";
import { InputReceiver } from "./input_receiver"; import { InputReceiver } from "./input_receiver";
import { waitNextFrame } from "./utils"; import { waitNextFrame } from "./utils";
import { RequestChannel } from "./request_channel"; import { RequestChannel } from "./request_channel";
import { MUSIC } from "../platform/sound"; import { MUSIC } from "../platform/sound";
const logger = createLogger("game_state"); const logger = createLogger("game_state");
/** /**
* Basic state of the game state machine. This is the base of the whole game * Basic state of the game state machine. This is the base of the whole game
*/ */
export class GameState { export abstract class GameState {
public key = key;
public stateManager: StateManager = null; public stateManager: StateManager = null;
public app: Application = null; public app: Application = null;
public fadingOut = false; public fadingOut = false;
public clickDetectors: Array<ClickDetector> = []; public clickDetectors: Array<ClickDetector> = [];
public inputReciever = new InputReceiver("state-" + key); public inputReceiver: InputReceiver;
public asyncChannel = new RequestChannel(); public asyncChannel = new RequestChannel();
public htmlElement: HTMLElement;
/** /**
* Constructs a new state with the given id * Constructs a new state with the given id
*/ */
constructor(public key) {
constructor(key) { this.inputReceiver = new InputReceiver("state-" + key);
this.inputReciever.backButton.add(this.onBackButton, this); this.inputReceiver.backButton.add(this.onBackButton, this);
} }
//// GETTERS / HELPER METHODS //// //// GETTERS / HELPER METHODS ////
/** /**
* Returns the states key * Returns the states key
* {}
*/ */
getKey(): string { getKey(): string {
return this.key; return this.key;
} }
/** /**
* Returns the html element of the state * Returns the html element of the state
* {}
*/ */
getDivElement(): HTMLElement { getDivElement(): HTMLElement {
return document.getElementById("state_" + this.key); return document.getElementById("state_" + this.key);
} }
/** /**
* Transfers to a new state * Transfers to a new state
*/ */
@ -51,8 +56,10 @@ export class GameState {
logger.warn("Skipping move to '" + stateKey + "' since already fading out"); logger.warn("Skipping move to '" + stateKey + "' since already fading out");
return; return;
} }
// Clean up event listeners // Clean up event listeners
this.internalCleanUpClickDetectors(); this.internalCleanUpClickDetectors();
// Fading // Fading
const fadeTime = this.internalGetFadeInOutTime(); const fadeTime = this.internalGetFadeInOutTime();
const doFade = !skipFadeOut && this.getHasFadeOut() && fadeTime !== 0; const doFade = !skipFadeOut && this.getHasFadeOut() && fadeTime !== 0;
@ -63,16 +70,16 @@ export class GameState {
setTimeout(() => { setTimeout(() => {
this.stateManager.moveToState(stateKey, payload); this.stateManager.moveToState(stateKey, payload);
}, fadeTime); }, fadeTime);
} } else {
else {
this.stateManager.moveToState(stateKey, payload); this.stateManager.moveToState(stateKey, payload);
} }
} }
/** /**
* Tracks clicks on a given element and calls the given callback *on this state*. * 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. * 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); const detector = new ClickDetector(element, args);
detector.click.add(handler, this); detector.click.add(handler, this);
if (G_IS_DEV) { if (G_IS_DEV) {
@ -82,50 +89,62 @@ export class GameState {
} }
this.clickDetectors.push(detector); this.clickDetectors.push(detector);
} }
/** /**
* Cancels all promises on the api as well as our async channel * Cancels all promises on the api as well as our async channel
*/ */
cancelAllAsyncOperations() { cancelAllAsyncOperations() {
this.asyncChannel.cancelAll(); this.asyncChannel.cancelAll();
} }
//// CALLBACKS //// //// CALLBACKS ////
/** /**
* Callback when entering the state, to be overriddemn * Callback when entering the state, to be overriddemn
*/ */
onEnter(payload: any) { } onEnter(payload: any) {}
/** /**
* Callback when leaving the state * Callback when leaving the state
*/ */
onLeave() { } onLeave() {}
/** /**
* Callback when the app got paused (on android, this means in background) * 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) * Callback when the app got resumed (on android, this means in foreground again)
*/ */
onAppResume() { } onAppResume() {}
/** /**
* Render callback * Render callback
*/ */
onRender(dt: number) { } onRender(dt: number) {}
/** /**
* Background tick callback, called while the game is inactiev * Background tick callback, called while the game is inactiev
*/ */
onBackgroundTick(dt: number) { } onBackgroundTick(dt: number) {}
/** /**
* Called when the screen resized * 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 * Internal backbutton handler, called when the hardware back button is pressed or
* the escape key is pressed * the escape key is pressed
*/ */
onBackButton() { } onBackButton() {}
//// INTERFACE //// //// INTERFACE ////
/** /**
* Should return how many mulliseconds to fade in / out the state. Not recommended to override! * 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 { getInOutFadeTime(): number {
if (globalConfig.debug.noArtificialDelays) { if (globalConfig.debug.noArtificialDelays) {
@ -133,38 +152,36 @@ export class GameState {
} }
return 200; return 200;
} }
/** /**
* Should return whether to fade in the game state. This will then apply the right css classes * Should return whether to fade in the game state. This will then apply the right css classes
* for the fadein. * for the fadein.
* {}
*/ */
getHasFadeIn(): boolean { getHasFadeIn(): boolean {
return true; return true;
} }
/** /**
* Should return whether to fade out the game state. This will then apply the right css classes * 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 * for the fadeout and wait the delay before moving states
* {}
*/ */
getHasFadeOut(): boolean { getHasFadeOut(): boolean {
return true; return true;
} }
/** /**
* Returns if this state should get paused if it does not have focus * 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 { getPauseOnFocusLost(): boolean {
return true; return true;
} }
/** /**
* Should return the html code of the state. * Should return the html code of the state.
* {}
* @abstract
*/ */
getInnerHTML(): string { abstract getInnerHTML(): string;
abstract;
return "";
}
/** /**
* Returns if the state has an unload confirmation, this is the * Returns if the state has an unload confirmation, this is the
* "Are you sure you want to leave the page" message. * "Are you sure you want to leave the page" message.
@ -172,46 +189,52 @@ export class GameState {
getHasUnloadConfirmation() { getHasUnloadConfirmation() {
return false; return false;
} }
/** /**
* Should return the theme music for this state * Should return the theme music for this state
* {}
*/ */
getThemeMusic(): string | null { getThemeMusic(): string | null {
return MUSIC.menu; return MUSIC.menu;
} }
/** /**
* Should return true if the player is currently ingame * Should return true if the player is currently ingame
* {}
*/ */
getIsIngame(): boolean { getIsIngame(): boolean {
return false; return false;
} }
/** /**
* Should return whether to clear the whole body content before entering the state. * Should return whether to clear the whole body content before entering the state.
* {}
*/ */
getRemovePreviousContent(): boolean { getRemovePreviousContent(): boolean {
return true; return true;
} }
//////////////////// ////////////////////
//// INTERNAL //// //// INTERNAL ////
/** /**
* Internal callback from the manager. Do not override! * Internal callback from the manager. Do not override!
*/ */
internalRegisterCallback(stateManager: StateManager, app) { internalRegisterCallback(stateManager: StateManager, app: Application) {
assert(stateManager, "No state manager"); assert(stateManager, "No state manager");
assert(app, "No app"); assert(app, "No app");
this.stateManager = stateManager; this.stateManager = stateManager;
this.app = app; this.app = app;
} }
/** /**
* Internal callback when entering the state. Do not override! * Internal callback when entering the state. Do not override!
*/ */
internalEnterCallback(payload: any, callCallback: boolean = true) { internalEnterCallback(payload: any, callCallback: boolean = true) {
logSection(this.key, "#26a69a"); logSection(this.key, "#26a69a");
this.app.inputMgr.pushReciever(this.inputReciever); this.app.inputMgr.pushReciever(this.inputReceiver);
this.htmlElement = this.getDivElement(); this.htmlElement = this.getDivElement();
this.htmlElement.classList.add("active"); this.htmlElement.classList.add("active");
// Apply classes in the next frame so the css transition keeps up // Apply classes in the next frame so the css transition keeps up
waitNextFrame().then(() => { waitNextFrame().then(() => {
if (this.htmlElement) { if (this.htmlElement) {
@ -219,33 +242,38 @@ export class GameState {
this.htmlElement.classList.remove("fadingIn"); this.htmlElement.classList.remove("fadingIn");
} }
}); });
// Call handler // Call handler
if (callCallback) { if (callCallback) {
this.onEnter(payload); this.onEnter(payload);
} }
} }
/** /**
* Internal callback when the state is left. Do not override! * Internal callback when the state is left. Do not override!
*/ */
internalLeaveCallback() { internalLeaveCallback() {
this.onLeave(); this.onLeave();
this.htmlElement.classList.remove("active"); this.htmlElement.classList.remove("active");
this.app.inputMgr.popReciever(this.inputReciever); this.app.inputMgr.popReciever(this.inputReceiver);
this.internalCleanUpClickDetectors(); this.internalCleanUpClickDetectors();
this.asyncChannel.cancelAll(); this.asyncChannel.cancelAll();
} }
/** /**
* Internal app pause callback * Internal app pause callback
*/ */
internalOnAppPauseCallback() { internalOnAppPauseCallback() {
this.onAppPause(); this.onAppPause();
} }
/** /**
* Internal app resume callback * Internal app resume callback
*/ */
internalOnAppResumeCallback() { internalOnAppResumeCallback() {
this.onAppResume(); this.onAppResume();
} }
/** /**
* Cleans up all click detectors * Cleans up all click detectors
*/ */
@ -257,16 +285,17 @@ export class GameState {
this.clickDetectors = []; this.clickDetectors = [];
} }
} }
/** /**
* Internal method to get the HTML of the game state. * Internal method to get the HTML of the game state.
* {}
*/ */
internalGetFullHtml(): string { internalGetFullHtml(): string {
return this.getInnerHTML(); return this.getInnerHTML();
} }
/** /**
* Internal method to compute the time to fade in / out * 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 { internalGetFadeInOutTime(): number {
if (G_IS_DEV && globalConfig.debug.fastGameEnter) { if (G_IS_DEV && globalConfig.debug.fastGameEnter) {

View File

@ -1,22 +1,27 @@
import { SingletonFactory } from "./singleton_factory"; import { SingletonFactory } from "./singleton_factory";
import { Factory } from "./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: { export let gBuildingsByCategory: {
[idx: string]: Array<Class<MetaBuilding>>; [idx: string]: Array<Class<MetaBuilding>>;
} = null; } = null;
export let gComponentRegistry: FactoryTemplate<Component> = new Factory("component");
export let gGameModeRegistry: FactoryTemplate<GameMode> = new Factory("gameMode"); export let gComponentRegistry = new Factory<Component>("component");
export let gGameSpeedRegistry: FactoryTemplate<BaseGameSpeed> = new Factory("gamespeed"); export let gGameModeRegistry = new Factory<GameMode>("gameMode");
export let gItemRegistry: FactoryTemplate<BaseItem> = new Factory("item"); export let gGameSpeedRegistry = new Factory<BaseGameSpeed>("gamespeed");
export let gItemRegistry = new Factory<BaseItem>("item");
// Helpers // Helpers
export function initBuildingsByCategory(buildings: {
[idx: string]: Array<Class<MetaBuilding>>; export function initBuildingsByCategory(buildings: { [idx: string]: Array<Class<MetaBuilding>> }) {
}) {
gBuildingsByCategory = buildings; gBuildingsByCategory = buildings;
} }

View File

@ -1,15 +1,16 @@
/* typehints:start */
import type { Application } from "../application"; import type { Application } from "../application";
/* typehints:end */
/** /**
* Used for the bug reporter, and the click detector which both have no handles to this. * 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! * It would be nicer to have no globals, but this is the only one. I promise!
*/ */
export let GLOBAL_APP: Application = null; export let GLOBAL_APP: Application = null;
export function setGlobalApp(app: Application) { export function setGlobalApp(app: Application) {
assert(!GLOBAL_APP, "Create application twice!"); assert(!GLOBAL_APP, "Create application twice!");
GLOBAL_APP = app; GLOBAL_APP = app;
} }
export const BUILD_OPTIONS = { export const BUILD_OPTIONS = {
HAVE_ASSERT: G_HAVE_ASSERT, HAVE_ASSERT: G_HAVE_ASSERT,
APP_ENVIRONMENT: G_APP_ENVIRONMENT, APP_ENVIRONMENT: G_APP_ENVIRONMENT,

View File

@ -1,91 +1,120 @@
/* typehints:start */ import { Application } from "../application";
import type { Application } from "../application";
import type { InputReceiver } from "./input_receiver"; import type { InputReceiver } from "./input_receiver";
/* typehints:end */
import { Signal, STOP_PROPAGATION } from "./signal"; import { Signal, STOP_PROPAGATION } from "./signal";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { arrayDeleteValue, fastArrayDeleteValue } from "./utils"; import { arrayDeleteValue, fastArrayDeleteValue } from "./utils";
const logger = createLogger("input_distributor"); const logger = createLogger("input_distributor");
export class InputDistributor { export class InputDistributor {
public app = app; public app: Application;
public recieverStack: Array<InputReceiver> = []; public receiverStack: Array<InputReceiver> = [];
public filters: Array<function(: boolean):boolean> = []; public filters: Array<(arg: any) => boolean> = [];
/** All keys which are currently down */
public keysDown = new Set(); public keysDown = new Set();
constructor(app) { constructor(app: Application) {
this.app = app;
this.bindToEvents(); this.bindToEvents();
} }
/** /**
* Attaches a new filter which can filter and reject events * Attaches a new filter which can filter and reject events
*/ */
installFilter(filter: function(: boolean):boolean) { installFilter(filter: (arg: any) => boolean) {
this.filters.push(filter); this.filters.push(filter);
} }
/** /**
* Removes an attached filter * Removes an attached filter
*/ */
dismountFilter(filter: function(: boolean):boolean) { dismountFilter(filter: (arg: any) => boolean) {
fastArrayDeleteValue(this.filters, filter); fastArrayDeleteValue(this.filters, filter);
} }
pushReciever(reciever: InputReceiver) {
pushReciever(reciever: InputReceiver) {
if (this.isRecieverAttached(reciever)) { if (this.isRecieverAttached(reciever)) {
assert(false, "Can not add reciever " + reciever.context + " twice"); assert(false, "Can not add reciever " + reciever.context + " twice");
logger.error("Can not add reciever", reciever.context, "twice"); logger.error("Can not add reciever", reciever.context, "twice");
return; return;
} }
this.recieverStack.push(reciever); this.receiverStack.push(reciever);
if (this.recieverStack.length > 10) {
logger.error("Reciever stack is huge, probably some dead receivers arround:", this.recieverStack.map(x => x.context)); 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"); assert(false, "Can not pop reciever " + reciever.context + " since its not contained");
logger.error("Can not pop reciever", reciever.context, "since its not contained"); logger.error("Can not pop reciever", reciever.context, "since its not contained");
return; return;
} }
if (this.recieverStack[this.recieverStack.length - 1] !== reciever) { 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.recieverStack.map(x => x.context)); 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) && isRecieverOnTop(reciever: InputReceiver) {
this.recieverStack[this.recieverStack.length - 1] === reciever); return (
this.isRecieverAttached(reciever) &&
this.receiverStack[this.receiverStack.length - 1] === reciever
);
} }
makeSureAttachedAndOnTop(reciever: InputReceiver) {
makeSureAttachedAndOnTop(reciever: InputReceiver) {
this.makeSureDetached(reciever); this.makeSureDetached(reciever);
this.pushReciever(reciever); this.pushReciever(reciever);
} }
makeSureDetached(reciever: InputReceiver) {
makeSureDetached(reciever: InputReceiver) {
if (this.isRecieverAttached(reciever)) { if (this.isRecieverAttached(reciever)) {
arrayDeleteValue(this.recieverStack, reciever); arrayDeleteValue(this.receiverStack, reciever);
} }
} }
destroyReceiver(reciever: InputReceiver) {
destroyReceiver(reciever: InputReceiver) {
this.makeSureDetached(reciever); this.makeSureDetached(reciever);
reciever.cleanup(); reciever.cleanup();
} }
// Internal // Internal
getTopReciever() { getTopReciever() {
if (this.recieverStack.length > 0) { if (this.receiverStack.length > 0) {
return this.recieverStack[this.recieverStack.length - 1]; return this.receiverStack[this.receiverStack.length - 1];
} }
return null; return null;
} }
bindToEvents() { bindToEvents() {
window.addEventListener("popstate", this.handleBackButton.bind(this), false); window.addEventListener("popstate", this.handleBackButton.bind(this), false);
document.addEventListener("backbutton", this.handleBackButton.bind(this), false); document.addEventListener("backbutton", this.handleBackButton.bind(this), false);
window.addEventListener("keydown", this.handleKeyMouseDown.bind(this)); window.addEventListener("keydown", this.handleKeyMouseDown.bind(this));
window.addEventListener("keyup", this.handleKeyMouseUp.bind(this)); window.addEventListener("keyup", this.handleKeyMouseUp.bind(this));
window.addEventListener("mousedown", this.handleKeyMouseDown.bind(this)); window.addEventListener("mousedown", this.handleKeyMouseDown.bind(this));
window.addEventListener("mouseup", this.handleKeyMouseUp.bind(this)); window.addEventListener("mouseup", this.handleKeyMouseUp.bind(this));
window.addEventListener("blur", this.handleBlur.bind(this)); window.addEventListener("blur", this.handleBlur.bind(this));
document.addEventListener("paste", this.handlePaste.bind(this)); document.addEventListener("paste", this.handlePaste.bind(this));
} }
forwardToReceiver(eventId, payload = null) { forwardToReceiver(eventId, payload = null) {
// Check filters // Check filters
for (let i = 0; i < this.filters.length; ++i) { for (let i = 0; i < this.filters.length; ++i) {
@ -93,6 +122,7 @@ export class InputDistributor {
return STOP_PROPAGATION; return STOP_PROPAGATION;
} }
} }
const reciever = this.getTopReciever(); const reciever = this.getTopReciever();
if (!reciever) { if (!reciever) {
logger.warn("Dismissing event because not reciever was found:", eventId); 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"); assert(signal instanceof Signal, "Not a valid event id");
return signal.dispatch(payload); return signal.dispatch(payload);
} }
handleBackButton(event: Event) {
handleBackButton(event: Event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.forwardToReceiver("backButton"); this.forwardToReceiver("backButton");
} }
/** /**
* Handles when the page got blurred * Handles when the page got blurred
*/ */
@ -114,13 +146,15 @@ export class InputDistributor {
this.forwardToReceiver("pageBlur", {}); this.forwardToReceiver("pageBlur", {});
this.keysDown.clear(); this.keysDown.clear();
} }
handlePaste(ev) { handlePaste(ev) {
this.forwardToReceiver("paste", ev); this.forwardToReceiver("paste", ev);
} }
handleKeyMouseDown(event: KeyboardEvent | MouseEvent) {
handleKeyMouseDown(event: KeyboardEvent | MouseEvent) {
const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode; const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
if (keyCode === 4 || // MB4 if (
keyCode === 4 || // MB4
keyCode === 5 || // MB5 keyCode === 5 || // MB5
keyCode === 9 || // TAB keyCode === 9 || // TAB
keyCode === 16 || // SHIFT keyCode === 16 || // SHIFT
@ -130,18 +164,23 @@ export class InputDistributor {
) { ) {
event.preventDefault(); event.preventDefault();
} }
const isInitial = !this.keysDown.has(keyCode); const isInitial = !this.keysDown.has(keyCode);
this.keysDown.add(keyCode); this.keysDown.add(keyCode);
if (this.forwardToReceiver("keydown", {
keyCode: keyCode, if (
shift: event.shiftKey, this.forwardToReceiver("keydown", {
alt: event.altKey, keyCode: keyCode,
ctrl: event.ctrlKey, shift: event.shiftKey,
initial: isInitial, alt: event.altKey,
event, ctrl: event.ctrlKey,
}) === STOP_PROPAGATION) { initial: isInitial,
event,
}) === STOP_PROPAGATION
) {
return; return;
} }
if (keyCode === 27) { if (keyCode === 27) {
// Escape key // Escape key
event.preventDefault(); event.preventDefault();
@ -149,9 +188,11 @@ export class InputDistributor {
return this.forwardToReceiver("backButton"); return this.forwardToReceiver("backButton");
} }
} }
handleKeyMouseUp(event: KeyboardEvent | MouseEvent) {
handleKeyMouseUp(event: KeyboardEvent | MouseEvent) {
const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode; const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
this.keysDown.delete(keyCode); this.keysDown.delete(keyCode);
this.forwardToReceiver("keyup", { this.forwardToReceiver("keyup", {
keyCode: keyCode, keyCode: keyCode,
shift: event.shiftKey, shift: event.shiftKey,

View File

@ -1,6 +1,5 @@
import { Signal } from "./signal"; import { Signal } from "./signal";
export class InputReceiver { export class InputReceiver {
public context = context;
public backButton = new Signal(); public backButton = new Signal();
public keydown = new Signal(); public keydown = new Signal();
public keyup = new Signal(); public keyup = new Signal();
@ -8,13 +7,14 @@ export class InputReceiver {
public destroyed = new Signal(); public destroyed = new Signal();
public paste = new Signal(); public paste = new Signal();
constructor(context = "unknown") { constructor(public context: string = "unknown") {}
}
cleanup() { cleanup() {
this.backButton.removeAll(); this.backButton.removeAll();
this.keydown.removeAll(); this.keydown.removeAll();
this.keyup.removeAll(); this.keyup.removeAll();
this.paste.removeAll(); this.paste.removeAll();
this.destroyed.dispatch(); this.destroyed.dispatch();
} }
} }

View File

@ -2,25 +2,27 @@ import { makeOffscreenBuffer } from "./buffer_utils";
import { AtlasSprite, BaseSprite, RegularSprite, SpriteAtlasLink } from "./sprites"; import { AtlasSprite, BaseSprite, RegularSprite, SpriteAtlasLink } from "./sprites";
import { cachebust } from "./cachebust"; import { cachebust } from "./cachebust";
import { createLogger } from "./logging"; 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 logger = createLogger("loader");
const missingSpriteIds = {}; const missingSpriteIds = {};
class LoaderImpl { class LoaderImpl {
public app = null; public app = null;
public sprites: Map<string, BaseSprite> = new Map(); public sprites: Map<string, BaseSprite> = new Map();
public rawImages = []; public rawImages = [];
public spriteNotFoundSprite: AtlasSprite;
constructor() { linkAppAfterBoot(app: Application) {
}
linkAppAfterBoot(app: Application) {
this.app = app; this.app = app;
this.makeSpriteNotFoundCanvas(); this.makeSpriteNotFoundCanvas();
} }
/** /**
* Fetches a given sprite from the cache * Fetches a given sprite from the cache
* {}
*/ */
getSpriteInternal(key: string): BaseSprite { getSpriteInternal(key: string): BaseSprite {
const sprite = this.sprites.get(key); const sprite = this.sprites.get(key);
@ -34,45 +36,53 @@ class LoaderImpl {
} }
return sprite; return sprite;
} }
/** /**
* Returns an atlas sprite from the cache * Returns an atlas sprite from the cache
* {}
*/ */
getSprite(key: string): AtlasSprite { getSprite(key: string): AtlasSprite {
const sprite = this.getSpriteInternal(key); const sprite = this.getSpriteInternal(key);
assert(sprite instanceof AtlasSprite || sprite === this.spriteNotFoundSprite, "Not an atlas sprite"); 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 * Returns a regular sprite from the cache
* {}
*/ */
getRegularSprite(key: string): RegularSprite { getRegularSprite(key: string): RegularSprite {
const sprite = this.getSpriteInternal(key); const sprite = this.getSpriteInternal(key);
assert(sprite instanceof RegularSprite || sprite === this.spriteNotFoundSprite, "Not a regular sprite"); assert(
return sprite as RegularSprite); 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 return this.app.backgroundResourceLoader
.preloadWithProgress("res/" + key, progress => { .preloadWithProgress("res/" + key, progress => {
progressHandler(progress); progressHandler(progress);
}) })
.then(url => { .then(url => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const image = new Image(); const image = new Image();
image.addEventListener("load", () => resolve(image)); image.addEventListener("load", () => resolve(image));
image.addEventListener("error", err => reject("Failed to load sprite " + key + ": " + err)); image.addEventListener("error", err =>
image.src = url; reject("Failed to load sprite " + key + ": " + err)
);
image.src = url;
});
}); });
});
} }
/** /**
* Preloads a sprite * Preloads a sprite
* {}
*/ */
preloadCSSSprite(key: string, progressHandler: (progress: number) => void): Promise<void> { preloadCSSSprite(key: string, progressHandler: (progress: number) => void): Promise<void> {
return this.internalPreloadImage(key, progressHandler).then(image => { return this.internalPreloadImage(key, progressHandler).then(image => {
@ -83,9 +93,9 @@ class LoaderImpl {
this.rawImages.push(image); this.rawImages.push(image);
}); });
} }
/** /**
* Preloads an atlas * Preloads an atlas
* {}
*/ */
preloadAtlas(atlas: AtlasDefinition, progressHandler: (progress: number) => void): Promise<void> { preloadAtlas(atlas: AtlasDefinition, progressHandler: (progress: number) => void): Promise<void> {
return this.internalPreloadImage(atlas.getFullSourcePath(), progressHandler).then(image => { return this.internalPreloadImage(atlas.getFullSourcePath(), progressHandler).then(image => {
@ -94,11 +104,15 @@ class LoaderImpl {
return this.internalParseAtlas(atlas, image); return this.internalParseAtlas(atlas, image);
}); });
} }
internalParseAtlas({ meta: { scale }, sourceData }: AtlasDefinition, loadedImage: HTMLImageElement) {
internalParseAtlas({ meta: { scale }, sourceData }: AtlasDefinition, loadedImage: HTMLImageElement) {
this.rawImages.push(loadedImage); this.rawImages.push(loadedImage);
for (const spriteName in sourceData) { for (const spriteName in sourceData) {
const { frame, sourceSize, spriteSourceSize } = sourceData[spriteName]; const { frame, sourceSize, spriteSourceSize } = sourceData[spriteName];
let sprite = this.sprites.get(spriteName) as AtlasSprite);
let sprite = this.sprites.get(spriteName) as AtlasSprite;
if (!sprite) { if (!sprite) {
sprite = new AtlasSprite(spriteName); sprite = new AtlasSprite(spriteName);
this.sprites.set(spriteName, sprite); this.sprites.set(spriteName, sprite);
@ -106,6 +120,7 @@ class LoaderImpl {
if (sprite.frozen) { if (sprite.frozen) {
continue; continue;
} }
const link = new SpriteAtlasLink({ const link = new SpriteAtlasLink({
packedX: frame.x, packedX: frame.x,
packedY: frame.y, packedY: frame.y,
@ -117,9 +132,11 @@ class LoaderImpl {
w: sourceSize.w, w: sourceSize.w,
h: sourceSize.h, h: sourceSize.h,
}); });
sprite.linksByResolution[scale] = link; sprite.linksByResolution[scale] = link;
} }
} }
/** /**
* Makes the canvas which shows the question mark, shown when a sprite was not found * Makes the canvas which shows the question mark, shown when a sprite was not found
*/ */
@ -156,4 +173,5 @@ class LoaderImpl {
this.spriteNotFoundSprite = sprite; this.spriteNotFoundSprite = sprite;
} }
} }
export const Loader = new LoaderImpl(); export const Loader = new LoaderImpl();

View File

@ -8,10 +8,7 @@ Logging functions
* Base logger class * Base logger class
*/ */
class Logger { class Logger {
public context = context; constructor(public context: string) {}
constructor(context) {
}
debug(...args) { debug(...args) {
globalDebug(this.context, ...args); globalDebug(this.context, ...args);
} }
@ -25,10 +22,12 @@ class Logger {
globalError(this.context, ...args); globalError(this.context, ...args);
} }
} }
export function createLogger(context) {
export function createLogger(context: string) {
return new Logger(context); return new Logger(context);
} }
function prepareObjectForLogging(obj, maxDepth = 1) {
function prepareObjectForLogging(obj: object, maxDepth = 1) {
if (!window.Sentry) { if (!window.Sentry) {
// Not required without sentry // Not required without sentry
return obj; return obj;
@ -39,20 +38,33 @@ function prepareObjectForLogging(obj, maxDepth = 1) {
const result = {}; const result = {};
for (const key in obj) { for (const key in obj) {
const val = obj[key]; const val = obj[key];
if (typeof val === "object") { if (typeof val === "object") {
if (maxDepth > 0) { if (maxDepth > 0) {
result[key] = prepareObjectForLogging(val, maxDepth - 1); result[key] = prepareObjectForLogging(val, maxDepth - 1);
} } else {
else {
result[key] = "[object]"; result[key] = "[object]";
} }
} } else {
else {
result[key] = val; result[key] = val;
} }
} }
return result; return result;
} }
type SerializedError = {
type: string;
message?: string;
name?: string;
stack?: string;
filename?: string;
lineno?: number;
colno?: number;
error?: string | SerializedError;
};
/** /**
* Serializes an error * Serializes an error
*/ */
@ -60,17 +72,17 @@ export function serializeError(err: Error | ErrorEvent) {
if (!err) { if (!err) {
return null; return null;
} }
const result = {
const result: SerializedError = {
type: err.constructor.name, type: err.constructor.name,
}; };
if (err instanceof Error) { if (err instanceof Error) {
result.message = err.message; result.message = err.message;
result.name = err.name; result.name = err.name;
result.stack = err.stack; result.stack = err.stack;
result.type = "{type.Error}"; result.type = "{type.Error}";
} } else if (err instanceof ErrorEvent) {
else if (err instanceof ErrorEvent) {
result.filename = err.filename; result.filename = err.filename;
result.message = err.message; result.message = err.message;
result.lineno = err.lineno; result.lineno = err.lineno;
@ -78,26 +90,26 @@ export function serializeError(err: Error | ErrorEvent) {
result.type = "{type.ErrorEvent}"; result.type = "{type.ErrorEvent}";
if (err.error) { if (err.error) {
result.error = serializeError(err.error); result.error = serializeError(err.error);
} } else {
else {
result.error = "{not-provided}"; result.error = "{not-provided}";
} }
} } else {
else {
result.type = "{unkown-type:" + typeof err + "}"; result.type = "{unkown-type:" + typeof err + "}";
} }
return result; return result;
} }
/** /**
* Serializes an event * Serializes an event
*/ */
function serializeEvent(event: Event) { function serializeEvent(event: Event) {
let result = { return {
type: "{type.Event:" + typeof event + "}", type: "{type.Event:" + typeof event + "}",
eventType: event.type,
}; };
result.eventType = event.type;
return result;
} }
/** /**
* Prepares a json payload * Prepares a json payload
*/ */
@ -113,25 +125,30 @@ function preparePayload(key: string, value: any) {
} }
return value; return value;
} }
/** /**
* Stringifies an object containing circular references and errors * Stringifies an object containing circular references and errors
*/ */
export function stringifyObjectContainingErrors(payload: any) { export function stringifyObjectContainingErrors(payload: any) {
return circularJson.stringify(payload, preparePayload); return circularJson.stringify(payload, preparePayload);
} }
export function globalDebug(context, ...args) { export function globalDebug(context, ...args) {
if (G_IS_DEV) { if (G_IS_DEV) {
logInternal(context, console.log, prepareArgsForLogging(args)); logInternal(context, console.log, prepareArgsForLogging(args));
} }
} }
export function globalLog(context, ...args) { export function globalLog(context, ...args) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
logInternal(context, console.log, prepareArgsForLogging(args)); logInternal(context, console.log, prepareArgsForLogging(args));
} }
export function globalWarn(context, ...args) { export function globalWarn(context, ...args) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
logInternal(context, console.warn, prepareArgsForLogging(args)); logInternal(context, console.warn, prepareArgsForLogging(args));
} }
export function globalError(context, ...args) { export function globalError(context, ...args) {
args = prepareArgsForLogging(args); args = prepareArgsForLogging(args);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -143,66 +160,77 @@ export function globalError(context, ...args) {
}); });
} }
} }
function prepareArgsForLogging(args) {
function prepareArgsForLogging(args: any[]) {
let result = []; let result = [];
for (let i = 0; i < args.length; ++i) { for (let i = 0; i < args.length; ++i) {
result.push(prepareObjectForLogging(args[i])); result.push(prepareObjectForLogging(args[i]));
} }
return result; return result;
} }
function internalBuildStringFromArgs(args: Array<any>) { function internalBuildStringFromArgs(args: Array<any>) {
let result = []; let result = [];
for (let i = 0; i < args.length; ++i) { for (let i = 0; i < args.length; ++i) {
let arg = args[i]; let arg = args[i];
if (typeof arg === "string" || if (
typeof arg === "string" ||
typeof arg === "number" || typeof arg === "number" ||
typeof arg === "boolean" || typeof arg === "boolean" ||
arg === null || arg === null ||
arg === undefined) { arg === undefined
) {
result.push("" + arg); result.push("" + arg);
} } else if (arg instanceof Error) {
else if (arg instanceof Error) {
result.push(arg.message); result.push(arg.message);
} } else {
else {
result.push("[object]"); result.push("[object]");
} }
} }
return result.join(" "); return result.join(" ");
} }
export function logSection(name, color) {
export function logSection(name: string, color: string) {
while (name.length <= 14) { while (name.length <= 14) {
name = " " + name + " "; name = " " + name + " ";
} }
name = name.padEnd(19, " "); 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----------------------------"; const line = "%c----------------------------";
console.log("\n" + line + " %c" + name + " " + line + "\n", lineCss, "color: " + color, lineCss); 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"; let context = handle || "unknown";
if (handle && handle.constructor && handle.constructor.name) { if (handle && handle.constructor && handle.constructor.name) {
context = handle.constructor.name; context = handle.constructor.name;
if (context === "String") { if (context === "String") {
context = handle; context = handle;
} }
} }
if (handle && handle.name) {
if (handle && typeof handle !== "string" && handle.name) {
context = handle.name; context = handle.name;
} }
return context + ""; return context + "";
} }
function logInternal(handle, consoleMethod, args) { function logInternal(handle, consoleMethod, args) {
const context = extractHandleContext(handle).padEnd(20, " "); const context = extractHandleContext(handle).padEnd(20, " ");
const labelColor = handle && handle.LOG_LABEL_COLOR ? handle.LOG_LABEL_COLOR : "#aaa"; const labelColor = handle && handle.LOG_LABEL_COLOR ? handle.LOG_LABEL_COLOR : "#aaa";
if (G_IS_DEV && globalConfig.debug.logTimestamps) { if (G_IS_DEV && globalConfig.debug.logTimestamps) {
const timestamp = "⏱ %c" + (Math.floor(performance.now()) + "").padEnd(6, " ") + ""; const timestamp = "⏱ %c" + (Math.floor(performance.now()) + "").padEnd(6, " ") + "";
consoleMethod.call(console, timestamp + " %c" + context, "color: #7f7;", "color: " + labelColor + ";", ...args); consoleMethod.call(
} console,
else { timestamp + " %c" + context,
"color: #7f7;",
"color: " + labelColor + ";",
...args
);
} else {
// if (G_IS_DEV && !globalConfig.debug.disableLoggingLogSources) { // if (G_IS_DEV && !globalConfig.debug.disableLoggingLogSources) {
consoleMethod.call(console, "%c" + context, "color: " + labelColor, ...args); consoleMethod.call(console, "%c" + context, "color: " + labelColor, ...args);
// } else { // } else {

View File

@ -9,9 +9,11 @@
// LZ-based compression algorithm, version 1.4.4 // LZ-based compression algorithm, version 1.4.4
const fromCharCode = String.fromCharCode; const fromCharCode = String.fromCharCode;
const hasOwnProperty = Object.prototype.hasOwnProperty; const hasOwnProperty = Object.prototype.hasOwnProperty;
const keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"; const keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
const baseReverseDic = {}; const baseReverseDic = {};
function getBaseValue(alphabet, character) {
function getBaseValue(alphabet: string, character: string) {
if (!baseReverseDic[alphabet]) { if (!baseReverseDic[alphabet]) {
baseReverseDic[alphabet] = {}; baseReverseDic[alphabet] = {};
for (let i = 0; i < alphabet.length; i++) { for (let i = 0; i < alphabet.length; i++) {
@ -20,8 +22,9 @@ function getBaseValue(alphabet, character) {
} }
return baseReverseDic[alphabet][character]; return baseReverseDic[alphabet][character];
} }
//compress into uint8array (UCS-2 big endian format) //compress into uint8array (UCS-2 big endian format)
export function compressU8(uncompressed) { export function compressU8(uncompressed: string) {
let compressed = compress(uncompressed); let compressed = compress(uncompressed);
let buf = new Uint8Array(compressed.length * 2); // 2 bytes per character let buf = new Uint8Array(compressed.length * 2); // 2 bytes per character
for (let i = 0, TotalLen = compressed.length; i < TotalLen; i++) { for (let i = 0, TotalLen = compressed.length; i < TotalLen; i++) {
@ -31,10 +34,12 @@ export function compressU8(uncompressed) {
} }
return buf; return buf;
} }
// Compreses with header // Compreses with header
export function compressU8WHeader(uncompressed: string, header: number) { export function compressU8WHeader(uncompressed: string, header: number) {
let compressed = compress(uncompressed); let compressed = compress(uncompressed);
let buf = new Uint8Array(2 + compressed.length * 2); // 2 bytes per character let buf = new Uint8Array(2 + compressed.length * 2); // 2 bytes per character
buf[0] = header >>> 8; buf[0] = header >>> 8;
buf[1] = header % 256; buf[1] = header % 256;
for (let i = 0, TotalLen = compressed.length; i < TotalLen; i++) { for (let i = 0, TotalLen = compressed.length; i < TotalLen; i++) {
@ -44,6 +49,7 @@ export function compressU8WHeader(uncompressed: string, header: number) {
} }
return buf; return buf;
} }
//decompress from uint8array (UCS-2 big endian format) //decompress from uint8array (UCS-2 big endian format)
export function decompressU8WHeader(compressed: Uint8Array) { export function decompressU8WHeader(compressed: Uint8Array) {
// let buf = new Array(compressed.length / 2); // 2 bytes per character // let buf = new Array(compressed.length / 2); // 2 bytes per character
@ -61,46 +67,59 @@ export function decompressU8WHeader(compressed: Uint8Array) {
} }
return decompress(result.join("")); return decompress(result.join(""));
} }
//compress into a string that is already URI encoded //compress into a string that is already URI encoded
export function compressX64(input) { export function compressX64(input: string) {
if (input == null) if (input == null) return "";
return "";
return _compress(input, 6, function (a) { return _compress(input, 6, function (a) {
return keyStrUriSafe.charAt(a); return keyStrUriSafe.charAt(a);
}); });
} }
//decompress from an output of compressToEncodedURIComponent //decompress from an output of compressToEncodedURIComponent
export function decompressX64(input) { export function decompressX64(input) {
if (input == null) if (input == null) return "";
return ""; if (input == "") return null;
if (input == "")
return null;
input = input.replace(/ /g, "+"); input = input.replace(/ /g, "+");
return _decompress(input.length, 32, function (index) { return _decompress(input.length, 32, function (index) {
return getBaseValue(keyStrUriSafe, input.charAt(index)); return getBaseValue(keyStrUriSafe, input.charAt(index));
}); });
} }
function compress(uncompressed) { function compress(uncompressed) {
return _compress(uncompressed, 16, function (a) { return _compress(uncompressed, 16, function (a) {
return fromCharCode(a); return fromCharCode(a);
}); });
} }
function _compress(uncompressed, bitsPerChar, getCharFromInt) {
if (uncompressed == null) function _compress(uncompressed: string, bitsPerChar: number, getCharFromInt: (char: number) => string) {
return ""; 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 let i,
context_dictSize = 3, context_numBits = 2, context_data = [], context_data_val = 0, context_data_position = 0, ii; 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) { for (ii = 0; ii < uncompressed.length; ii += 1) {
context_c = uncompressed.charAt(ii); context_c = uncompressed.charAt(ii);
if (!hasOwnProperty.call(context_dictionary, context_c)) { if (!hasOwnProperty.call(context_dictionary, context_c)) {
context_dictionary[context_c] = context_dictSize++; context_dictionary[context_c] = context_dictSize++;
context_dictionaryToCreate[context_c] = true; context_dictionaryToCreate[context_c] = true;
} }
context_wc = context_w + context_c; context_wc = context_w + context_c;
if (hasOwnProperty.call(context_dictionary, context_wc)) { if (hasOwnProperty.call(context_dictionary, context_wc)) {
context_w = context_wc; context_w = context_wc;
} } else {
else {
if (hasOwnProperty.call(context_dictionaryToCreate, context_w)) { if (hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
if (context_w.charCodeAt(0) < 256) { if (context_w.charCodeAt(0) < 256) {
for (i = 0; i < context_numBits; i++) { for (i = 0; i < context_numBits; i++) {
@ -109,8 +128,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_data_position = 0; context_data_position = 0;
context_data.push(getCharFromInt(context_data_val)); context_data.push(getCharFromInt(context_data_val));
context_data_val = 0; context_data_val = 0;
} } else {
else {
context_data_position++; context_data_position++;
} }
} }
@ -121,14 +139,12 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_data_position = 0; context_data_position = 0;
context_data.push(getCharFromInt(context_data_val)); context_data.push(getCharFromInt(context_data_val));
context_data_val = 0; context_data_val = 0;
} } else {
else {
context_data_position++; context_data_position++;
} }
value = value >> 1; value = value >> 1;
} }
} } else {
else {
value = 1; value = 1;
for (i = 0; i < context_numBits; i++) { for (i = 0; i < context_numBits; i++) {
context_data_val = (context_data_val << 1) | value; context_data_val = (context_data_val << 1) | value;
@ -136,8 +152,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_data_position = 0; context_data_position = 0;
context_data.push(getCharFromInt(context_data_val)); context_data.push(getCharFromInt(context_data_val));
context_data_val = 0; context_data_val = 0;
} } else {
else {
context_data_position++; context_data_position++;
} }
value = 0; value = 0;
@ -149,8 +164,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_data_position = 0; context_data_position = 0;
context_data.push(getCharFromInt(context_data_val)); context_data.push(getCharFromInt(context_data_val));
context_data_val = 0; context_data_val = 0;
} } else {
else {
context_data_position++; context_data_position++;
} }
value = value >> 1; value = value >> 1;
@ -162,8 +176,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_numBits++; context_numBits++;
} }
delete context_dictionaryToCreate[context_w]; delete context_dictionaryToCreate[context_w];
} } else {
else {
value = context_dictionary[context_w]; value = context_dictionary[context_w];
for (i = 0; i < context_numBits; i++) { for (i = 0; i < context_numBits; i++) {
context_data_val = (context_data_val << 1) | (value & 1); context_data_val = (context_data_val << 1) | (value & 1);
@ -171,8 +184,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_data_position = 0; context_data_position = 0;
context_data.push(getCharFromInt(context_data_val)); context_data.push(getCharFromInt(context_data_val));
context_data_val = 0; context_data_val = 0;
} } else {
else {
context_data_position++; context_data_position++;
} }
value = value >> 1; value = value >> 1;
@ -198,8 +210,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_data_position = 0; context_data_position = 0;
context_data.push(getCharFromInt(context_data_val)); context_data.push(getCharFromInt(context_data_val));
context_data_val = 0; context_data_val = 0;
} } else {
else {
context_data_position++; context_data_position++;
} }
} }
@ -210,14 +221,12 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_data_position = 0; context_data_position = 0;
context_data.push(getCharFromInt(context_data_val)); context_data.push(getCharFromInt(context_data_val));
context_data_val = 0; context_data_val = 0;
} } else {
else {
context_data_position++; context_data_position++;
} }
value = value >> 1; value = value >> 1;
} }
} } else {
else {
value = 1; value = 1;
for (i = 0; i < context_numBits; i++) { for (i = 0; i < context_numBits; i++) {
context_data_val = (context_data_val << 1) | value; context_data_val = (context_data_val << 1) | value;
@ -225,8 +234,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_data_position = 0; context_data_position = 0;
context_data.push(getCharFromInt(context_data_val)); context_data.push(getCharFromInt(context_data_val));
context_data_val = 0; context_data_val = 0;
} } else {
else {
context_data_position++; context_data_position++;
} }
value = 0; value = 0;
@ -238,8 +246,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_data_position = 0; context_data_position = 0;
context_data.push(getCharFromInt(context_data_val)); context_data.push(getCharFromInt(context_data_val));
context_data_val = 0; context_data_val = 0;
} } else {
else {
context_data_position++; context_data_position++;
} }
value = value >> 1; value = value >> 1;
@ -251,8 +258,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_numBits++; context_numBits++;
} }
delete context_dictionaryToCreate[context_w]; delete context_dictionaryToCreate[context_w];
} } else {
else {
value = context_dictionary[context_w]; value = context_dictionary[context_w];
for (i = 0; i < context_numBits; i++) { for (i = 0; i < context_numBits; i++) {
context_data_val = (context_data_val << 1) | (value & 1); context_data_val = (context_data_val << 1) | (value & 1);
@ -260,8 +266,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_data_position = 0; context_data_position = 0;
context_data.push(getCharFromInt(context_data_val)); context_data.push(getCharFromInt(context_data_val));
context_data_val = 0; context_data_val = 0;
} } else {
else {
context_data_position++; context_data_position++;
} }
value = value >> 1; value = value >> 1;
@ -273,6 +278,7 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_numBits++; context_numBits++;
} }
} }
// Mark the end of the stream // Mark the end of the stream
value = 2; value = 2;
for (i = 0; i < context_numBits; i++) { for (i = 0; i < context_numBits; i++) {
@ -281,12 +287,12 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
context_data_position = 0; context_data_position = 0;
context_data.push(getCharFromInt(context_data_val)); context_data.push(getCharFromInt(context_data_val));
context_data_val = 0; context_data_val = 0;
} } else {
else {
context_data_position++; context_data_position++;
} }
value = value >> 1; value = value >> 1;
} }
// Flush the last char // Flush the last char
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (true) {
@ -294,26 +300,39 @@ function _compress(uncompressed, bitsPerChar, getCharFromInt) {
if (context_data_position == bitsPerChar - 1) { if (context_data_position == bitsPerChar - 1) {
context_data.push(getCharFromInt(context_data_val)); context_data.push(getCharFromInt(context_data_val));
break; break;
} } else context_data_position++;
else
context_data_position++;
} }
return context_data.join(""); return context_data.join("");
} }
function decompress(compressed) {
if (compressed == null) function decompress(compressed: string) {
return ""; if (compressed == null) return "";
if (compressed == "") if (compressed == "") return null;
return null;
return _decompress(compressed.length, 32768, function (index) { return _decompress(compressed.length, 32768, function (index) {
return compressed.charCodeAt(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) { for (i = 0; i < 3; i += 1) {
dictionary[i] = i; dictionary[i] = i;
} }
bits = 0; bits = 0;
maxpower = Math.pow(2, 2); maxpower = Math.pow(2, 2);
power = 1; power = 1;
@ -327,6 +346,7 @@ function _decompress(length, resetValue, getNextValue) {
bits |= (resb > 0 ? 1 : 0) * power; bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1; power <<= 1;
} }
switch ((next = bits)) { switch ((next = bits)) {
case 0: case 0:
bits = 0; bits = 0;
@ -366,6 +386,7 @@ function _decompress(length, resetValue, getNextValue) {
dictionary[3] = c; dictionary[3] = c;
w = c; w = c;
result.push(c); result.push(c);
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (true) {
if (data.index > length) { if (data.index > length) {
@ -424,23 +445,24 @@ function _decompress(length, resetValue, getNextValue) {
case 2: case 2:
return result.join(""); return result.join("");
} }
if (enlargeIn == 0) { if (enlargeIn == 0) {
enlargeIn = Math.pow(2, numBits); enlargeIn = Math.pow(2, numBits);
numBits++; numBits++;
} }
if (dictionary[c]) { if (dictionary[c]) {
// @ts-ignore // @ts-ignore
entry = dictionary[c]; entry = dictionary[c];
} } else {
else {
if (c === dictSize) { if (c === dictSize) {
entry = w + w.charAt(0); entry = w + w.charAt(0);
} } else {
else {
return null; return null;
} }
} }
result.push(entry); result.push(entry);
// Add w+entry[0] to the dictionary. // Add w+entry[0] to the dictionary.
dictionary[dictSize++] = w + entry.charAt(0); dictionary[dictSize++] = w + entry.charAt(0);
enlargeIn--; enlargeIn--;

View File

@ -1,9 +1,7 @@
/* typehints:start */
import type { Application } from "../application"; import type { Application } from "../application";
/* typehints:end */
import { Signal, STOP_PROPAGATION } from "./signal"; import { Signal, STOP_PROPAGATION } from "./signal";
import { arrayDeleteValue, waitNextFrame } from "./utils"; import { arrayDeleteValue, waitNextFrame } from "./utils";
import { ClickDetector } from "./click_detector"; import { ClickDetector, ClickDetectorConstructorArgs } from "./click_detector";
import { SOUNDS } from "../platform/sound"; import { SOUNDS } from "../platform/sound";
import { InputReceiver } from "./input_receiver"; import { InputReceiver } from "./input_receiver";
import { FormElement } from "./modal_dialog_forms"; import { FormElement } from "./modal_dialog_forms";
@ -11,6 +9,7 @@ import { globalConfig } from "./config";
import { getStringForKeyCode } from "../game/key_action_mapper"; import { getStringForKeyCode } from "../game/key_action_mapper";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { T } from "../translations"; import { T } from "../translations";
/* /*
* *************************************************** * ***************************************************
* *
@ -21,33 +20,74 @@ import { T } from "../translations";
* *
* *************************************************** * ***************************************************
*/ */
const kbEnter = 13; const kbEnter = 13;
const kbCancel = 27; const kbCancel = 27;
const logger = createLogger("dialogs"); 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 * Basic text based dialog
*/ */
export class Dialog { export class Dialog<Buttons extends string[] = []> {
public app = app; public app: Application;
public title = title; public title: string;
public contentHTML = contentHTML; public contentHTML: string;
public type = type; public type: string;
public buttonIds = buttons; public buttonIds: string[];
public closeButton = closeButton; public closeButton: boolean;
public closeRequested = new Signal(); public closeRequested = new Signal();
public buttonSignals = {}; public buttonSignals: {
[key in Buttons[number]]: Signal<any[]>;
} = {} as any;
public valueChosen = new Signal(); public valueChosen = new Signal();
public timeouts = []; public timeouts = [];
public clickDetectors = []; public clickDetectors = [];
public inputReciever = new InputReceiver("dialog-" + this.title); public inputReciever: InputReceiver;
public enterHandler = null; public enterHandler = null;
public escapeHandler = null; public escapeHandler = null;
public dialogElem: HTMLDivElement;
public element: HTMLDivElement;
/** /**
* *
* Constructs a new dialog with the given options * 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) { for (let i = 0; i < buttons.length; ++i) {
if (G_IS_DEV && globalConfig.debug.disableTimedButtons) { if (G_IS_DEV && globalConfig.debug.disableTimedButtons) {
this.buttonIds[i] = this.buttonIds[i].replace(":timeout", ""); this.buttonIds[i] = this.buttonIds[i].replace(":timeout", "");
@ -57,76 +97,100 @@ export class Dialog {
} }
this.inputReciever.keydown.add(this.handleKeydown, this); this.inputReciever.keydown.add(this.handleKeydown, this);
} }
/** /**
* Internal keydown handler * Internal keydown handler
*/ */
handleKeydown({ keyCode, shift, alt, ctrl }: { handleKeydown({
keyCode,
shift,
alt,
ctrl,
}: {
keyCode: number; keyCode: number;
shift: boolean; shift: boolean;
alt: boolean; alt: boolean;
ctrl: boolean; ctrl: boolean;
}) { }): void | STOP_PROPAGATION {
if (keyCode === kbEnter && this.enterHandler) { if (keyCode === kbEnter && this.enterHandler) {
this.internalButtonHandler(this.enterHandler); this.internalButtonHandler(this.enterHandler);
return STOP_PROPAGATION; return STOP_PROPAGATION;
} }
if (keyCode === kbCancel && this.escapeHandler) { if (keyCode === kbCancel && this.escapeHandler) {
this.internalButtonHandler(this.escapeHandler); this.internalButtonHandler(this.escapeHandler);
return STOP_PROPAGATION; return STOP_PROPAGATION;
} }
} }
internalButtonHandler(id, ...payload) {
internalButtonHandler(id: string, ...payload: any[]) {
this.app.inputMgr.popReciever(this.inputReciever); this.app.inputMgr.popReciever(this.inputReciever);
if (id !== "close-button") { if (id !== "close-button") {
this.buttonSignals[id].dispatch(...payload); this.buttonSignals[id].dispatch(...payload);
} }
this.closeRequested.dispatch(); this.closeRequested.dispatch();
} }
createElement() { createElement() {
const elem = document.createElement("div"); const elem = document.createElement("div");
elem.classList.add("ingameDialog"); elem.classList.add("ingameDialog");
this.dialogElem = document.createElement("div"); this.dialogElem = document.createElement("div");
this.dialogElem.classList.add("dialogInner"); this.dialogElem.classList.add("dialogInner");
if (this.type) { if (this.type) {
this.dialogElem.classList.add(this.type); this.dialogElem.classList.add(this.type);
} }
elem.appendChild(this.dialogElem); elem.appendChild(this.dialogElem);
const title = document.createElement("h1"); const title = document.createElement("h1");
title.innerText = this.title; title.innerText = this.title;
title.classList.add("title"); title.classList.add("title");
this.dialogElem.appendChild(title); this.dialogElem.appendChild(title);
if (this.closeButton) { if (this.closeButton) {
this.dialogElem.classList.add("hasCloseButton"); this.dialogElem.classList.add("hasCloseButton");
const closeBtn = document.createElement("button"); const closeBtn = document.createElement("button");
closeBtn.classList.add("closeButton"); closeBtn.classList.add("closeButton");
this.trackClicks(closeBtn, () => this.internalButtonHandler("close-button"), { this.trackClicks(closeBtn, () => this.internalButtonHandler("close-button"), {
applyCssClass: "pressedSmallElement", applyCssClass: "pressedSmallElement",
}); });
title.appendChild(closeBtn); title.appendChild(closeBtn);
this.inputReciever.backButton.add(() => this.internalButtonHandler("close-button")); this.inputReciever.backButton.add(() => this.internalButtonHandler("close-button"));
} }
const content = document.createElement("div"); const content = document.createElement("div");
content.classList.add("content"); content.classList.add("content");
content.innerHTML = this.contentHTML; content.innerHTML = this.contentHTML;
this.dialogElem.appendChild(content); this.dialogElem.appendChild(content);
if (this.buttonIds.length > 0) { if (this.buttonIds.length > 0) {
const buttons = document.createElement("div"); const buttons = document.createElement("div");
buttons.classList.add("buttons"); buttons.classList.add("buttons");
// Create buttons // Create buttons
for (let i = 0; i < this.buttonIds.length; ++i) { for (let i = 0; i < this.buttonIds.length; ++i) {
const [buttonId, buttonStyle, rawParams] = this.buttonIds[i].split(":"); const [buttonId, buttonStyle, rawParams] = this.buttonIds[i].split(":");
const button = document.createElement("button"); const button = document.createElement("button");
button.classList.add("button"); button.classList.add("button");
button.classList.add("styledButton"); button.classList.add("styledButton");
button.classList.add(buttonStyle); button.classList.add(buttonStyle);
button.innerText = T.dialogs.buttons[buttonId]; button.innerText = T.dialogs.buttons[buttonId];
const params = (rawParams || "").split("/"); const params = (rawParams || "").split("/");
const useTimeout = params.indexOf("timeout") >= 0; const useTimeout = params.indexOf("timeout") >= 0;
const isEnter = params.indexOf("enter") >= 0; const isEnter = params.indexOf("enter") >= 0;
const isEscape = params.indexOf("escape") >= 0; const isEscape = params.indexOf("escape") >= 0;
if (isEscape && this.closeButton) { if (isEscape && this.closeButton) {
logger.warn("Showing dialog with close button, and additional cancel button"); logger.warn("Showing dialog with close button, and additional cancel button");
} }
if (useTimeout) { if (useTimeout) {
button.classList.add("timedButton"); button.classList.add("timedButton");
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
@ -143,6 +207,7 @@ export class Dialog {
spacer.innerHTML = getStringForKeyCode(isEnter ? kbEnter : kbCancel); spacer.innerHTML = getStringForKeyCode(isEnter ? kbEnter : kbCancel);
button.appendChild(spacer); button.appendChild(spacer);
// } // }
if (isEnter) { if (isEnter) {
this.enterHandler = buttonId; this.enterHandler = buttonId;
} }
@ -150,21 +215,26 @@ export class Dialog {
this.escapeHandler = buttonId; this.escapeHandler = buttonId;
} }
} }
this.trackClicks(button, () => this.internalButtonHandler(buttonId)); this.trackClicks(button, () => this.internalButtonHandler(buttonId));
buttons.appendChild(button); buttons.appendChild(button);
} }
this.dialogElem.appendChild(buttons); this.dialogElem.appendChild(buttons);
} } else {
else {
this.dialogElem.classList.add("buttonless"); this.dialogElem.classList.add("buttonless");
} }
this.element = elem; this.element = elem;
this.app.inputMgr.pushReciever(this.inputReciever); this.app.inputMgr.pushReciever(this.inputReciever);
return this.element; return this.element;
} }
setIndex(index) { setIndex(index) {
this.element.style.zIndex = index; this.element.style.zIndex = index;
} }
destroy() { destroy() {
if (!this.element) { if (!this.element) {
assert(false, "Tried to destroy dialog twice"); 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, // dispatched to the modal dialogs, it will not call the internalButtonHandler,
// and thus our receiver stays attached the whole time // and thus our receiver stays attached the whole time
this.app.inputMgr.destroyReceiver(this.inputReciever); this.app.inputMgr.destroyReceiver(this.inputReciever);
for (let i = 0; i < this.clickDetectors.length; ++i) { for (let i = 0; i < this.clickDetectors.length; ++i) {
this.clickDetectors[i].cleanup(); this.clickDetectors[i].cleanup();
} }
this.clickDetectors = []; this.clickDetectors = [];
this.element.remove(); this.element.remove();
this.element = null; this.element = null;
for (let i = 0; i < this.timeouts.length; ++i) { for (let i = 0; i < this.timeouts.length; ++i) {
clearTimeout(this.timeouts[i]); clearTimeout(this.timeouts[i]);
} }
this.timeouts = []; this.timeouts = [];
} }
hide() { hide() {
this.element.classList.remove("visible"); this.element.classList.remove("visible");
} }
show() { show() {
this.element.classList.add("visible"); this.element.classList.add("visible");
} }
/** /**
* Helper method to track clicks on an element * 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); const detector = new ClickDetector(elem, args);
detector.click.add(handler, this); detector.click.add(handler, this);
this.clickDetectors.push(detector); this.clickDetectors.push(detector);
return detector; return detector;
} }
} }
/** /**
* Dialog which simply shows a loading spinner * Dialog which simply shows a loading spinner
*/ */
export class DialogLoading extends Dialog { export class DialogLoading extends Dialog {
public text = text; public text: string;
constructor(app, text = "") { constructor(app, text = "") {
super({ super({
@ -216,46 +293,85 @@ export class DialogLoading extends Dialog {
buttons: [], buttons: [],
type: "loading", type: "loading",
}); });
// Loading dialog can not get closed with back button // Loading dialog can not get closed with back button
this.inputReciever.backButton.removeAll(); this.inputReciever.backButton.removeAll();
this.inputReciever.context = "dialog-loading"; this.inputReciever.context = "dialog-loading";
this.text = text;
} }
createElement() { createElement() {
const elem = document.createElement("div"); const elem = document.createElement("div");
elem.classList.add("ingameDialog"); elem.classList.add("ingameDialog");
elem.classList.add("loadingDialog"); elem.classList.add("loadingDialog");
this.element = elem; this.element = elem;
if (this.text) { if (this.text) {
const text = document.createElement("div"); const text = document.createElement("div");
text.classList.add("text"); text.classList.add("text");
text.innerText = this.text; text.innerText = this.text;
elem.appendChild(text); elem.appendChild(text);
} }
const loader = document.createElement("div"); const loader = document.createElement("div");
loader.classList.add("prefab_LoadingTextWithAnim"); loader.classList.add("prefab_LoadingTextWithAnim");
loader.classList.add("loadingIndicator"); loader.classList.add("loadingIndicator");
elem.appendChild(loader); elem.appendChild(loader);
this.app.inputMgr.pushReciever(this.inputReciever); this.app.inputMgr.pushReciever(this.inputReciever);
return elem; 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'>"; let html = "<div class='optionParent'>";
options.options.forEach(({ value, text, desc = null, iconPrefix = null }) => { options.options.forEach(({ value, text, desc = null, iconPrefix = null }) => {
const descHtml = desc ? `<span class="desc">${desc}</span>` : ""; const descHtml = desc ? `<span class="desc">${desc}</span>` : "";
let iconHtml = iconPrefix ? `<span class="icon icon-${iconPrefix}-${value}"></span>` : ""; let iconHtml = iconPrefix ? `<span class="icon icon-${iconPrefix}-${value}"></span>` : "";
html += ` 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} ${iconHtml}
<span class='title'>${text}</span> <span class='title'>${text}</span>
${descHtml} ${descHtml}
</div> </div>
`; `;
}); });
html += "</div>"; html += "</div>";
super({ super({
app, app,
@ -265,11 +381,16 @@ export class DialogOptionChooser extends Dialog {
type: "info", type: "info",
closeButton: true, closeButton: true,
}); });
this.options = options;
this.initialOption = options.active;
this.buttonSignals.optionSelected = new Signal(); this.buttonSignals.optionSelected = new Signal();
} }
createElement() { createElement() {
const div = super.createElement(); const div = super.createElement();
this.dialogElem.classList.add("optionChooserDialog"); this.dialogElem.classList.add("optionChooserDialog");
div.querySelectorAll("[data-optionvalue]").forEach(handle => { div.querySelectorAll("[data-optionvalue]").forEach(handle => {
const value = handle.getAttribute("data-optionvalue"); const value = handle.getAttribute("data-optionvalue");
if (!handle) { if (!handle) {
@ -285,13 +406,13 @@ export class DialogOptionChooser extends Dialog {
targetOnly: true, targetOnly: true,
}); });
this.clickDetectors.push(detector); this.clickDetectors.push(detector);
if (value !== this.initialOption) { if (value !== this.initialOption) {
detector.click.add(() => { detector.click.add(() => {
const selected = div.querySelector(".option.active"); const selected = div.querySelector(".option.active");
if (selected) { if (selected) {
selected.classList.remove("active"); selected.classList.remove("active");
} } else {
else {
logger.warn("No selected option"); logger.warn("No selected option");
} }
handle.classList.add("active"); handle.classList.add("active");
@ -303,27 +424,51 @@ export class DialogOptionChooser extends Dialog {
return div; 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 = ""; let html = "";
html += desc + "<br>"; html += desc + "<br>";
for (let i = 0; i < formElements.length; ++i) { for (let i = 0; i < formElements.length; ++i) {
html += formElements[i].getHtml(); html += formElements[i].getHtml();
} }
super({ super({
app, app,
title: title, title: title,
contentHTML: html, contentHTML: html,
buttons: buttons, buttons: buttons as any,
type: "info", type: "info",
closeButton, closeButton,
}); });
this.confirmButtonId = confirmButtonId;
this.formElements = formElements;
this.enterHandler = confirmButtonId;
} }
internalButtonHandler(id, ...payload) {
internalButtonHandler(id: string, ...payload) {
if (id === this.confirmButtonId) { if (id === this.confirmButtonId) {
if (this.hasAnyInvalid()) { if (this.hasAnyInvalid()) {
this.dialogElem.classList.remove("errorShake"); this.dialogElem.classList.remove("errorShake");
@ -336,8 +481,10 @@ export class DialogWithForm extends Dialog {
return; return;
} }
} }
super.internalButtonHandler(id, payload); super.internalButtonHandler(id, payload);
} }
hasAnyInvalid() { hasAnyInvalid() {
for (let i = 0; i < this.formElements.length; ++i) { for (let i = 0; i < this.formElements.length; ++i) {
if (!this.formElements[i].isValid()) { if (!this.formElements[i].isValid()) {
@ -346,6 +493,7 @@ export class DialogWithForm extends Dialog {
} }
return false; return false;
} }
createElement() { createElement() {
const div = super.createElement(); const div = super.createElement();
for (let i = 0; i < this.formElements.length; ++i) { for (let i = 0; i < this.formElements.length; ++i) {

View File

@ -11,42 +11,58 @@ import { Signal } from "./signal";
* *
* *************************************************** * ***************************************************
*/ */
export class FormElement { export abstract class FormElement {
public id = id;
public label = label;
public valueChosen = new Signal(); 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; abstract bindEvents(parent: Element, clickTrackers: ClickDetector[]);
return "";
} focus() {}
getFormElement(parent) {
return parent.querySelector("[data-formId='" + this.id + "']");
}
bindEvents(parent, clickTrackers) {
abstract;
}
focus() { }
isValid() { isValid() {
return true; 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); super(id, label);
this.placeholder = placeholder;
this.defaultValue = defaultValue;
this.inputType = inputType;
this.validator = validator;
} }
getHtml() { getHtml() {
let classes = []; let classes = [];
let inputType = "text"; let inputType = "text";
@ -85,37 +101,46 @@ export class FormElementInput extends FormElement {
</div> </div>
`; `;
} }
bindEvents(parent, clickTrackers) { bindEvents(parent, clickTrackers) {
this.element = this.getFormElement(parent); this.element = this.getFormElement(parent);
this.element.addEventListener("input", event => this.updateErrorState()); this.element.addEventListener("input", event => this.updateErrorState());
this.updateErrorState(); this.updateErrorState();
} }
updateErrorState() { updateErrorState() {
this.element.classList.toggle("errored", !this.isValid()); this.element.classList.toggle("errored", !this.isValid());
} }
isValid() { isValid() {
return !this.validator || this.validator(this.element.value); return !this.validator || this.validator(this.element.value);
} }
getValue() { getValue() {
return this.element.value; return this.element.value;
} }
setValue(value) { setValue(value) {
this.element.value = value; this.element.value = value;
this.updateErrorState(); this.updateErrorState();
} }
focus() { focus() {
this.element.focus(); this.element.focus();
this.element.select(); this.element.select();
} }
} }
export class FormElementCheckbox extends FormElement { export class FormElementCheckbox extends FormElement {
public defaultValue = defaultValue; public defaultValue: boolean;
public value = this.defaultValue; public value: boolean;
public element = null; public element: Element = null;
constructor({ id, label, defaultValue = true }) { constructor({ id, label, defaultValue = true }: { id: string; label: string; defaultValue?: boolean }) {
super(id, label); super(id, label);
this.defaultValue = defaultValue;
this.value = this.defaultValue;
} }
getHtml() { getHtml() {
return ` return `
<div class="formElement checkBoxFormElem"> <div class="formElement checkBoxFormElem">
@ -126,7 +151,8 @@ export class FormElementCheckbox extends FormElement {
</div> </div>
`; `;
} }
bindEvents(parent, clickTrackers) {
bindEvents(parent: Element, clickTrackers: ClickDetector[]) {
this.element = this.getFormElement(parent); this.element = this.getFormElement(parent);
const detector = new ClickDetector(this.element, { const detector = new ClickDetector(this.element, {
consumeEvents: false, consumeEvents: false,
@ -135,23 +161,30 @@ export class FormElementCheckbox extends FormElement {
clickTrackers.push(detector); clickTrackers.push(detector);
detector.click.add(this.toggle, this); detector.click.add(this.toggle, this);
} }
getValue() { getValue() {
return this.value; return this.value;
} }
toggle() { toggle() {
this.value = !this.value; this.value = !this.value;
this.element.classList.toggle("checked", 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); super(id, label);
this.items = items;
} }
getHtml() { getHtml() {
let classes = []; let classes = [];
return ` return `
@ -161,10 +194,13 @@ export class FormElementItemChooser extends FormElement {
</div> </div>
`; `;
} }
bindEvents(parent: HTMLElement, clickTrackers: Array<ClickDetector>) {
bindEvents(parent: HTMLElement, clickTrackers: Array<ClickDetector>) {
this.element = this.getFormElement(parent); this.element = this.getFormElement(parent);
for (let i = 0; i < this.items.length; ++i) { for (let i = 0; i < this.items.length; ++i) {
const item = this.items[i]; const item = this.items[i];
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = 128; canvas.width = 128;
canvas.height = 128; canvas.height = 128;
@ -172,6 +208,7 @@ export class FormElementItemChooser extends FormElement {
item.drawFullSizeOnCanvas(context, 128); item.drawFullSizeOnCanvas(context, 128);
this.element.appendChild(canvas); this.element.appendChild(canvas);
const detector = new ClickDetector(canvas, {}); const detector = new ClickDetector(canvas, {});
clickTrackers.push(detector); clickTrackers.push(detector);
detector.click.add(() => { detector.click.add(() => {
this.chosenItem = item; this.chosenItem = item;
@ -179,11 +216,14 @@ export class FormElementItemChooser extends FormElement {
}); });
} }
} }
isValid() { isValid() {
return true; return true;
} }
getValue() { getValue() {
return null; return null;
} }
focus() { }
focus() {}
} }

View File

@ -17,8 +17,7 @@ function stringPolyfills() {
padString = String(typeof padString !== "undefined" ? padString : " "); padString = String(typeof padString !== "undefined" ? padString : " ");
if (this.length >= targetLength) { if (this.length >= targetLength) {
return String(this); return String(this);
} } else {
else {
targetLength = targetLength - this.length; targetLength = targetLength - this.length;
if (targetLength > padString.length) { if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed 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 : " "); padString = String(typeof padString !== "undefined" ? padString : " ");
if (this.length > targetLength) { if (this.length > targetLength) {
return String(this); return String(this);
} } else {
else {
targetLength = targetLength - this.length; targetLength = targetLength - this.length;
if (targetLength > padString.length) { if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed 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) { if (!Object.values) {
// @ts-ignore // @ts-ignore
Object.values = function values(O) { 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) { if (!Object.entries) {
// @ts-ignore // @ts-ignore
Object.entries = function entries(O) { 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]]] : []),
[]
);
}; };
} }
} }

View File

@ -1,5 +1,6 @@
const queryString = require("query-string"); const queryString = require("query-string");
const options = queryString.parse(location.search); const options = queryString.parse(location.search);
export let queryParamOptions = { export let queryParamOptions = {
embedProvider: null, embedProvider: null,
abtVariant: null, abtVariant: null,

View File

@ -1,6 +1,5 @@
/* typehints:start */
import type { Application } from "../application"; import type { Application } from "../application";
/* typehints:end */
import { sha1, CRC_PREFIX, computeCrc } from "./sensitive_utils.encrypt"; import { sha1, CRC_PREFIX, computeCrc } from "./sensitive_utils.encrypt";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { FILE_NOT_FOUND } from "../platform/storage"; import { FILE_NOT_FOUND } from "../platform/storage";
@ -14,55 +13,53 @@ const debounce = require("debounce-promise");
const logger = createLogger("read_write_proxy"); const logger = createLogger("read_write_proxy");
const salt = accessNestedPropertyReverse(globalConfig, ["file", "info"]); const salt = accessNestedPropertyReverse(globalConfig, ["file", "info"]);
// Helper which only writes / reads if verify() works. Also performs migration // Helper which only writes / reads if verify() works. Also performs migration
export class ReadWriteProxy {
public app: Application = app; export abstract class ReadWriteProxy {
public filename = filename;
public currentData: object = null; public currentData: object = null;
public debouncedWrite = debounce(this.doWriteAsync.bind(this), 50); 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 // TODO: EXTREMELY HACKY! To verify we need to do this a step later
if (G_IS_DEV && IS_DEBUG) { if (G_IS_DEV && IS_DEBUG) {
setTimeout(() => { 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 // -- Methods to override
/** {} */ abstract verify(data): ExplainedResult;
verify(data): ExplainedResult {
abstract;
return ExplainedResult.bad();
}
// Should return the default data // Should return the default data
getDefaultData() { abstract getDefaultData(): object;
abstract;
return {};
}
// Should return the current version as an integer // Should return the current version as an integer
getCurrentVersion() { abstract getCurrentVersion(): number;
abstract;
return 0;
}
// Should migrate the data (Modify in place) // Should migrate the data (Modify in place)
/** {} */ abstract migrate(data: object): ExplainedResult;
migrate(data): ExplainedResult {
abstract;
return ExplainedResult.bad();
}
// -- / Methods // -- / Methods
// Resets whole data, returns promise // Resets whole data, returns promise
resetEverythingAsync() { resetEverythingAsync() {
logger.warn("Reset data to default"); logger.warn("Reset data to default");
this.currentData = this.getDefaultData(); this.currentData = this.getDefaultData();
return this.writeAsync(); return this.writeAsync();
} }
static serializeObject(obj: object) {
static serializeObject(obj: object) {
const jsonString = JSON.stringify(compressObject(obj)); const jsonString = JSON.stringify(compressObject(obj));
const checksum = computeCrc(jsonString + salt); const checksum = computeCrc(jsonString + salt);
return compressionPrefix + compressX64(checksum + jsonString); 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)); const decompressed = decompressX64(text.substr(compressionPrefix.length));
if (!decompressed) { if (!decompressed) {
// LZ string decompression failure // LZ string decompression failure
@ -72,154 +69,185 @@ export class ReadWriteProxy {
// String too short // String too short
throw new Error("bad-content / payload-too-small"); throw new Error("bad-content / payload-too-small");
} }
// Compare stored checksum with actual checksum // Compare stored checksum with actual checksum
const checksum = decompressed.substring(0, 40); const checksum = decompressed.substring(0, 40);
const jsonString = decompressed.substr(40); const jsonString = decompressed.substr(40);
const desiredChecksum = checksum.startsWith(CRC_PREFIX) const desiredChecksum = checksum.startsWith(CRC_PREFIX)
? computeCrc(jsonString + salt) ? computeCrc(jsonString + salt)
: sha1(jsonString + salt); : sha1(jsonString + salt);
if (desiredChecksum !== checksum) { if (desiredChecksum !== checksum) {
// Checksum mismatch // Checksum mismatch
throw new Error("bad-content / checksum-mismatch"); throw new Error("bad-content / checksum-mismatch");
} }
const parsed = JSON.parse(jsonString); const parsed = JSON.parse(jsonString);
const decoded = decompressObject(parsed); const decoded = decompressObject(parsed);
return decoded; return decoded;
} }
/** /**
* Writes the data asychronously, fails if verify() fails. * Writes the data asychronously, fails if verify() fails.
* Debounces the operation by up to 50ms * Debounces the operation by up to 50ms
* {}
*/ */
writeAsync(): Promise<void> { writeAsync(): Promise<void> {
const verifyResult = this.internalVerifyEntry(this.currentData); const verifyResult = this.internalVerifyEntry(this.currentData);
if (!verifyResult.result) { if (!verifyResult.result) {
logger.error("Tried to write invalid data to", this.filename, "reason:", verifyResult.reason); logger.error("Tried to write invalid data to", this.filename, "reason:", verifyResult.reason);
return Promise.reject(verifyResult.reason); return Promise.reject(verifyResult.reason);
} }
return this.debouncedWrite(); return this.debouncedWrite();
} }
/** /**
* Actually writes the data asychronously * Actually writes the data asychronously
* {}
*/ */
doWriteAsync(): Promise<void> { doWriteAsync(): Promise<void> {
return asyncCompressor return asyncCompressor
.compressObjectAsync(this.currentData) .compressObjectAsync(this.currentData)
.then(compressed => { .then(compressed => {
return this.app.storage.writeFileAsync(this.filename, compressed); return this.app.storage.writeFileAsync(this.filename, compressed);
}) })
.then(() => { .then(() => {
logger.log("📄 Wrote", this.filename); logger.log("📄 Wrote", this.filename);
}) })
.catch(err => { .catch(err => {
logger.error("Failed to write", this.filename, ":", err); logger.error("Failed to write", this.filename, ":", err);
throw err; throw err;
}); });
} }
// Reads the data asynchronously, fails if verify() fails // Reads the data asynchronously, fails if verify() fails
readAsync() { readAsync() {
// Start read request // Start read request
return (this.app.storage return (
.readFileAsync(this.filename) this.app.storage
// Check for errors during read .readFileAsync(this.filename)
.catch(err => {
if (err === FILE_NOT_FOUND) { // Check for errors during read
logger.log("File not found, using default data"); .catch(err => {
// File not found or unreadable, assume default file if (err === FILE_NOT_FOUND) {
return Promise.resolve(null); logger.log("File not found, using default data");
} // File not found or unreadable, assume default file
return Promise.reject("file-error: " + err); return Promise.resolve(null);
}) }
// Decrypt data (if its encrypted) return Promise.reject("file-error: " + err);
// @ts-ignore })
.then(rawData => {
if (rawData == null) { // Decrypt data (if its encrypted)
// So, the file has not been found, use default data // @ts-ignore
return JSON.stringify(compressObject(this.getDefaultData())); .then(rawData => {
} if (rawData == null) {
if (rawData.startsWith(compressionPrefix)) { // So, the file has not been found, use default data
const decompressed = decompressX64(rawData.substr(compressionPrefix.length)); return JSON.stringify(compressObject(this.getDefaultData()));
if (!decompressed) { }
// LZ string decompression failure if (rawData.startsWith(compressionPrefix)) {
return Promise.reject("bad-content / decompression-failed"); const decompressed = decompressX64(rawData.substr(compressionPrefix.length));
} if (!decompressed) {
if (decompressed.length < 40) { // LZ string decompression failure
// String too short return Promise.reject("bad-content / decompression-failed");
return Promise.reject("bad-content / payload-too-small"); }
} if (decompressed.length < 40) {
// Compare stored checksum with actual checksum // String too short
const checksum = decompressed.substring(0, 40); return Promise.reject("bad-content / payload-too-small");
const jsonString = decompressed.substr(40); }
const desiredChecksum = checksum.startsWith(CRC_PREFIX) // Compare stored checksum with actual checksum
? computeCrc(jsonString + salt) const checksum = decompressed.substring(0, 40);
: sha1(jsonString + salt); const jsonString = decompressed.substr(40);
if (desiredChecksum !== checksum) { const desiredChecksum = checksum.startsWith(CRC_PREFIX)
// Checksum mismatch ? computeCrc(jsonString + salt)
return Promise.reject("bad-content / checksum-mismatch: " + desiredChecksum + " vs " + checksum); : sha1(jsonString + salt);
} if (desiredChecksum !== checksum) {
return jsonString; // Checksum mismatch
} return Promise.reject(
else { "bad-content / checksum-mismatch: " + desiredChecksum + " vs " + checksum
if (!G_IS_DEV) { );
return Promise.reject("bad-content / missing-compression"); }
} return jsonString;
} } else {
return rawData; if (!G_IS_DEV) {
}) return Promise.reject("bad-content / missing-compression");
// Parse JSON, this could throw but that's fine }
.then(res => { }
try { return rawData;
return JSON.parse(res); })
} // Parse JSON, this could throw but that's fine
catch (ex) { .then(res => {
logger.error("Failed to parse file content of", this.filename, ":", ex, "(content was:", res, ")"); try {
throw new Error("invalid-serialized-data"); return JSON.parse(res);
} } catch (ex) {
}) logger.error(
// Decompress "Failed to parse file content of",
.then(compressed => decompressObject(compressed)) this.filename,
// Verify basic structure ":",
.then(contents => { ex,
const result = this.internalVerifyBasicStructure(contents); "(content was:",
if (!result.isGood()) { res,
return Promise.reject("verify-failed: " + result.reason); ")"
} );
return contents; throw new Error("invalid-serialized-data");
}) }
// Check version and migrate if required })
.then(contents => { // Decompress
if (contents.version > this.getCurrentVersion()) { .then(compressed => decompressObject(compressed))
return Promise.reject("stored-data-is-newer"); // Verify basic structure
} .then(contents => {
if (contents.version < this.getCurrentVersion()) { const result = this.internalVerifyBasicStructure(contents);
logger.log("Trying to migrate data object from version", contents.version, "to", this.getCurrentVersion()); if (!result.isGood()) {
const migrationResult = this.migrate(contents); // modify in place return Promise.reject("verify-failed: " + result.reason);
if (migrationResult.isBad()) { }
return Promise.reject("migration-failed: " + migrationResult.reason); return contents;
} })
} // Check version and migrate if required
return contents; .then(contents => {
}) if (contents.version > this.getCurrentVersion()) {
// Verify return Promise.reject("stored-data-is-newer");
.then(contents => { }
const verifyResult = this.internalVerifyEntry(contents); if (contents.version < this.getCurrentVersion()) {
if (!verifyResult.result) { logger.log(
logger.error("Read invalid data from", this.filename, "reason:", verifyResult.reason, "contents:", contents); "Trying to migrate data object from version",
return Promise.reject("invalid-data: " + verifyResult.reason); contents.version,
} "to",
return contents; this.getCurrentVersion()
}) );
// Store const migrationResult = this.migrate(contents); // modify in place
.then(contents => { if (migrationResult.isBad()) {
this.currentData = contents; return Promise.reject("migration-failed: " + migrationResult.reason);
logger.log("📄 Read data with version", this.currentData.version, "from", this.filename); }
return contents; }
}) return contents;
// Catchall })
.catch(err => { // Verify
return Promise.reject("Failed to read " + this.filename + ": " + err); .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 * Deletes the file
@ -235,14 +263,18 @@ export class ReadWriteProxy {
return ExplainedResult.bad("Data is empty"); return ExplainedResult.bad("Data is empty");
} }
if (!Number.isInteger(data.version) || data.version < 0) { 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(); return ExplainedResult.good();
} }
/** {} */ /** {} */
internalVerifyEntry(data): ExplainedResult { internalVerifyEntry(data): ExplainedResult {
if (data.version !== this.getCurrentVersion()) { 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); const verifyStructureError = this.internalVerifyBasicStructure(data);
if (!verifyStructureError.isGood()) { if (!verifyStructureError.isGood()) {

View File

@ -2,119 +2,109 @@ import { globalConfig } from "./config";
import { epsilonCompare, round2Digits } from "./utils"; import { epsilonCompare, round2Digits } from "./utils";
import { Vector } from "./vector"; import { Vector } from "./vector";
export class Rectangle { export class Rectangle {
public x = x; constructor(public x: number = 0, public y: number = 0, public w: number = 0, public h: number = 0) {}
public y = y;
public w = w;
public h = h;
constructor(x = 0, y = 0, w = 0, h = 0) {
}
/** /**
* Creates a rectangle from top right bottom and left offsets * Creates a rectangle from top right bottom and left offsets
*/ */
static fromTRBL(top: number, right: number, bottom: number, left: number) { static fromTRBL(top: number, right: number, bottom: number, left: number) {
return new Rectangle(left, top, right - left, bottom - top); return new Rectangle(left, top, right - left, bottom - top);
} }
/** /**
* Constructs a new square rectangle * Constructs a new square rectangle
*/ */
static fromSquare(x: number, y: number, size: number) { static fromSquare(x: number, y: number, size: number) {
return new Rectangle(x, y, size, size); 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 left = Math.min(p1.x, p2.x);
const top = Math.min(p1.y, p2.y); const top = Math.min(p1.y, p2.y);
const right = Math.max(p1.x, p2.x); const right = Math.max(p1.x, p2.x);
const bottom = Math.max(p1.y, p2.y); const bottom = Math.max(p1.y, p2.y);
return new Rectangle(left, top, right - left, bottom - top); 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); return new Rectangle(-Math.ceil(width / 2), -Math.ceil(height / 2), width, height);
} }
/** /**
* Returns if a intersects b * Returns if a intersects b
*/ */
static intersects(a: Rectangle, b: Rectangle) { static intersects(a: Rectangle, b: Rectangle) {
return a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom; return a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom;
} }
/** /**
* Copies this instance * Copies this instance
* {}
*/ */
clone(): Rectangle { clone(): Rectangle {
return new Rectangle(this.x, this.y, this.w, this.h); return new Rectangle(this.x, this.y, this.w, this.h);
} }
/** /**
* Returns if this rectangle is empty * Returns if this rectangle is empty
* {}
*/ */
isEmpty(): boolean { isEmpty(): boolean {
return epsilonCompare(this.w * this.h, 0); return epsilonCompare(this.w * this.h, 0);
} }
/** /**
* Returns if this rectangle is equal to the other while taking an epsilon into account * Returns if this rectangle is equal to the other while taking an epsilon into account
*/ */
equalsEpsilon(other: Rectangle, epsilon: number) { 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.y, other.y, epsilon) &&
epsilonCompare(this.w, other.w, epsilon) && epsilonCompare(this.w, other.w, epsilon) &&
epsilonCompare(this.h, other.h, epsilon)); epsilonCompare(this.h, other.h, epsilon)
);
} }
/**
* {}
*/
left(): number { left(): number {
return this.x; return this.x;
} }
/**
* {}
*/
right(): number { right(): number {
return this.x + this.w; return this.x + this.w;
} }
/**
* {}
*/
top(): number { top(): number {
return this.y; return this.y;
} }
/**
* {}
*/
bottom(): number { bottom(): number {
return this.y + this.h; return this.y + this.h;
} }
/** /**
* Returns Top, Right, Bottom, Left * Returns Top, Right, Bottom, Left
* {}
*/ */
trbl(): [ trbl(): [number, number, number, number] {
number,
number,
number,
number
] {
return [this.y, this.right(), this.bottom(), this.x]; return [this.y, this.right(), this.bottom(), this.x];
} }
/** /**
* Returns the center of the rect * Returns the center of the rect
* {}
*/ */
getCenter(): Vector { getCenter(): Vector {
return new Vector(this.x + this.w / 2, this.y + this.h / 2); return new Vector(this.x + this.w / 2, this.y + this.h / 2);
} }
/** /**
* Sets the right side of the rect without moving it * Sets the right side of the rect without moving it
*/ */
setRight(right: number) { setRight(right: number) {
this.w = right - this.x; this.w = right - this.x;
} }
/** /**
* Sets the bottom side of the rect without moving it * Sets the bottom side of the rect without moving it
*/ */
setBottom(bottom: number) { setBottom(bottom: number) {
this.h = bottom - this.y; this.h = bottom - this.y;
} }
/** /**
* Sets the top side of the rect without scaling it * Sets the top side of the rect without scaling it
*/ */
@ -123,6 +113,7 @@ export class Rectangle {
this.y = top; this.y = top;
this.setBottom(bottom); this.setBottom(bottom);
} }
/** /**
* Sets the left side of the rect without scaling it * Sets the left side of the rect without scaling it
*/ */
@ -131,6 +122,7 @@ export class Rectangle {
this.x = left; this.x = left;
this.setRight(right); this.setRight(right);
} }
/** /**
* Returns the top left point * Returns the top left point
* {} * {}
@ -138,6 +130,7 @@ export class Rectangle {
topLeft(): Vector { topLeft(): Vector {
return new Vector(this.x, this.y); return new Vector(this.x, this.y);
} }
/** /**
* Returns the bottom left point * Returns the bottom left point
* {} * {}
@ -145,6 +138,7 @@ export class Rectangle {
bottomRight(): Vector { bottomRight(): Vector {
return new Vector(this.right(), this.bottom()); return new Vector(this.right(), this.bottom());
} }
/** /**
* Moves the rectangle by the given parameters * Moves the rectangle by the given parameters
*/ */
@ -152,6 +146,7 @@ export class Rectangle {
this.x += x; this.x += x;
this.y += y; this.y += y;
} }
/** /**
* Moves the rectangle by the given vector * Moves the rectangle by the given vector
*/ */
@ -159,6 +154,7 @@ export class Rectangle {
this.x += vec.x; this.x += vec.x;
this.y += vec.y; this.y += vec.y;
} }
/** /**
* Scales every parameter (w, h, x, y) by the given factor. Useful to transform from world to * Scales every parameter (w, h, x, y) by the given factor. Useful to transform from world to
* tile space and vice versa * tile space and vice versa
@ -166,50 +162,55 @@ export class Rectangle {
allScaled(factor: number) { allScaled(factor: number) {
return new Rectangle(this.x * factor, this.y * factor, this.w * factor, this.h * factor); return new Rectangle(this.x * factor, this.y * factor, this.w * factor, this.h * factor);
} }
/** /**
* Expands the rectangle in all directions * Expands the rectangle in all directions
* {} new rectangle * @returns new rectangle
*/ */
expandedInAllDirections(amount: number): Rectangle { expandedInAllDirections(amount: number): Rectangle {
return new Rectangle(this.x - amount, this.y - amount, this.w + 2 * amount, this.h + 2 * amount); return new Rectangle(this.x - amount, this.y - amount, this.w + 2 * amount, this.h + 2 * amount);
} }
/** /**
* Returns if the given rectangle is contained * Returns if the given rectangle is contained
* {}
*/ */
containsRect(rect: Rectangle): boolean { containsRect(rect: Rectangle): boolean {
return (this.x <= rect.right() && return (
this.x <= rect.right() &&
rect.x <= this.right() && rect.x <= this.right() &&
this.y <= rect.bottom() && this.y <= rect.bottom() &&
rect.y <= this.bottom()); rect.y <= this.bottom()
);
} }
/** /**
* Returns if this rectangle contains the other rectangle specified by the parameters * Returns if this rectangle contains the other rectangle specified by the parameters
* {}
*/ */
containsRect4Params(x: number, y: number, w: number, h: number): boolean { 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(); 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) * Returns if the rectangle contains the given circle at (x, y) with the radius (radius)
* {}
*/ */
containsCircle(x: number, y: number, radius: number): boolean { containsCircle(x: number, y: number, radius: number): boolean {
return (this.x <= x + radius && return (
this.x <= x + radius &&
x - radius <= this.right() && x - radius <= this.right() &&
this.y <= y + radius && this.y <= y + radius &&
y - radius <= this.bottom()); y - radius <= this.bottom()
);
} }
/** /**
* Returns if the rectangle contains the given point * Returns if the rectangle contains the given point
* {}
*/ */
containsPoint(x: number, y: number): boolean { containsPoint(x: number, y: number): boolean {
return x >= this.x && x < this.right() && y >= this.y && y < this.bottom(); 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 * Returns the shared area with another rectangle, or null if there is no intersection
* {}
*/ */
getIntersection(rect: Rectangle): Rectangle | null { getIntersection(rect: Rectangle): Rectangle | null {
const left = Math.max(this.x, rect.x); const left = Math.max(this.x, rect.x);
@ -221,6 +222,7 @@ export class Rectangle {
} }
return Rectangle.fromTRBL(top, right, bottom, left); return Rectangle.fromTRBL(top, right, bottom, left);
} }
/** /**
* Returns whether the rectangle fully intersects the given rectangle * Returns whether the rectangle fully intersects the given rectangle
*/ */
@ -228,6 +230,7 @@ export class Rectangle {
const intersection = this.getIntersection(rect); const intersection = this.getIntersection(rect);
return intersection && Math.abs(intersection.w * intersection.h - rect.w * rect.h) < 0.001; return intersection && Math.abs(intersection.w * intersection.h - rect.w * rect.h) < 0.001;
} }
/** /**
* Returns the union of this rectangle with another * Returns the union of this rectangle with another
*/ */
@ -247,23 +250,28 @@ export class Rectangle {
const bottom = Math.max(this.bottom(), rect.bottom()); const bottom = Math.max(this.bottom(), rect.bottom());
return Rectangle.fromTRBL(top, right, bottom, left); return Rectangle.fromTRBL(top, right, bottom, left);
} }
/** /**
* Good for caching stuff * Good for caching stuff
*/ */
toCompareableString() { toCompareableString() {
return (round2Digits(this.x) + return (
round2Digits(this.x) +
"/" + "/" +
round2Digits(this.y) + round2Digits(this.y) +
"/" + "/" +
round2Digits(this.w) + round2Digits(this.w) +
"/" + "/" +
round2Digits(this.h)); round2Digits(this.h)
);
} }
/** /**
* Good for printing stuff * Good for printing stuff
*/ */
toString() { toString() {
return ("[x:" + return (
"[x:" +
round2Digits(this.x) + round2Digits(this.x) +
"| y:" + "| y:" +
round2Digits(this.y) + round2Digits(this.y) +
@ -271,13 +279,19 @@ export class Rectangle {
round2Digits(this.w) + round2Digits(this.w) +
"| h:" + "| h:" +
round2Digits(this.h) + round2Digits(this.h) +
"]"); "]"
);
} }
/** /**
* Returns a new rectangle in tile space which includes all tiles which are visible in this rect * Returns a new rectangle in tile space which includes all tiles which are visible in this rect
* {}
*/ */
toTileCullRectangle(): Rectangle { 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)
);
} }
} }

View File

@ -4,42 +4,40 @@ const logger = createLogger("request_channel");
// Thrown when a request is aborted // Thrown when a request is aborted
export const PROMISE_ABORTED = "promise-aborted"; export const PROMISE_ABORTED = "promise-aborted";
export class RequestChannel { export class RequestChannel {
public pendingPromises: Array<Promise> = []; public pendingPromises: Array<Promise<any>> = [];
constructor() {
}
/**
*
* {}
*/
watch(promise: Promise<any>): Promise<any> { watch(promise: Promise<any>): Promise<any> {
// log(this, "Added new promise:", promise, "(pending =", this.pendingPromises.length, ")"); // log(this, "Added new promise:", promise, "(pending =", this.pendingPromises.length, ")");
let cancelled = false; let cancelled = false;
const wrappedPromise = new Promise((resolve, reject) => { const wrappedPromise = new Promise((resolve, reject) => {
promise.then(result => { promise.then(
// Remove from pending promises result => {
fastArrayDeleteValueIfContained(this.pendingPromises, wrappedPromise); // Remove from pending promises
// If not cancelled, resolve promise with same payload fastArrayDeleteValueIfContained(this.pendingPromises, wrappedPromise);
if (!cancelled) {
resolve.call(this, result); // 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 // Add cancel handler
// @ts-ignore // @ts-ignore
wrappedPromise.cancel = function () { wrappedPromise.cancel = function () {
@ -48,6 +46,7 @@ export class RequestChannel {
this.pendingPromises.push(wrappedPromise); this.pendingPromises.push(wrappedPromise);
return wrappedPromise; return wrappedPromise;
} }
cancelAll() { cancelAll() {
if (this.pendingPromises.length > 0) { if (this.pendingPromises.length > 0) {
logger.log("Cancel all pending promises (", this.pendingPromises.length, ")"); logger.log("Cancel all pending promises (", this.pendingPromises.length, ")");

View File

@ -7,24 +7,24 @@ import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso";
export class RestrictionManager extends ReadWriteProxy { export class RestrictionManager extends ReadWriteProxy {
public currentData = this.getDefaultData(); public currentData = this.getDefaultData();
constructor(app) { constructor(app) {
super(app, "restriction-flags.bin"); super(app, "restriction-flags.bin");
} }
// -- RW Proxy Impl // -- RW Proxy Impl
verify(data: any) { verify(data: any) {
return ExplainedResult.good(); return ExplainedResult.good();
} }
getDefaultData() { getDefaultData() {
return { return {
version: this.getCurrentVersion(), version: this.getCurrentVersion(),
}; };
} }
getCurrentVersion() { getCurrentVersion() {
return 1; return 1;
} }
migrate(data: any) { migrate(data: any) {
return ExplainedResult.good(); return ExplainedResult.good();
} }
initialize() { initialize() {

View File

@ -16,6 +16,7 @@ function Mash() {
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32 return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
}; };
} }
function makeNewRng(seed: number | string) { function makeNewRng(seed: number | string) {
// Johannes Baagøe <baagoe@baagoe.com>, 2010 // Johannes Baagøe <baagoe@baagoe.com>, 2010
var c = 1; var c = 1;
@ -23,6 +24,7 @@ function makeNewRng(seed: number | string) {
let s0 = mash(" "); let s0 = mash(" ");
let s1 = mash(" "); let s1 = mash(" ");
let s2 = mash(" "); let s2 = mash(" ");
s0 -= mash(seed); s0 -= mash(seed);
if (s0 < 0) { if (s0 < 0) {
s0 += 1; s0 += 1;
@ -36,49 +38,59 @@ function makeNewRng(seed: number | string) {
s2 += 1; s2 += 1;
} }
mash = null; mash = null;
var random = function () { var random = function () {
var t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32 var t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
s0 = s1; s0 = s1;
s1 = s2; s1 = s2;
return (s2 = t - (c = t | 0)); return (s2 = t - (c = t | 0));
}; };
random.exportState = function () { random.exportState = function () {
return [s0, s1, s2, c]; return [s0, s1, s2, c];
}; };
random.importState = function (i) { random.importState = function (i) {
s0 = +i[0] || 0; s0 = +i[0] || 0;
s1 = +i[1] || 0; s1 = +i[1] || 0;
s2 = +i[2] || 0; s2 = +i[2] || 0;
c = +i[3] || 0; c = +i[3] || 0;
}; };
return random; 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 * Re-seeds the generator
*/ */
reseed(seed: number | string) { reseed(seed: number | string) {
this.internalRng = makeNewRng(seed || Math.random()); this.internalRng = makeNewRng(seed || Math.random());
} }
/** /**
* {} between 0 and 1 * @returns between 0 and 1
*/ */
next(): number { next(): number {
return this.internalRng(); return this.internalRng();
} }
/** /**
* Random choice of an array * Random choice of an array
*/ */
choice(array: array) { choice(array: any[]) {
const index = this.nextIntRange(0, array.length); const index = this.nextIntRange(0, array.length);
return array[index]; return array[index];
} }
/** /**
* {} Integer in range [min, max[ * @returns Integer in range [min, max[]
*/ */
nextIntRange(min: number, max: number): number { nextIntRange(min: number, max: number): number {
assert(Number.isFinite(min), "Minimum is no integer"); assert(Number.isFinite(min), "Minimum is no integer");
@ -86,13 +98,15 @@ export class RandomNumberGenerator {
assert(max > min, "rng: max <= min"); assert(max > min, "rng: max <= min");
return Math.floor(this.next() * (max - min) + 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 { nextRange(min: number, max: number): number {
assert(max > min, "rng: max <= min"); assert(max > min, "rng: max <= min");
return this.next() * (max - min) + min; return this.next() * (max - min) + min;
} }
/** /**
* Updates the seed * Updates the seed
*/ */

View File

@ -1,15 +1,19 @@
import { createHash } from "rusha"; import { createHash } from "rusha";
import crc32 from "crc/crc32"; import crc32 from "crc/crc32";
import { decompressX64 } from "./lzstring"; import { decompressX64 } from "./lzstring";
export function sha1(str) { export function sha1(str) {
return createHash().update(str).digest("hex"); return createHash().update(str).digest("hex");
} }
// Window.location.host // Window.location.host
export function getNameOfProvider() { export function getNameOfProvider() {
return window[decompressX64("DYewxghgLgliB2Q")][decompressX64("BYewzgLgdghgtgUyA")]; return window[decompressX64("DYewxghgLgliB2Q")][decompressX64("BYewzgLgdghgtgUyA")];
} }
// Distinguish legacy crc prefixes // Distinguish legacy crc prefixes
export const CRC_PREFIX = "crc32".padEnd(32, "-"); export const CRC_PREFIX = "crc32".padEnd(32, "-");
/** /**
* Computes the crc for a given string * Computes the crc for a given string
*/ */

View File

@ -1,48 +1,55 @@
export const STOP_PROPAGATION = "stop_propagation"; export const STOP_PROPAGATION = "stop_propagation";
export class Signal { export type STOP_PROPAGATION = typeof STOP_PROPAGATION;
public receivers = [];
export class Signal<T extends any[]> {
public receivers: {
receiver: (...args: T) => STOP_PROPAGATION | void;
scope: object;
}[] = [];
public modifyCount = 0; public modifyCount = 0;
constructor() {
}
/** /**
* Adds a new signal listener * 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"); assert(receiver, "receiver is null");
this.receivers.push({ receiver, scope }); this.receivers.push({ receiver, scope });
++this.modifyCount; ++this.modifyCount;
} }
/** /**
* Adds a new signal listener * 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"); assert(receiver, "receiver is null");
this.receivers.unshift({ receiver, scope }); this.receivers.unshift({ receiver, scope });
++this.modifyCount; ++this.modifyCount;
} }
/** /**
* Dispatches the signal * Dispatches the signal
* @param {} payload
*/ */
dispatch() { dispatch(...payload: T): void | STOP_PROPAGATION {
const modifyState = this.modifyCount; const modifyState = this.modifyCount;
const n = this.receivers.length; const n = this.receivers.length;
for (let i = 0; i < n; ++i) { for (let i = 0; i < n; ++i) {
const { receiver, scope } = this.receivers[i]; const { receiver, scope } = this.receivers[i];
if (receiver.apply(scope, arguments) === STOP_PROPAGATION) { if (receiver.apply(scope, payload) === STOP_PROPAGATION) {
return STOP_PROPAGATION; return STOP_PROPAGATION;
} }
if (modifyState !== this.modifyCount) { if (modifyState !== this.modifyCount) {
// Signal got modified during iteration // Signal got modified during iteration
return STOP_PROPAGATION; return STOP_PROPAGATION;
} }
} }
} }
/** /**
* Removes a receiver * Removes a receiver
*/ */
remove(receiver: function) { remove(receiver: (...args: any[]) => any) {
let index = null; let index = null;
const n = this.receivers.length; const n = this.receivers.length;
for (let i = 0; i < n; ++i) { for (let i = 0; i < n; ++i) {
@ -55,6 +62,7 @@ export class Signal {
this.receivers.splice(index, 1); this.receivers.splice(index, 1);
++this.modifyCount; ++this.modifyCount;
} }
/** /**
* Removes all receivers * Removes all receivers
*/ */

View File

@ -1,40 +1,46 @@
import { createLogger } from "./logging"; import { createLogger } from "./logging";
const logger = createLogger("singleton_factory"); const logger = createLogger("singleton_factory");
// simple factory pattern // simple factory pattern
export class SingletonFactory { export class SingletonFactory<T extends { getId(): string }> {
public id = id; public entries: T[] = [];
public entries = []; public idToEntry: {
public idToEntry = {}; [id: string]: T;
} = {};
constructor(public id?: string) {}
constructor(id) {
}
getId() { getId() {
return this.id; return this.id;
} }
register(classHandle) {
register(classHandle: Class<T>) {
// First, construct instance // First, construct instance
const instance = new classHandle(); const instance = new classHandle();
// Extract id // Extract id
const id = instance.getId(); const id = instance.getId();
assert(id, "Factory: Invalid id for class " + classHandle.name + ": " + id); assert(id, "Factory: Invalid id for class " + classHandle.name + ": " + id);
// Check duplicates // Check duplicates
assert(!this.idToEntry[id], "Duplicate factory entry for " + id); assert(!this.idToEntry[id], "Duplicate factory entry for " + id);
// Insert // Insert
this.entries.push(instance); this.entries.push(instance);
this.idToEntry[id] = instance; this.idToEntry[id] = instance;
} }
/** /**
* Checks if a given id is registered * Checks if a given id is registered
* {}
*/ */
hasId(id: string): boolean { hasId(id: string): boolean {
return !!this.idToEntry[id]; return !!this.idToEntry[id];
} }
/** /**
* Finds an instance by a given id * Finds an instance by a given id
* {}
*/ */
findById(id: string): object { findById(id: string): T {
const entry = this.idToEntry[id]; const entry = this.idToEntry[id];
if (!entry) { if (!entry) {
logger.error("Object with id", id, "is not registered!"); logger.error("Object with id", id, "is not registered!");
@ -43,12 +49,11 @@ export class SingletonFactory {
} }
return entry; return entry;
} }
/**
/**
* Finds an instance by its constructor (The class handle) * 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) { for (let i = 0; i < this.entries.length; ++i) {
if (this.entries[i] instanceof classHandle) { if (this.entries[i] instanceof classHandle) {
return this.entries[i]; return this.entries[i];
@ -57,23 +62,23 @@ export class SingletonFactory {
assert(false, "Factory: Object not found by classHandle (classid: " + classHandle.name + ")"); assert(false, "Factory: Object not found by classHandle (classid: " + classHandle.name + ")");
return null; return null;
} }
/** /**
* Returns all entries * Returns all entries
* {}
*/ */
getEntries(): Array<object> { getEntries(): Array<T> {
return this.entries; return this.entries;
} }
/** /**
* Returns all registered ids * Returns all registered ids
* {}
*/ */
getAllIds(): Array<string> { getAllIds(): Array<string> {
return Object.keys(this.idToEntry); return Object.keys(this.idToEntry);
} }
/** /**
* Returns amount of stored entries * Returns amount of stored entries
* {}
*/ */
getNumEntries(): number { getNumEntries(): number {
return this.entries.length; return this.entries.length;

View File

@ -1,181 +1,288 @@
import { DrawParameters } from "./draw_parameters"; import { DrawParameters } from "./draw_parameters";
import { Rectangle } from "./rectangle"; import { Rectangle } from "./rectangle";
import { round3Digits } from "./utils"; import { round3Digits } from "./utils";
export const ORIGINAL_SPRITE_SCALE = "0.75"; export const ORIGINAL_SPRITE_SCALE = "0.75";
export const FULL_CLIP_RECT = new Rectangle(0, 0, 1, 1); export const FULL_CLIP_RECT = new Rectangle(0, 0, 1, 1);
const EXTRUDE = 0.1; const EXTRUDE = 0.1;
export class BaseSprite { export abstract class BaseSprite {
/** /**
* Returns the raw handle * Returns the raw handle
* {}
* @abstract
*/ */
getRawTexture(): HTMLImageElement | HTMLCanvasElement { abstract getRawTexture(): HTMLImageElement | HTMLCanvasElement;
abstract;
return null;
}
/** /**
* Draws the sprite * Draws the sprite
*/ */
draw(context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) { abstract draw(context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number);
// eslint-disable-line no-unused-vars
abstract;
}
} }
/** /**
* Position of a sprite within an atlas * Position of a sprite within an atlas
*/ */
export class SpriteAtlasLink { export class SpriteAtlasLink {
public packedX = packedX; public packedX: number;
public packedY = packedY; public packedY: number;
public packedW = packedW; public packedW: number;
public packedH = packedH; public packedH: number;
public packOffsetX = packOffsetX; public packOffsetX: number;
public packOffsetY = packOffsetY; public packOffsetY: number;
public atlas = atlas; public atlas: HTMLImageElement | HTMLCanvasElement;
public w = w; public w: number;
public h = h; 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 { export class AtlasSprite extends BaseSprite {
public linksByResolution: { public linksByResolution: {
[idx: string]: SpriteAtlasLink; [idx: string]: SpriteAtlasLink;
} = {}; } = {};
public spriteName = spriteName;
public frozen = false; public frozen = false;
constructor(spriteName = "sprite") { constructor(public spriteName: string = "sprite") {
super(); super();
} }
getRawTexture() { getRawTexture() {
return this.linksByResolution[ORIGINAL_SPRITE_SCALE].atlas; return this.linksByResolution[ORIGINAL_SPRITE_SCALE].atlas;
} }
/** /**
* Draws the sprite onto a regular context using no contexts * Draws the sprite onto a regular context using no contexts
* @see {BaseSprite.draw} * @see {BaseSprite.draw}
*/ */
draw(context, x, y, w, h) { draw(context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) {
if (G_IS_DEV) { if (G_IS_DEV) {
assert(context instanceof CanvasRenderingContext2D, "Not a valid context"); assert(context instanceof CanvasRenderingContext2D, "Not a valid context");
} }
const link = this.linksByResolution[ORIGINAL_SPRITE_SCALE]; const link = this.linksByResolution[ORIGINAL_SPRITE_SCALE];
if (!link) { if (!link) {
throw new Error("draw: Link for " + throw new Error(
this.spriteName + "draw: Link for " +
" not known: " + this.spriteName +
ORIGINAL_SPRITE_SCALE + " not known: " +
" (having " + ORIGINAL_SPRITE_SCALE +
Object.keys(this.linksByResolution) + " (having " +
")"); Object.keys(this.linksByResolution) +
")"
);
} }
const width = w || link.w; const width = w || link.w;
const height = h || link.h; const height = h || link.h;
const scaleW = width / link.w; const scaleW = width / link.w;
const scaleH = height / link.h; 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); 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); this.draw(context, x - size / 2, y - size / 2, size, size);
} }
/** /**
* Draws the sprite * 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) { if (G_IS_DEV) {
assert(parameters instanceof DrawParameters, "Not a valid context"); assert(parameters instanceof DrawParameters, "Not a valid context");
assert(!!w && w > 0, "Not a valid width:" + w); assert(!!w && w > 0, "Not a valid width:" + w);
assert(!!h && h > 0, "Not a valid height:" + h); assert(!!h && h > 0, "Not a valid height:" + h);
} }
const visibleRect = parameters.visibleRect; const visibleRect = parameters.visibleRect;
const scale = parameters.desiredAtlasScale; const scale = parameters.desiredAtlasScale;
const link = this.linksByResolution[scale]; const link = this.linksByResolution[scale];
if (!link) { if (!link) {
throw new Error("drawCached: Link for " + throw new Error(
this.spriteName + "drawCached: Link for " +
" at scale " + this.spriteName +
scale + " at scale " +
" not known (having " + scale +
Object.keys(this.linksByResolution) + " not known (having " +
")"); Object.keys(this.linksByResolution) +
")"
);
} }
const scaleW = w / link.w; const scaleW = w / link.w;
const scaleH = h / link.h; const scaleH = h / link.h;
let destX = x + link.packOffsetX * scaleW; let destX = x + link.packOffsetX * scaleW;
let destY = y + link.packOffsetY * scaleH; let destY = y + link.packOffsetY * scaleH;
let destW = link.packedW * scaleW; let destW = link.packedW * scaleW;
let destH = link.packedH * scaleH; let destH = link.packedH * scaleH;
let srcX = link.packedX; let srcX = link.packedX;
let srcY = link.packedY; let srcY = link.packedY;
let srcW = link.packedW; let srcW = link.packedW;
let srcH = link.packedH; let srcH = link.packedH;
let intersection = null; let intersection = null;
if (clipping) { if (clipping) {
const rect = new Rectangle(destX, destY, destW, destH); const rect = new Rectangle(destX, destY, destW, destH);
intersection = rect.getIntersection(visibleRect); intersection = rect.getIntersection(visibleRect);
if (!intersection) { if (!intersection) {
return; return;
} }
srcX += (intersection.x - destX) / scaleW; srcX += (intersection.x - destX) / scaleW;
srcY += (intersection.y - destY) / scaleH; srcY += (intersection.y - destY) / scaleH;
srcW *= intersection.w / destW; srcW *= intersection.w / destW;
srcH *= intersection.h / destH; srcH *= intersection.h / destH;
destX = intersection.x; destX = intersection.x;
destY = intersection.y; destY = intersection.y;
destW = intersection.w; destW = intersection.w;
destH = intersection.h; destH = intersection.h;
} }
parameters.context.drawImage(link.atlas,
// atlas src pos parameters.context.drawImage(
srcX, srcY, link.atlas,
// atlas src size // atlas src pos
srcW, srcH, srcX,
// dest pos and size srcY,
destX - EXTRUDE, destY - EXTRUDE, destW + 2 * EXTRUDE, destH + 2 * EXTRUDE); // 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 * 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) { if (G_IS_DEV) {
assert(parameters instanceof DrawParameters, "Not a valid context"); assert(parameters instanceof DrawParameters, "Not a valid context");
assert(!!w && w > 0, "Not a valid width:" + w); assert(!!w && w > 0, "Not a valid width:" + w);
assert(!!h && h > 0, "Not a valid height:" + h); assert(!!h && h > 0, "Not a valid height:" + h);
assert(clipRect, "No clip rect given!"); assert(clipRect, "No clip rect given!");
} }
const scale = parameters.desiredAtlasScale; const scale = parameters.desiredAtlasScale;
const link = this.linksByResolution[scale]; const link = this.linksByResolution[scale];
if (!link) { if (!link) {
throw new Error("drawCachedWithClipRect: Link for " + throw new Error(
this.spriteName + "drawCachedWithClipRect: Link for " +
" at scale " + this.spriteName +
scale + " at scale " +
" not known (having " + scale +
Object.keys(this.linksByResolution) + " not known (having " +
")"); Object.keys(this.linksByResolution) +
")"
);
} }
const scaleW = w / link.w; const scaleW = w / link.w;
const scaleH = h / link.h; const scaleH = h / link.h;
let destX = x + link.packOffsetX * scaleW + clipRect.x * w; let destX = x + link.packOffsetX * scaleW + clipRect.x * w;
let destY = y + link.packOffsetY * scaleH + clipRect.y * h; let destY = y + link.packOffsetY * scaleH + clipRect.y * h;
let destW = link.packedW * scaleW * clipRect.w; let destW = link.packedW * scaleW * clipRect.w;
let destH = link.packedH * scaleH * clipRect.h; let destH = link.packedH * scaleH * clipRect.h;
let srcX = link.packedX + clipRect.x * link.packedW; let srcX = link.packedX + clipRect.x * link.packedW;
let srcY = link.packedY + clipRect.y * link.packedH; let srcY = link.packedY + clipRect.y * link.packedH;
let srcW = link.packedW * clipRect.w; let srcW = link.packedW * clipRect.w;
let srcH = link.packedH * clipRect.h; let srcH = link.packedH * clipRect.h;
parameters.context.drawImage(link.atlas,
// atlas src pos parameters.context.drawImage(
srcX, srcY, link.atlas,
// atlas src siize // atlas src pos
srcW, srcH, srcX,
// dest pos and size srcY,
destX - EXTRUDE, destY - EXTRUDE, destW + 2 * EXTRUDE, destH + 2 * EXTRUDE); // atlas src siize
srcW,
srcH,
// dest pos and size
destX - EXTRUDE,
destY - EXTRUDE,
destW + 2 * EXTRUDE,
destH + 2 * EXTRUDE
);
} }
/** /**
* Renders into an html element * Renders into an html element
*/ */
@ -183,44 +290,56 @@ export class AtlasSprite extends BaseSprite {
element.style.position = "relative"; element.style.position = "relative";
element.innerHTML = this.getAsHTML(w, h); element.innerHTML = this.getAsHTML(w, h);
} }
/** /**
* Returns the html to render as icon * Returns the html to render as icon
*/ */
getAsHTML(w: number, h: number) { getAsHTML(w: number, h: number) {
const link = this.linksByResolution["0.5"]; const link = this.linksByResolution["0.5"];
if (!link) { if (!link) {
throw new Error("getAsHTML: Link for " + throw new Error(
this.spriteName + "getAsHTML: Link for " +
" at scale 0.5" + this.spriteName +
" not known (having " + " at scale 0.5" +
Object.keys(this.linksByResolution) + " not known (having " +
")"); Object.keys(this.linksByResolution) +
")"
);
} }
// Find out how much we have to scale it so that it fits // Find out how much we have to scale it so that it fits
const scaleX = w / link.w; const scaleX = w / link.w;
const scaleY = h / link.h; const scaleY = h / link.h;
// Find out how big the scaled atlas is // Find out how big the scaled atlas is
const atlasW = link.atlas.width * scaleX; const atlasW = link.atlas.width * scaleX;
const atlasH = link.atlas.height * scaleY; const atlasH = link.atlas.height * scaleY;
// @ts-ignore // @ts-ignore
const srcSafe = link.atlas.src.replaceAll("\\", "/"); const srcSafe = link.atlas.src.replaceAll("\\", "/");
// Find out how big we render the sprite // Find out how big we render the sprite
const widthAbsolute = scaleX * link.packedW; const widthAbsolute = scaleX * link.packedW;
const heightAbsolute = scaleY * link.packedH; const heightAbsolute = scaleY * link.packedH;
// Compute the position in the relative container // Compute the position in the relative container
const leftRelative = (link.packOffsetX * scaleX) / w; const leftRelative = (link.packOffsetX * scaleX) / w;
const topRelative = (link.packOffsetY * scaleY) / h; const topRelative = (link.packOffsetY * scaleY) / h;
const widthRelative = widthAbsolute / w; const widthRelative = widthAbsolute / w;
const heightRelative = heightAbsolute / h; const heightRelative = heightAbsolute / h;
// Scale the atlas relative to the width and height of the element // Scale the atlas relative to the width and height of the element
const bgW = atlasW / widthAbsolute; const bgW = atlasW / widthAbsolute;
const bgH = atlasH / heightAbsolute; const bgH = atlasH / heightAbsolute;
// Figure out what the position of the atlas is // Figure out what the position of the atlas is
const bgX = link.packedX * scaleX; const bgX = link.packedX * scaleX;
const bgY = link.packedY * scaleY; const bgY = link.packedY * scaleY;
// Fuck you, whoever thought its a good idea to make background-position work like it does now // Fuck you, whoever thought its a good idea to make background-position work like it does now
const bgXRelative = -bgX / (widthAbsolute - atlasW); const bgXRelative = -bgX / (widthAbsolute - atlasW);
const bgYRelative = -bgY / (heightAbsolute - atlasH); const bgYRelative = -bgY / (heightAbsolute - atlasH);
return ` return `
<span class="spritesheetImage" style=" <span class="spritesheetImage" style="
background-image: url('${srcSafe}'); background-image: url('${srcSafe}');
@ -229,23 +348,23 @@ export class AtlasSprite extends BaseSprite {
width: ${round3Digits(widthRelative * 100.0)}%; width: ${round3Digits(widthRelative * 100.0)}%;
height: ${round3Digits(heightRelative * 100.0)}%; height: ${round3Digits(heightRelative * 100.0)}%;
background-repeat: repeat; 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)}%; background-size: ${round3Digits(bgW * 100.0)}% ${round3Digits(bgH * 100.0)}%;
"></span> "></span>
`; `;
} }
} }
export class RegularSprite extends BaseSprite { export class RegularSprite extends BaseSprite {
public w = w; constructor(public sprite: HTMLCanvasElement | HTMLImageElement, public w: number, public h: number) {
public h = h;
public sprite = sprite;
constructor(sprite, w, h) {
super(); super();
} }
getRawTexture() { getRawTexture() {
return this.sprite; return this.sprite;
} }
/** /**
* Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing * Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing
* images into buffers * images into buffers
@ -258,6 +377,7 @@ export class RegularSprite extends BaseSprite {
assert(h !== undefined, "No height given"); assert(h !== undefined, "No height given");
context.drawImage(this.sprite, x, y, w, h); context.drawImage(this.sprite, x, y, w, h);
} }
/** /**
* Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing * Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing
* images into buffers * images into buffers

View File

@ -3,15 +3,31 @@ import { Entity } from "../game/entity";
import { globalConfig } from "./config"; import { globalConfig } from "./config";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { Rectangle } from "./rectangle"; import { Rectangle } from "./rectangle";
import type { GameRoot } from "../game/root";
const logger = createLogger("stale_areas"); const logger = createLogger("stale_areas");
export class StaleAreaDetector { export class StaleAreaDetector {
public root = root; public root: GameRoot;
public name = name; public name: string;
public recomputeMethod = recomputeMethod; public recomputeMethod: (rect: Rectangle) => void;
public staleArea: Rectangle = null; 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 * Invalidates the given area
*/ */
@ -19,17 +35,18 @@ export class StaleAreaDetector {
// logger.log(this.name, "invalidated", area.toString()); // logger.log(this.name, "invalidated", area.toString());
if (this.staleArea) { if (this.staleArea) {
this.staleArea = this.staleArea.getUnion(area); this.staleArea = this.staleArea.getUnion(area);
} } else {
else {
this.staleArea = area.clone(); this.staleArea = area.clone();
} }
} }
/** /**
* Makes this detector recompute the area of an entity whenever * Makes this detector recompute the area of an entity whenever
* it changes in any way * it changes in any way
*/ */
recomputeOnComponentsChanged(components: Array<typeof Component>, tilesAround: number) { recomputeOnComponentsChanged(components: Array<typeof Component>, tilesAround: number) {
const componentIds = components.map(component => component.getId()); const componentIds = components.map(component => component.getId());
/** /**
* Internal checker method * Internal checker method
*/ */
@ -37,22 +54,28 @@ export class StaleAreaDetector {
if (!this.root.gameInitialized) { if (!this.root.gameInitialized) {
return; return;
} }
// Check for all components // Check for all components
for (let i = 0; i < componentIds.length; ++i) { for (let i = 0; i < componentIds.length; ++i) {
if (entity.components[componentIds[i]]) { if (entity.components[componentIds[i]]) {
// Entity is relevant, compute affected area // 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); this.invalidate(area);
return; return;
} }
} }
}; };
this.root.signals.entityAdded.add(checker); this.root.signals.entityAdded.add(checker);
this.root.signals.entityChanged.add(checker); this.root.signals.entityChanged.add(checker);
this.root.signals.entityComponentRemoved.add(checker); this.root.signals.entityComponentRemoved.add(checker);
this.root.signals.entityGotNewComponent.add(checker); this.root.signals.entityGotNewComponent.add(checker);
this.root.signals.entityDestroyed.add(checker); this.root.signals.entityDestroyed.add(checker);
} }
/** /**
* Updates the stale area * Updates the stale area
*/ */

View File

@ -1,27 +1,27 @@
/* typehints:start*/
import type { Application } from "../application"; import type { Application } from "../application";
/* typehints:end*/
import { GameState } from "./game_state"; import { GameState } from "./game_state";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { waitNextFrame, removeAllChildren } from "./utils"; import { waitNextFrame, removeAllChildren } from "./utils";
import { MOD_SIGNALS } from "../mods/mod_signals"; import { MOD_SIGNALS } from "../mods/mod_signals";
const logger = createLogger("state_manager"); const logger = createLogger("state_manager");
/** /**
* This is the main state machine which drives the game states. * This is the main state machine which drives the game states.
*/ */
export class StateManager { export class StateManager {
public app = app;
public currentState: GameState = null; public currentState: GameState = null;
public stateClasses: { public stateClasses: {
[idx: string]: new () => GameState; [idx: string]: new () => GameState;
} = {}; } = {};
constructor(app) { constructor(public app: Application) {}
}
/** /**
* Registers a new state class, should be a GameState derived class * 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 // Create a dummy to retrieve the key
const dummy = new stateClass(); const dummy = new stateClass();
assert(dummy instanceof GameState, "Not a state!"); assert(dummy instanceof GameState, "Not a state!");
@ -29,6 +29,7 @@ export class StateManager {
assert(!this.stateClasses[key], `State '${key}' is already registered!`); assert(!this.stateClasses[key], `State '${key}' is already registered!`);
this.stateClasses[key] = stateClass; this.stateClasses[key] = stateClass;
} }
/** /**
* Constructs a new state or returns the instance from the cache * 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!`); assert(false, `State '${key}' is not known!`);
} }
/** /**
* Moves to a given state * Moves to a given state
*/ */
moveToState(key: string, payload = {}) { moveToState(key: string, payload: object = {}) {
if (window.APP_ERROR_OCCURED) { if (window.APP_ERROR_OCCURED) {
console.warn("Skipping state transition because of application crash"); console.warn("Skipping state transition because of application crash");
return; return;
} }
if (this.currentState) { if (this.currentState) {
if (key === this.currentState.getKey()) { if (key === this.currentState.getKey()) {
logger.error(`State '${key}' is already active!`); logger.error(`State '${key}' is already active!`);
return false; return false;
} }
this.currentState.internalLeaveCallback(); this.currentState.internalLeaveCallback();
// Remove all references // Remove all references
for (const stateKey in this.currentState) { for (const stateKey in this.currentState) {
if (this.currentState.hasOwnProperty(stateKey)) { if (this.currentState.hasOwnProperty(stateKey)) {
@ -60,42 +64,56 @@ export class StateManager {
} }
this.currentState = null; this.currentState = null;
} }
this.currentState = this.constructState(key); this.currentState = this.constructState(key);
this.currentState.internalRegisterCallback(this, this.app); this.currentState.internalRegisterCallback(this, this.app);
// Clean up old elements // Clean up old elements
if (this.currentState.getRemovePreviousContent()) { if (this.currentState.getRemovePreviousContent()) {
removeAllChildren(document.body); removeAllChildren(document.body);
} }
document.body.className = "gameState " + (this.currentState.getHasFadeIn() ? "" : "arrived"); document.body.className = "gameState " + (this.currentState.getHasFadeIn() ? "" : "arrived");
document.body.id = "state_" + key; document.body.id = "state_" + key;
if (this.currentState.getRemovePreviousContent()) { if (this.currentState.getRemovePreviousContent()) {
document.body.innerHTML = this.currentState.internalGetFullHtml(); document.body.innerHTML = this.currentState.internalGetFullHtml();
} }
const dialogParent = document.createElement("div"); const dialogParent = document.createElement("div");
dialogParent.classList.add("modalDialogParent"); dialogParent.classList.add("modalDialogParent");
document.body.appendChild(dialogParent); document.body.appendChild(dialogParent);
try { try {
this.currentState.internalEnterCallback(payload); this.currentState.internalEnterCallback(payload);
} } catch (ex) {
catch (ex) {
console.error(ex); console.error(ex);
throw ex; throw ex;
} }
this.app.sound.playThemeMusic(this.currentState.getThemeMusic()); this.app.sound.playThemeMusic(this.currentState.getThemeMusic());
this.currentState.onResized(this.app.screenWidth, this.app.screenHeight); this.currentState.onResized(this.app.screenWidth, this.app.screenHeight);
this.app.analytics.trackStateEnter(key); this.app.analytics.trackStateEnter(key);
window.history.pushState({
key, window.history.pushState(
}, key); {
key,
},
key
);
MOD_SIGNALS.stateEntered.dispatch(this.currentState); MOD_SIGNALS.stateEntered.dispatch(this.currentState);
waitNextFrame().then(() => { waitNextFrame().then(() => {
document.body.classList.add("arrived"); document.body.classList.add("arrived");
}); });
return true; return true;
} }
/** /**
* Returns the current state * Returns the current state
* {}
*/ */
getCurrentState(): GameState { getCurrentState(): GameState {
return this.currentState; return this.currentState;

View File

@ -1,23 +1,34 @@
import { Application } from "../application";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { T } from "../translations"; import { T } from "../translations";
import { openStandaloneLink } from "./config"; import { openStandaloneLink } from "./config";
export let WEB_STEAM_SSO_AUTHENTICATED = false; 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) { if (G_IS_STANDALONE) {
return; return;
} }
if (window.location.search.includes("sso_logout_silent")) { if (window.location.search.includes("sso_logout_silent")) {
window.localStorage.setItem("steam_sso_auth_token", ""); window.localStorage.setItem("steam_sso_auth_token", "");
window.location.replace("/"); window.location.replace("/");
return new Promise(() => null); return new Promise(() => null);
} }
if (window.location.search.includes("sso_logout")) { if (window.location.search.includes("sso_logout")) {
const { ok } = dialogs.showWarning(T.dialogs.steamSsoError.title, T.dialogs.steamSsoError.desc); const { ok } = dialogs.showWarning(T.dialogs.steamSsoError.title, T.dialogs.steamSsoError.desc);
window.localStorage.setItem("steam_sso_auth_token", ""); window.localStorage.setItem("steam_sso_auth_token", "");
ok.add(() => window.location.replace("/")); ok.add(() => window.location.replace("/"));
return new Promise(() => null); return new Promise(() => null);
} }
if (window.location.search.includes("steam_sso_no_ownership")) { 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", ""); window.localStorage.setItem("steam_sso_auth_token", "");
getStandalone.add(() => { getStandalone.add(() => {
openStandaloneLink(app, "sso_ownership"); openStandaloneLink(app, "sso_ownership");
@ -26,20 +37,24 @@ export async function authorizeViaSSOToken(app, dialogs) {
ok.add(() => window.location.replace("/")); ok.add(() => window.location.replace("/"));
return new Promise(() => null); return new Promise(() => null);
} }
const token = window.localStorage.getItem("steam_sso_auth_token"); const token = window.localStorage.getItem("steam_sso_auth_token");
if (!token) { if (!token) {
return Promise.resolve(); return Promise.resolve();
} }
const apiUrl = app.clientApi.getEndpoint(); const apiUrl = app.clientApi.getEndpoint();
console.warn("Authorizing via token:", token); console.warn("Authorizing via token:", token);
const verify = async () => { const verify = async () => {
const token = window.localStorage.getItem("steam_sso_auth_token"); const token = window.localStorage.getItem("steam_sso_auth_token");
if (!token) { if (!token) {
window.location.replace("?sso_logout"); window.location.replace("?sso_logout");
return; return;
} }
try { try {
const response = await Promise.race([ const response = (await Promise.race([
fetch(apiUrl + "/v1/sso/refresh", { fetch(apiUrl + "/v1/sso/refresh", {
method: "POST", method: "POST",
body: token, body: token,
@ -50,7 +65,8 @@ export async function authorizeViaSSOToken(app, dialogs) {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
setTimeout(() => reject("timeout exceeded"), 20000); setTimeout(() => reject("timeout exceeded"), 20000);
}), }),
]); ])) as Response;
const responseText = await response.json(); const responseText = await response.json();
if (!responseText.token) { if (!responseText.token) {
console.warn("Failed to register"); console.warn("Failed to register");
@ -58,17 +74,18 @@ export async function authorizeViaSSOToken(app, dialogs) {
window.location.replace("?sso_logout"); window.location.replace("?sso_logout");
return; return;
} }
window.localStorage.setItem("steam_sso_auth_token", responseText.token); window.localStorage.setItem("steam_sso_auth_token", responseText.token);
app.clientApi.token = responseText.token; app.clientApi.token = responseText.token;
WEB_STEAM_SSO_AUTHENTICATED = true; WEB_STEAM_SSO_AUTHENTICATED = true;
} } catch (ex) {
catch (ex) {
console.warn("Auth failure", ex); console.warn("Auth failure", ex);
window.localStorage.setItem("steam_sso_auth_token", ""); window.localStorage.setItem("steam_sso_auth_token", "");
window.location.replace("/"); window.location.replace("/");
return new Promise(() => null); return new Promise(() => null);
} }
}; };
await verify(); await verify();
setInterval(verify, 120000); setInterval(verify, 120000);
} }

View File

@ -2,15 +2,22 @@ import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { GameState } from "./game_state"; import { GameState } from "./game_state";
import { T } from "../translations"; 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. * scrollable content.
*/ */
export class TextualGameState extends GameState { export class TextualGameState extends GameState {
public backToStateId: string;
public backToStatePayload: object;
public containerElement: HTMLDivElement;
public headerElement: HTMLDivElement;
public dialogs: HUDModalDialogs;
///// INTERFACE //// ///// 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() * with the content of getMainContentHTML()
* {}
*/ */
getInnerHTML(): string { getInnerHTML(): string {
return ` return `
@ -19,21 +26,24 @@ export class TextualGameState extends GameState {
</div> </div>
`; `;
} }
/** /**
* Should return the states HTML content. * Should return the states HTML content.
*/ */
getMainContentHTML() { getMainContentHTML() {
return ""; return "";
} }
/** /**
* Should return the title of the game state. If null, no title and back button will * Should return the title of the game state. If null, no title and back button will
* get created * get created
* {}
*/ */
getStateHeaderTitle(): string | null { getStateHeaderTitle(): string | null {
return null; return null;
} }
///////////// /////////////
/** /**
* Back button handler, can be overridden. Per default it goes back to the main menu, * 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. * or if coming from the game it moves back to the game again.
@ -41,17 +51,18 @@ export class TextualGameState extends GameState {
onBackButton() { onBackButton() {
if (this.backToStateId) { if (this.backToStateId) {
this.moveToState(this.backToStateId, this.backToStatePayload); this.moveToState(this.backToStateId, this.backToStatePayload);
} } else {
else {
this.moveToState(this.getDefaultPreviousState()); this.moveToState(this.getDefaultPreviousState());
} }
} }
/** /**
* Returns the default state to go back to * Returns the default state to go back to
*/ */
getDefaultPreviousState() { getDefaultPreviousState() {
return "MainMenuState"; return "MainMenuState";
} }
/** /**
* Goes to a new state, telling him to go back to this state later * 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 * Removes all click detectors, except the one on the back button. Useful when regenerating
* content. * content.
@ -79,6 +91,7 @@ export class TextualGameState extends GameState {
i -= 1; i -= 1;
} }
} }
/** /**
* Overrides the GameState implementation to provide our own html * Overrides the GameState implementation to provide our own html
*/ */
@ -87,10 +100,11 @@ export class TextualGameState extends GameState {
if (this.getStateHeaderTitle()) { if (this.getStateHeaderTitle()) {
headerHtml = ` headerHtml = `
<div class="headerBar"> <div class="headerBar">
<h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1> <h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1>
</div>`; </div>`;
} }
return ` return `
${headerHtml} ${headerHtml}
<div class="container"> <div class="container">
@ -99,7 +113,9 @@ export class TextualGameState extends GameState {
</div> </div>
`; `;
} }
//// INTERNALS ///// //// INTERNALS /////
/** /**
* Overrides the GameState leave callback to cleanup stuff * Overrides the GameState leave callback to cleanup stuff
*/ */
@ -107,6 +123,7 @@ export class TextualGameState extends GameState {
super.internalLeaveCallback(); super.internalLeaveCallback();
this.dialogs.cleanup(); this.dialogs.cleanup();
} }
/** /**
* Overrides the GameState enter callback to setup required stuff * Overrides the GameState enter callback to setup required stuff
*/ */
@ -116,18 +133,23 @@ export class TextualGameState extends GameState {
this.backToStateId = payload.backToStateId; this.backToStateId = payload.backToStateId;
this.backToStatePayload = payload.backToStatePayload; this.backToStatePayload = payload.backToStatePayload;
} }
this.htmlElement.classList.add("textualState"); this.htmlElement.classList.add("textualState");
if (this.getStateHeaderTitle()) { if (this.getStateHeaderTitle()) {
this.htmlElement.classList.add("hasTitle"); this.htmlElement.classList.add("hasTitle");
} }
this.containerElement = this.htmlElement.querySelector(".widthKeeper .container"); this.containerElement = this.htmlElement.querySelector(".widthKeeper .container");
this.headerElement = this.htmlElement.querySelector(".headerBar > h1"); this.headerElement = this.htmlElement.querySelector(".headerBar > h1");
if (this.headerElement) { if (this.headerElement) {
this.trackClicks(this.headerElement, this.onBackButton); this.trackClicks(this.headerElement, this.onBackButton);
} }
this.dialogs = new HUDModalDialogs(null, this.app); this.dialogs = new HUDModalDialogs(null, this.app);
const dialogsElement = document.body.querySelector(".modalDialogParent"); const dialogsElement = document.body.querySelector(".modalDialogParent");
this.dialogs.initializeToElement(dialogsElement); this.dialogs.initializeToElement(dialogsElement);
this.onEnter(payload); this.onEnter(payload);
} }
} }

View File

@ -1,7 +1,10 @@
export class TrackedState { export class TrackedState<T> {
public lastSeenValue = null; 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) { if (callbackMethod) {
this.callback = callbackMethod; this.callback = callbackMethod;
if (callbackScope) { 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) { if (value !== this.lastSeenValue) {
// Copy value since the changeHandler call could actually modify our lastSeenValue // Copy value since the changeHandler call could actually modify our lastSeenValue
const valueCopy = value; const valueCopy = value;
@ -17,23 +21,22 @@ export class TrackedState {
if (changeHandler) { if (changeHandler) {
if (changeScope) { if (changeScope) {
changeHandler.call(changeScope, valueCopy); changeHandler.call(changeScope, valueCopy);
} } else {
else {
changeHandler(valueCopy); changeHandler(valueCopy);
} }
} } else if (this.callback) {
else if (this.callback) {
this.callback(value); this.callback(value);
} } else {
else {
assert(false, "No callback specified"); assert(false, "No callback specified");
} }
} }
} }
setSilent(value) {
setSilent(value: T) {
this.lastSeenValue = value; this.lastSeenValue = value;
} }
get() {
get(): T {
return this.lastSeenValue; return this.lastSeenValue;
} }
} }

View File

@ -1,23 +1,23 @@
import { T } from "../translations"; import { T } from "../translations";
import { rando } from "@nastyox/rando.js"; import { rando } from "@nastyox/rando.js";
import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso"; import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso";
const bigNumberSuffixTranslationKeys = ["thousands", "millions", "billions", "trillions"]; const bigNumberSuffixTranslationKeys = ["thousands", "millions", "billions", "trillions"];
/** /**
* Returns a platform name * Returns a platform name
* {}
*/ */
export function getPlatformName(): "android" | "browser" | "ios" | "standalone" | "unknown" { export function getPlatformName(): "android" | "browser" | "ios" | "standalone" | "unknown" {
if (G_IS_STANDALONE) { if (G_IS_STANDALONE) {
return "standalone"; return "standalone";
} } else if (G_IS_BROWSER) {
else if (G_IS_BROWSER) {
return "browser"; return "browser";
} }
return "unknown"; return "unknown";
} }
/** /**
* Makes a new 2D array with undefined contents * Makes a new 2D array with undefined contents
* {}
*/ */
export function make2DUndefinedArray(w: number, h: number): Array<Array<any>> { export function make2DUndefinedArray(w: number, h: number): Array<Array<any>> {
const result = new Array(w); const result = new Array(w);
@ -26,36 +26,39 @@ export function make2DUndefinedArray(w: number, h: number): Array<Array<any>> {
} }
return result; return result;
} }
/** /**
* Creates a new map (an empty object without any props) * Creates a new map (an empty object without any props)
*/ */
export function newEmptyMap() { export function newEmptyMap(): object {
return Object.create(null); return Object.create(null);
} }
/** /**
* Returns a random integer in the range [start,end] * Returns a random integer in the range [start,end]
*/ */
export function randomInt(start: number, end: number) { export function randomInt(start: number, end: number) {
return rando(start, end); return rando(start, end);
} }
/** /**
* Access an object in a very annoying way, used for obsfuscation. * 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; let result = obj;
for (let i = keys.length - 1; i >= 0; --i) { for (let i = keys.length - 1; i >= 0; --i) {
result = result[keys[i]]; result = result[keys[i]];
} }
return result; return result;
} }
/** /**
* Chooses a random entry of an array * 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)]; return arr[Math.floor(Math.random() * arr.length)];
} }
/** /**
* Deletes from an array by swapping with the last element * 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]; const last = array[array.length - 1];
array[index] = last; array[index] = last;
} }
// Finally remove the last element // Finally remove the last element
array.length -= 1; array.length -= 1;
} }
/** /**
* Deletes from an array by swapping with the last element. Searches * Deletes from an array by swapping with the last element. Searches
* for the value in the array first * for the value in the array first
@ -87,6 +92,7 @@ export function fastArrayDeleteValue(array: Array<any>, value: any) {
} }
return fastArrayDelete(array, index); return fastArrayDelete(array, index);
} }
/** /**
* @see fastArrayDeleteValue * @see fastArrayDeleteValue
*/ */
@ -100,6 +106,7 @@ export function fastArrayDeleteValueIfContained(array: Array<any>, value: any) {
} }
return fastArrayDelete(array, index); return fastArrayDelete(array, index);
} }
/** /**
* Deletes from an array at the given 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); array.splice(index, 1);
} }
/** /**
* Deletes the given value from an array * Deletes the given value from an array
*/ */
@ -123,19 +131,21 @@ export function arrayDeleteValue(array: Array<any>, value: any) {
} }
return arrayDelete(array, index); return arrayDelete(array, index);
} }
/** /**
* Compare two floats for epsilon equality * Compare two floats for epsilon equality
* {}
*/ */
export function epsilonCompare(a: number, b: number, epsilon = 1e-5): boolean { export function epsilonCompare(a: number, b: number, epsilon = 1e-5): boolean {
return Math.abs(a - b) < epsilon; return Math.abs(a - b) < epsilon;
} }
/** /**
* Interpolates two numbers * Interpolates two numbers
*/ */
export function lerp(a: number, b: number, x: number) { export function lerp(a: number, b: number, x: number) {
return a * (1 - x) + b * x; return a * (1 - x) + b * x;
} }
/** /**
* Finds a value which is nice to display, e.g. 15669 -> 15000. Also handles fractional stuff * 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; let roundAmount = 1;
if (num > 50000) { if (num > 50000) {
roundAmount = 10000; roundAmount = 10000;
} } else if (num > 20000) {
else if (num > 20000) {
roundAmount = 5000; roundAmount = 5000;
} } else if (num > 5000) {
else if (num > 5000) {
roundAmount = 1000; roundAmount = 1000;
} } else if (num > 2000) {
else if (num > 2000) {
roundAmount = 500; roundAmount = 500;
} } else if (num > 1000) {
else if (num > 1000) {
roundAmount = 100; roundAmount = 100;
} } else if (num > 100) {
else if (num > 100) {
roundAmount = 20; roundAmount = 20;
} } else if (num > 20) {
else if (num > 20) {
roundAmount = 5; roundAmount = 5;
} }
const niceValue = Math.floor(num / roundAmount) * roundAmount; const niceValue = Math.floor(num / roundAmount) * roundAmount;
if (num >= 10) { if (num >= 10) {
return Math.round(niceValue); return Math.round(niceValue);
@ -175,8 +180,10 @@ export function findNiceValue(num: number) {
if (num >= 1) { if (num >= 1) {
return Math.round(niceValue * 10) / 10; return Math.round(niceValue * 10) / 10;
} }
return Math.round(niceValue * 100) / 100; return Math.round(niceValue * 100) / 100;
} }
/** /**
* Finds a nice integer value * Finds a nice integer value
* @see findNiceValue * @see findNiceValue
@ -184,16 +191,18 @@ export function findNiceValue(num: number) {
export function findNiceIntegerValue(num: number) { export function findNiceIntegerValue(num: number) {
return Math.ceil(findNiceValue(num)); return Math.ceil(findNiceValue(num));
} }
/** /**
* Formats a big number * 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 ? "-" : ""; const sign = num < 0 ? "-" : "";
num = Math.abs(num); num = Math.abs(num);
if (num > 1e54) { if (num > 1e54) {
return sign + T.global.infinite; return sign + T.global.infinite;
} }
if (num < 10 && !Number.isInteger(num)) { if (num < 10 && !Number.isInteger(num)) {
return sign + num.toFixed(2); return sign + num.toFixed(2);
} }
@ -201,10 +210,10 @@ export function formatBigNumber(num: number, separator: string= = T.global.decim
return sign + num.toFixed(1); return sign + num.toFixed(1);
} }
num = Math.floor(num); num = Math.floor(num);
if (num < 1000) { if (num < 1000) {
return sign + "" + num; return sign + "" + num;
} } else {
else {
let leadingDigits = num; let leadingDigits = num;
let suffix = ""; let suffix = "";
for (let suffixIndex = 0; suffixIndex < bigNumberSuffixTranslationKeys.length; ++suffixIndex) { 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; return sign + leadingDigitsNoTrailingDecimal + suffix;
} }
} }
/** /**
* Formats a big number, but does not add any suffix and instead uses its full representation * 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) { if (num < 1000) {
return num + ""; return num + "";
} }
@ -240,11 +249,12 @@ export function formatBigNumberFull(num: number, divider: string= = T.global.tho
rest = Math.floor(rest / 1000); rest = Math.floor(rest / 1000);
} }
out = rest + divider + out; out = rest + divider + out;
return out.substring(0, out.length - 1); return out.substring(0, out.length - 1);
} }
/** /**
* Waits two frames so the ui is updated * Waits two frames so the ui is updated
* {}
*/ */
export function waitNextFrame(): Promise<void> { export function waitNextFrame(): Promise<void> {
return new Promise(function (resolve) { return new Promise(function (resolve) {
@ -255,44 +265,46 @@ export function waitNextFrame(): Promise<void> {
}); });
}); });
} }
/** /**
* Rounds 1 digit * Rounds 1 digit
* {}
*/ */
export function round1Digit(n: number): number { export function round1Digit(n: number): number {
return Math.floor(n * 10.0) / 10.0; return Math.floor(n * 10.0) / 10.0;
} }
/** /**
* Rounds 2 digits * Rounds 2 digits
* {}
*/ */
export function round2Digits(n: number): number { export function round2Digits(n: number): number {
return Math.floor(n * 100.0) / 100.0; return Math.floor(n * 100.0) / 100.0;
} }
/** /**
* Rounds 3 digits * Rounds 3 digits
* {}
*/ */
export function round3Digits(n: number): number { export function round3Digits(n: number): number {
return Math.floor(n * 1000.0) / 1000.0; return Math.floor(n * 1000.0) / 1000.0;
} }
/** /**
* Rounds 4 digits * Rounds 4 digits
* {}
*/ */
export function round4Digits(n: number): number { export function round4Digits(n: number): number {
return Math.floor(n * 10000.0) / 10000.0; return Math.floor(n * 10000.0) / 10000.0;
} }
/** /**
* Clamps a value between [min, max] * 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)); return Math.max(minimum, Math.min(maximum, v));
} }
/** /**
* Helper method to create a new div element * 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"); const div = document.createElement("div");
if (id) { if (id) {
div.id = id; div.id = id;
@ -303,18 +315,25 @@ export function makeDivElement(id: string= = null, classes: Array<string>= = [],
div.innerHTML = innerHTML; div.innerHTML = innerHTML;
return div; return div;
} }
/** /**
* Helper method to create a new 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); const div = makeDivElement(id, classes, innerHTML);
parent.appendChild(div); parent.appendChild(div);
return div; return div;
} }
/** /**
* Helper method to create a new button element * 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"); const element = document.createElement("button");
for (let i = 0; i < classes.length; ++i) { for (let i = 0; i < classes.length; ++i) {
element.classList.add(classes[i]); element.classList.add(classes[i]);
@ -323,14 +342,16 @@ export function makeButtonElement(classes: Array<string>= = [], innerHTML: strin
element.innerHTML = innerHTML; element.innerHTML = innerHTML;
return element; return element;
} }
/** /**
* Helper method to create a new button * 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); const element = makeButtonElement(classes, innerHTML);
parent.appendChild(element); parent.appendChild(element);
return element; return element;
} }
/** /**
* Removes all children of the given element * Removes all children of the given element
*/ */
@ -341,6 +362,7 @@ export function removeAllChildren(elem: Element) {
range.deleteContents(); range.deleteContents();
} }
} }
/** /**
* Returns if the game supports this browser * 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 new IE Edge outputs to true now for window.chrome
// and if not iOS Chrome check // and if not iOS Chrome check
// so use the below updated condition // so use the below updated condition
if (G_IS_STANDALONE) { if (G_IS_STANDALONE) {
return true; return true;
} }
// @ts-ignore // @ts-ignore
var isChromium = window.chrome; var isChromium = window.chrome;
var winNav = window.navigator; var winNav = window.navigator;
@ -362,93 +386,102 @@ export function isSupportedBrowser() {
// @ts-ignore // @ts-ignore
var isIEedge = winNav.userAgent.indexOf("Edge") > -1; var isIEedge = winNav.userAgent.indexOf("Edge") > -1;
var isIOSChrome = winNav.userAgent.match("CriOS"); var isIOSChrome = winNav.userAgent.match("CriOS");
if (isIOSChrome) { if (isIOSChrome) {
// is Google Chrome on IOS // is Google Chrome on IOS
return false; return false;
} } else if (
else if (isChromium !== null && isChromium !== null &&
typeof isChromium !== "undefined" && typeof isChromium !== "undefined" &&
vendorName === "Google Inc." && vendorName === "Google Inc." &&
isIEedge === false) { isIEedge === false
) {
// is Google Chrome // is Google Chrome
return true; return true;
} } else {
else {
// not Google Chrome // not Google Chrome
return false; return false;
} }
} }
/** /**
* Formats an amount of seconds into something like "5s ago" * Formats an amount of seconds into something like "5s ago"
* {}
*/ */
export function formatSecondsToTimeAgo(secs: number): string { export function formatSecondsToTimeAgo(secs: number): string {
const seconds = Math.floor(secs); const seconds = Math.floor(secs);
const minutes = Math.floor(seconds / 60); const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24); const days = Math.floor(hours / 24);
if (seconds < 60) { if (seconds < 60) {
if (seconds === 1) { if (seconds === 1) {
return T.global.time.oneSecondAgo; return T.global.time.oneSecondAgo;
} }
return T.global.time.xSecondsAgo.replace("<x>", "" + seconds); return T.global.time.xSecondsAgo.replace("<x>", "" + seconds);
} } else if (minutes < 60) {
else if (minutes < 60) {
if (minutes === 1) { if (minutes === 1) {
return T.global.time.oneMinuteAgo; return T.global.time.oneMinuteAgo;
} }
return T.global.time.xMinutesAgo.replace("<x>", "" + minutes); return T.global.time.xMinutesAgo.replace("<x>", "" + minutes);
} } else if (hours < 24) {
else if (hours < 24) {
if (hours === 1) { if (hours === 1) {
return T.global.time.oneHourAgo; return T.global.time.oneHourAgo;
} }
return T.global.time.xHoursAgo.replace("<x>", "" + hours); return T.global.time.xHoursAgo.replace("<x>", "" + hours);
} } else {
else {
if (days === 1) { if (days === 1) {
return T.global.time.oneDayAgo; return T.global.time.oneDayAgo;
} }
return T.global.time.xDaysAgo.replace("<x>", "" + days); return T.global.time.xDaysAgo.replace("<x>", "" + days);
} }
} }
/** /**
* Formats seconds into a readable string like "5h 23m" * Formats seconds into a readable string like "5h 23m"
* {}
*/ */
export function formatSeconds(secs: number): string { export function formatSeconds(secs: number): string {
const trans = T.global.time; const trans = T.global.time;
secs = Math.ceil(secs); secs = Math.ceil(secs);
if (secs < 60) { if (secs < 60) {
return trans.secondsShort.replace("<seconds>", "" + secs); return trans.secondsShort.replace("<seconds>", "" + secs);
} } else if (secs < 60 * 60) {
else if (secs < 60 * 60) {
const minutes = Math.floor(secs / 60); const minutes = Math.floor(secs / 60);
const seconds = secs % 60; const seconds = secs % 60;
return trans.minutesAndSecondsShort return trans.minutesAndSecondsShort
.replace("<seconds>", "" + seconds) .replace("<seconds>", "" + seconds)
.replace("<minutes>", "" + minutes); .replace("<minutes>", "" + minutes);
} } else {
else {
const hours = Math.floor(secs / 3600); const hours = Math.floor(secs / 3600);
const minutes = Math.floor(secs / 60) % 60; const minutes = Math.floor(secs / 60) % 60;
return trans.hoursAndMinutesShort.replace("<minutes>", "" + minutes).replace("<hours>", "" + hours); return trans.hoursAndMinutesShort.replace("<minutes>", "" + minutes).replace("<hours>", "" + hours);
} }
} }
/** /**
* Formats a number like 2.51 to "2.5" * 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); return round1Digit(speed).toString().replace(".", separator);
} }
/** /**
* Formats a number like 2.51 to "2.51 items / s" * Formats a number like 2.51 to "2.51 items / s"
*/ */
export function formatItemsPerSecond(speed: number, double: boolean= = false, separator: string= = T.global.decimalSeparator) { export function formatItemsPerSecond(
return ((speed === 1.0 speed: number,
? T.ingame.buildingPlacement.infoTexts.oneItemPerSecond double: boolean = false,
: T.ingame.buildingPlacement.infoTexts.itemsPerSecond.replace("<x>", round2Digits(speed).toString().replace(".", separator))) + (double ? " " + T.ingame.buildingPlacement.infoTexts.itemsPerSecondDouble : "")); 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 * Rotates a flat 3x3 matrix clockwise
* Entries: * Entries:
@ -475,11 +508,11 @@ export function rotateFlatMatrix3x3(flatMatrix: Array<number>) {
flatMatrix[2], flatMatrix[2],
]; ];
} }
/** /**
* Generates rotated variants of the matrix * 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 = { const result = {
0: originalMatrix, 0: originalMatrix,
}; };
@ -492,9 +525,15 @@ export function generateMatrixRotations(originalMatrix: Array<number>): Object<n
return result; return result;
} }
export type DirectionalObject = {
top: any;
right: any;
bottom: any;
left: any;
};
/** /**
* Rotates a directional object * Rotates a directional object
* {}
*/ */
export function rotateDirectionalObject(obj: DirectionalObject, rotation): DirectionalObject { export function rotateDirectionalObject(obj: DirectionalObject, rotation): DirectionalObject {
const queue = [obj.top, obj.right, obj.bottom, obj.left]; const queue = [obj.top, obj.right, obj.bottom, obj.left];
@ -502,6 +541,7 @@ export function rotateDirectionalObject(obj: DirectionalObject, rotation): Direc
rotation -= 90; rotation -= 90;
queue.push(queue.shift()); queue.push(queue.shift());
} }
return { return {
top: queue[0], top: queue[0],
right: queue[1], right: queue[1],
@ -509,19 +549,21 @@ export function rotateDirectionalObject(obj: DirectionalObject, rotation): Direc
left: queue[3], left: queue[3],
}; };
} }
/** /**
* Modulo which works for negative numbers * Modulo which works for negative numbers
*/ */
export function safeModulo(n: number, m: number) { export function safeModulo(n: number, m: number) {
return ((n % m) + m) % m; return ((n % m) + m) % m;
} }
/** /**
* Returns a smooth pulse between 0 and 1 * Returns a smooth pulse between 0 and 1
* {}
*/ */
export function smoothPulse(time: number): number { export function smoothPulse(time: number): number {
return Math.sin(time * 4) * 0.5 + 0.5; return Math.sin(time * 4) * 0.5 + 0.5;
} }
/** /**
* Fills in a <link> tag * 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 href='" + link + "' target='_blank'>")
.replace("</link>", "</a>"); .replace("</link>", "</a>");
} }
/** /**
* Generates a file download * Generates a file download
*/ */
@ -537,11 +580,14 @@ export function generateFileDownload(filename: string, text: string) {
var element = document.createElement("a"); var element = document.createElement("a");
element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text)); element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
element.setAttribute("download", filename); element.setAttribute("download", filename);
element.style.display = "none"; element.style.display = "none";
document.body.appendChild(element); document.body.appendChild(element);
element.click(); element.click();
document.body.removeChild(element); document.body.removeChild(element);
} }
/** /**
* Starts a file chooser * Starts a file chooser
*/ */
@ -549,25 +595,26 @@ export function startFileChoose(acceptedType: string = ".bin") {
var input = document.createElement("input"); var input = document.createElement("input");
input.type = "file"; input.type = "file";
input.accept = acceptedType; input.accept = acceptedType;
return new Promise(resolve => { return new Promise(resolve => {
input.onchange = _ => resolve(input.files[0]); input.onchange = _ => resolve(input.files[0]);
input.click(); input.click();
}); });
} }
const MAX_ROMAN_NUMBER = 49; const MAX_ROMAN_NUMBER = 49;
const romanLiteralsCache = ["0"]; const romanLiteralsCache = ["0"];
/**
*
* {}
*/
export function getRomanNumber(number: number): string { export function getRomanNumber(number: number): string {
number = Math.max(0, Math.round(number)); number = Math.max(0, Math.round(number));
if (romanLiteralsCache[number]) { if (romanLiteralsCache[number]) {
return romanLiteralsCache[number]; return romanLiteralsCache[number];
} }
if (number > MAX_ROMAN_NUMBER) { if (number > MAX_ROMAN_NUMBER) {
return String(number); return String(number);
} }
function formatDigit(digit, unit, quintuple, decuple) { function formatDigit(digit, unit, quintuple, decuple) {
switch (digit) { switch (digit) {
case 0: case 0:
@ -587,22 +634,29 @@ export function getRomanNumber(number: number): string {
return quintuple + formatDigit(digit - 5, unit, quintuple, decuple); return quintuple + formatDigit(digit - 5, unit, quintuple, decuple);
} }
} }
let thousands = Math.floor(number / 1000); let thousands = Math.floor(number / 1000);
let thousandsPart = ""; let thousandsPart = "";
while (thousands > 0) { while (thousands > 0) {
thousandsPart += "M"; thousandsPart += "M";
thousands -= 1; thousands -= 1;
} }
const hundreds = Math.floor((number % 1000) / 100); const hundreds = Math.floor((number % 1000) / 100);
const hundredsPart = formatDigit(hundreds, "C", "D", "M"); const hundredsPart = formatDigit(hundreds, "C", "D", "M");
const tens = Math.floor((number % 100) / 10); const tens = Math.floor((number % 100) / 10);
const tensPart = formatDigit(tens, "X", "L", "C"); const tensPart = formatDigit(tens, "X", "L", "C");
const units = number % 10; const units = number % 10;
const unitsPart = formatDigit(units, "I", "V", "X"); const unitsPart = formatDigit(units, "I", "V", "X");
const formatted = thousandsPart + hundredsPart + tensPart + unitsPart; const formatted = thousandsPart + hundredsPart + tensPart + unitsPart;
romanLiteralsCache[number] = formatted; romanLiteralsCache[number] = formatted;
return formatted; return formatted;
} }
/** /**
* Returns the appropriate logo sprite path * Returns the appropriate logo sprite path
*/ */
@ -615,10 +669,11 @@ export function getLogoSprite() {
} }
return "logo.png"; return "logo.png";
} }
/** /**
* Rejects a promise after X ms * 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([ return Promise.race([
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
setTimeout(() => reject("timeout of " + timeout + " ms exceeded"), timeout); setTimeout(() => reject("timeout of " + timeout + " ms exceeded"), timeout);

View File

@ -1,324 +1,315 @@
import { globalConfig } from "./config"; import { globalConfig } from "./config";
import { safeModulo } from "./utils"; import { safeModulo } from "./utils";
const tileSize = globalConfig.tileSize; const tileSize = globalConfig.tileSize;
const halfTileSize = globalConfig.halfTileSize; const halfTileSize = globalConfig.halfTileSize;
/**
* @enum {string} export enum enumDirection {
*/ top = "top",
export const enumDirection = { right = "right",
top: "top", bottom = "bottom",
right: "right", left = "left",
bottom: "bottom", }
left: "left",
};
/**
* @enum {string}
*/
export const enumInvertedDirections = { export const enumInvertedDirections = {
[enumDirection.top]: enumDirection.bottom, [enumDirection.top]: enumDirection.bottom,
[enumDirection.right]: enumDirection.left, [enumDirection.right]: enumDirection.left,
[enumDirection.bottom]: enumDirection.top, [enumDirection.bottom]: enumDirection.top,
[enumDirection.left]: enumDirection.right, [enumDirection.left]: enumDirection.right,
}; };
/**
* @enum {number}
*/
export const enumDirectionToAngle = { export const enumDirectionToAngle = {
[enumDirection.top]: 0, [enumDirection.top]: 0,
[enumDirection.right]: 90, [enumDirection.right]: 90,
[enumDirection.bottom]: 180, [enumDirection.bottom]: 180,
[enumDirection.left]: 270, [enumDirection.left]: 270,
}; };
/**
* @enum {enumDirection}
*/
export const enumAngleToDirection = { export const enumAngleToDirection = {
0: enumDirection.top, 0: enumDirection.top,
90: enumDirection.right, 90: enumDirection.right,
180: enumDirection.bottom, 180: enumDirection.bottom,
270: enumDirection.left, 270: enumDirection.left,
}; };
export const arrayAllDirections: Array<enumDirection> = [ export const arrayAllDirections: Array<enumDirection> = [
enumDirection.top, enumDirection.top,
enumDirection.right, enumDirection.right,
enumDirection.bottom, enumDirection.bottom,
enumDirection.left, 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 * return a copy of the vector
* {}
*/ */
copy(): Vector { copy(): Vector {
return new Vector(this.x, this.y); return new Vector(this.x, this.y);
} }
/** /**
* Adds a vector and return a new vector * Adds a vector and return a new vector
* {}
*/ */
add(other: Vector): Vector { add(other: Vector): Vector {
return new Vector(this.x + other.x, this.y + other.y); return new Vector(this.x + other.x, this.y + other.y);
} }
/** /**
* Adds a vector * Adds a vector
* {}
*/ */
addInplace(other: Vector): Vector { addInplace(other: Vector): Vector {
this.x += other.x; this.x += other.x;
this.y += other.y; this.y += other.y;
return this; return this;
} }
/** /**
* Substracts a vector and return a new vector * Substracts a vector and return a new vector
* {}
*/ */
sub(other: Vector): Vector { sub(other: Vector): Vector {
return new Vector(this.x - other.x, this.y - other.y); return new Vector(this.x - other.x, this.y - other.y);
} }
/** /**
* Subs a vector * Subs a vector
* {}
*/ */
subInplace(other: Vector): Vector { subInplace(other: Vector): Vector {
this.x -= other.x; this.x -= other.x;
this.y -= other.y; this.y -= other.y;
return this; return this;
} }
/** /**
* Multiplies with a vector and return a new vector * Multiplies with a vector and return a new vector
* {}
*/ */
mul(other: Vector): Vector { mul(other: Vector): Vector {
return new Vector(this.x * other.x, this.y * other.y); return new Vector(this.x * other.x, this.y * other.y);
} }
/** /**
* Adds two scalars and return a new vector * Adds two scalars and return a new vector
* {}
*/ */
addScalars(x: number, y: number): Vector { addScalars(x: number, y: number): Vector {
return new Vector(this.x + x, this.y + y); return new Vector(this.x + x, this.y + y);
} }
/** /**
* Substracts a scalar and return a new vector * Substracts a scalar and return a new vector
* {}
*/ */
subScalar(f: number): Vector { subScalar(f: number): Vector {
return new Vector(this.x - f, this.y - f); return new Vector(this.x - f, this.y - f);
} }
/** /**
* Substracts two scalars and return a new vector * Substracts two scalars and return a new vector
* {}
*/ */
subScalars(x: number, y: number): Vector { subScalars(x: number, y: number): Vector {
return new Vector(this.x - x, this.y - y); return new Vector(this.x - x, this.y - y);
} }
/** /**
* Returns the euclidian length * Returns the euclidian length
* {}
*/ */
length(): number { length(): number {
return Math.hypot(this.x, this.y); return Math.hypot(this.x, this.y);
} }
/** /**
* Returns the square length * Returns the square length
* {}
*/ */
lengthSquare(): number { lengthSquare(): number {
return this.x * this.x + this.y * this.y; return this.x * this.x + this.y * this.y;
} }
/** /**
* Divides both components by a scalar and return a new vector * Divides both components by a scalar and return a new vector
* {}
*/ */
divideScalar(f: number): Vector { divideScalar(f: number): Vector {
return new Vector(this.x / f, this.y / f); return new Vector(this.x / f, this.y / f);
} }
/** /**
* Divides both components by the given scalars and return a new vector * Divides both components by the given scalars and return a new vector
* {}
*/ */
divideScalars(a: number, b: number): Vector { divideScalars(a: number, b: number): Vector {
return new Vector(this.x / a, this.y / b); return new Vector(this.x / a, this.y / b);
} }
/** /**
* Divides both components by a scalar * Divides both components by a scalar
* {}
*/ */
divideScalarInplace(f: number): Vector { divideScalarInplace(f: number): Vector {
this.x /= f; this.x /= f;
this.y /= f; this.y /= f;
return this; return this;
} }
/** /**
* Multiplies both components with a scalar and return a new vector * Multiplies both components with a scalar and return a new vector
* {}
*/ */
multiplyScalar(f: number): Vector { multiplyScalar(f: number): Vector {
return new Vector(this.x * f, this.y * f); return new Vector(this.x * f, this.y * f);
} }
/** /**
* Multiplies both components with two scalars and returns a new vector * Multiplies both components with two scalars and returns a new vector
* {}
*/ */
multiplyScalars(a: number, b: number): Vector { multiplyScalars(a: number, b: number): Vector {
return new Vector(this.x * a, this.y * b); 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 both components, compute the maximum of each component and the given scalar, and return a new vector.
* For example: * For example:
* - new Vector(-1, 5).maxScalar(0) -> Vector(0, 5) * - new Vector(-1, 5).maxScalar(0) -> Vector(0, 5)
* {}
*/ */
maxScalar(f: number): Vector { maxScalar(f: number): Vector {
return new Vector(Math.max(f, this.x), Math.max(f, this.y)); return new Vector(Math.max(f, this.x), Math.max(f, this.y));
} }
/** /**
* Adds a scalar to both components and return a new vector * Adds a scalar to both components and return a new vector
* {}
*/ */
addScalar(f: number): Vector { addScalar(f: number): Vector {
return new Vector(this.x + f, this.y + f); return new Vector(this.x + f, this.y + f);
} }
/** /**
* Computes the component wise minimum and return a new vector * Computes the component wise minimum and return a new vector
* {}
*/ */
min(v: Vector): Vector { min(v: Vector): Vector {
return new Vector(Math.min(v.x, this.x), Math.min(v.y, this.y)); 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 * Computes the component wise maximum and return a new vector
* {}
*/ */
max(v: Vector): Vector { max(v: Vector): Vector {
return new Vector(Math.max(v.x, this.x), Math.max(v.y, this.y)); return new Vector(Math.max(v.x, this.x), Math.max(v.y, this.y));
} }
/** /**
* Computes the component wise absolute * Computes the component wise absolute
* {}
*/ */
abs(): Vector { abs(): Vector {
return new Vector(Math.abs(this.x), Math.abs(this.y)); return new Vector(Math.abs(this.x), Math.abs(this.y));
} }
/** /**
* Computes the scalar product * Computes the scalar product
* {}
*/ */
dot(v: Vector): number { dot(v: Vector): number {
return this.x * v.x + this.y * v.y; return this.x * v.x + this.y * v.y;
} }
/** /**
* Computes the distance to a given vector * Computes the distance to a given vector
* {}
*/ */
distance(v: Vector): number { distance(v: Vector): number {
return Math.hypot(this.x - v.x, this.y - v.y); return Math.hypot(this.x - v.x, this.y - v.y);
} }
/** /**
* Computes the square distance to a given vectort * Computes the square distance to a given vectort
* {}
*/ */
distanceSquare(v: Vector): number { distanceSquare(v: Vector): number {
const dx = this.x - v.x; const dx = this.x - v.x;
const dy = this.y - v.y; const dy = this.y - v.y;
return dx * dx + dy * dy; return dx * dx + dy * dy;
} }
/** /**
* Returns x % f, y % f * Returns x % f, y % f
* {} new vector
*/ */
modScalar(f: number): Vector { modScalar(f: number): Vector {
return new Vector(safeModulo(this.x, f), safeModulo(this.y, f)); return new Vector(safeModulo(this.x, f), safeModulo(this.y, f));
} }
/** /**
* Computes and returns the center between both points * Computes and returns the center between both points
* {}
*/ */
centerPoint(v: Vector): Vector { centerPoint(v: Vector): Vector {
const cx = this.x + v.x; const cx = this.x + v.x;
const cy = this.y + v.y; const cy = this.y + v.y;
return new Vector(cx / 2, cy / 2); return new Vector(cx / 2, cy / 2);
} }
/** /**
* Computes componentwise floor and returns a new vector * Computes componentwise floor and returns a new vector
* {}
*/ */
floor(): Vector { floor(): Vector {
return new Vector(Math.floor(this.x), Math.floor(this.y)); return new Vector(Math.floor(this.x), Math.floor(this.y));
} }
/** /**
* Computes componentwise ceil and returns a new vector * Computes componentwise ceil and returns a new vector
* {}
*/ */
ceil(): Vector { ceil(): Vector {
return new Vector(Math.ceil(this.x), Math.ceil(this.y)); return new Vector(Math.ceil(this.x), Math.ceil(this.y));
} }
/** /**
* Computes componentwise round and return a new vector * Computes componentwise round and return a new vector
* {}
*/ */
round(): Vector { round(): Vector {
return new Vector(Math.round(this.x), Math.round(this.y)); return new Vector(Math.round(this.x), Math.round(this.y));
} }
/** /**
* Converts this vector from world to tile space and return a new vector * Converts this vector from world to tile space and return a new vector
* {}
*/ */
toTileSpace(): Vector { toTileSpace(): Vector {
return new Vector(Math.floor(this.x / tileSize), Math.floor(this.y / tileSize)); 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 * Converts this vector from world to street space and return a new vector
* {}
*/ */
toStreetSpace(): Vector { toStreetSpace(): Vector {
return new Vector(Math.floor(this.x / halfTileSize + 0.25), Math.floor(this.y / halfTileSize + 0.25)); 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 * Converts this vector to world space and return a new vector
* {}
*/ */
toWorldSpace(): Vector { toWorldSpace(): Vector {
return new Vector(this.x * tileSize, this.y * tileSize); return new Vector(this.x * tileSize, this.y * tileSize);
} }
/** /**
* Converts this vector to world space and return a new vector * Converts this vector to world space and return a new vector
* {}
*/ */
toWorldSpaceCenterOfTile(): Vector { toWorldSpaceCenterOfTile(): Vector {
return new Vector(this.x * tileSize + halfTileSize, this.y * tileSize + halfTileSize); return new Vector(this.x * tileSize + halfTileSize, this.y * tileSize + halfTileSize);
} }
/** /**
* Converts the top left tile position of this vector * Converts the top left tile position of this vector
* {}
*/ */
snapWorldToTile(): Vector { snapWorldToTile(): Vector {
return new Vector(Math.floor(this.x / tileSize) * tileSize, Math.floor(this.y / tileSize) * tileSize); 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 * Normalizes the vector, dividing by the length(), and return a new vector
* {}
*/ */
normalize(): Vector { normalize(): Vector {
const len = Math.max(1e-5, Math.hypot(this.x, this.y)); const len = Math.max(1e-5, Math.hypot(this.x, this.y));
return new Vector(this.x / len, this.y / len); return new Vector(this.x / len, this.y / len);
} }
/** /**
* Normalizes the vector, dividing by the length(), and return a new vector * Normalizes the vector, dividing by the length(), and return a new vector
* {}
*/ */
normalizeIfGreaterOne(): Vector { normalizeIfGreaterOne(): Vector {
const len = Math.max(1, Math.hypot(this.x, this.y)); const len = Math.max(1, Math.hypot(this.x, this.y));
return new Vector(this.x / len, this.y / len); return new Vector(this.x / len, this.y / len);
} }
/** /**
* Returns the normalized vector to the other point * Returns the normalized vector to the other point
* {}
*/ */
normalizedDirection(v: Vector): Vector { normalizedDirection(v: Vector): Vector {
const dx = v.x - this.x; const dx = v.x - this.x;
@ -326,52 +317,55 @@ export class Vector {
const len = Math.max(1e-5, Math.hypot(dx, dy)); const len = Math.max(1e-5, Math.hypot(dx, dy));
return new Vector(dx / len, dy / len); return new Vector(dx / len, dy / len);
} }
/** /**
* Returns a perpendicular vector * Returns a perpendicular vector
* {}
*/ */
findPerpendicular(): Vector { findPerpendicular(): Vector {
return new Vector(-this.y, this.x); return new Vector(-this.y, this.x);
} }
/** /**
* Returns the unnormalized direction to the other point * Returns the unnormalized direction to the other point
* {}
*/ */
direction(v: Vector): Vector { direction(v: Vector): Vector {
return new Vector(v.x - this.x, v.y - this.y); return new Vector(v.x - this.x, v.y - this.y);
} }
/** /**
* Returns a string representation of the vector * Returns a string representation of the vector
* {}
*/ */
toString(): string { toString(): string {
return this.x + "," + this.y; return this.x + "," + this.y;
} }
/** /**
* Compares both vectors for exact equality. Does not do an epsilon compare * Compares both vectors for exact equality. Does not do an epsilon compare
* {}
*/ */
equals(v: Vector): Boolean { equals(v: Vector): Boolean {
return this.x === v.x && this.y === v.y; return this.x === v.x && this.y === v.y;
} }
/** /**
* Rotates this vector * Rotates this vector
* {} new vector * @returns new vector
*/ */
rotated(angle: number): Vector { rotated(angle: number): Vector {
const sin = Math.sin(angle); const sin = Math.sin(angle);
const cos = Math.cos(angle); const cos = Math.cos(angle);
return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos); return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
} }
/** /**
* Rotates this vector * Rotates this vector
* {} this vector * @returns this vector
*/ */
rotateInplaceFastMultipleOf90(angle: number): Vector { rotateInplaceFastMultipleOf90(angle: number): Vector {
// const sin = Math.sin(angle); // const sin = Math.sin(angle);
// const cos = Math.cos(angle); // const cos = Math.cos(angle);
// let sin = 0, cos = 1; // let sin = 0, cos = 1;
assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle); assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle);
switch (angle) { switch (angle) {
case 0: case 0:
case 360: { case 360: {
@ -380,6 +374,7 @@ export class Vector {
case 90: { case 90: {
// sin = 1; // sin = 1;
// cos = 0; // cos = 0;
const x = this.x; const x = this.x;
this.x = -this.y; this.x = -this.y;
this.y = x; this.y = x;
@ -388,6 +383,7 @@ export class Vector {
case 180: { case 180: {
// sin = 0 // sin = 0
// cos = -1 // cos = -1
this.x = -this.x; this.x = -this.x;
this.y = -this.y; this.y = -this.y;
return this; return this;
@ -395,6 +391,7 @@ export class Vector {
case 270: { case 270: {
// sin = -1 // sin = -1
// cos = 0 // cos = 0
const x = this.x; const x = this.x;
this.x = this.y; this.x = this.y;
this.y = -x; 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); // return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
} }
/** /**
* Rotates this vector * Rotates this vector
* {} new vector * @returns new vector
*/ */
rotateFastMultipleOf90(angle: number): Vector { rotateFastMultipleOf90(angle: number): Vector {
assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle); assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle);
switch (angle) { switch (angle) {
case 360: case 360:
case 0: { case 0: {
@ -433,9 +432,9 @@ export class Vector {
} }
} }
} }
/** /**
* Helper method to rotate a direction * Helper method to rotate a direction
* {}
*/ */
static transformDirectionFromMultipleOf90(direction: enumDirection, angle: number): enumDirection { static transformDirectionFromMultipleOf90(direction: enumDirection, angle: number): enumDirection {
if (angle === 0 || angle === 360) { if (angle === 0 || angle === 360) {
@ -500,74 +499,72 @@ export class Vector {
return; return;
} }
} }
/** /**
* Compares both vectors for epsilon equality * Compares both vectors for epsilon equality
* {}
*/ */
equalsEpsilon(v: Vector, epsilon = 1e-5): Boolean { equalsEpsilon(v: Vector, epsilon = 1e-5): Boolean {
return Math.abs(this.x - v.x) < 1e-5 && Math.abs(this.y - v.y) < epsilon; return Math.abs(this.x - v.x) < 1e-5 && Math.abs(this.y - v.y) < epsilon;
} }
/** /**
* Returns the angle * Returns the angle
* {} 0 .. 2 PI * @returns 0 .. 2 PI
*/ */
angle(): number { angle(): number {
return Math.atan2(this.y, this.x) + Math.PI / 2; return Math.atan2(this.y, this.x) + Math.PI / 2;
} }
/** /**
* Serializes the vector to a string * Serializes the vector to a string
* {}
*/ */
serializeTile(): string { serializeTile(): string {
return String.fromCharCode(33 + this.x) + String.fromCharCode(33 + this.y); return String.fromCharCode(33 + this.x) + String.fromCharCode(33 + this.y);
} }
/** /**
* Creates a simple representation of the vector * Creates a simple representation of the vector
*/ */
serializeSimple() { serializeSimple() {
return { x: this.x, y: this.y }; return { x: this.x, y: this.y };
} }
/**
* {}
*/
serializeTileToInt(): number { serializeTileToInt(): number {
return this.x + this.y * 256; return this.x + this.y * 256;
} }
/**
*
* {}
*/
static deserializeTileFromInt(i: number): Vector { static deserializeTileFromInt(i: number): Vector {
const x = i % 256; const x = i % 256;
const y = Math.floor(i / 256); const y = Math.floor(i / 256);
return new Vector(x, y); return new Vector(x, y);
} }
/** /**
* Deserializes a vector from a string * Deserializes a vector from a string
* {}
*/ */
static deserializeTile(s: string): Vector { static deserializeTile(s: string): Vector {
return new Vector(s.charCodeAt(0) - 33, s.charCodeAt(1) - 33); return new Vector(s.charCodeAt(0) - 33, s.charCodeAt(1) - 33);
} }
/** /**
* Deserializes a vector from a serialized json object * Deserializes a vector from a serialized json object
* {}
*/ */
static fromSerializedObject(obj: object): Vector { static fromSerializedObject(obj: any): Vector {
if (obj) { if (obj) {
return new Vector(obj.x || 0, obj.y || 0); 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 * 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) { 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); return new Vector(v1.x * (1 - a) + v2.x * a, v1.y * (1 - a) + v2.y * a);
} }
/** /**
* Mapping from string direction to actual vector * Mapping from string direction to actual vector
* @enum {Vector}
*/ */
export const enumDirectionToVector = { export const enumDirectionToVector = {
top: new Vector(0, -1), top: new Vector(0, -1),