mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-06-13 13:04:03 +00:00
Cleaned up /core
This commit is contained in:
parent
4dd57c1605
commit
c399e0d7f1
@ -1,16 +1,21 @@
|
|||||||
import { Signal } from "./signal";
|
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;
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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 */
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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 {
|
||||||
|
@ -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--;
|
||||||
|
@ -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) {
|
||||||
|
@ -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() {}
|
||||||
}
|
}
|
||||||
|
@ -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]]] : []),
|
||||||
|
[]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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()) {
|
||||||
|
@ -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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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, ")");
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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),
|
||||||
|
Loading…
Reference in New Issue
Block a user