parent
5a10e73dc5
commit
d3fe689b31
@ -0,0 +1 @@
|
||||
built-temp
|
@ -0,0 +1,357 @@
|
||||
import { AnimationFrame } from "./core/animation_frame";
|
||||
import { BackgroundResourcesLoader } from "./core/background_resources_loader";
|
||||
import { IS_MOBILE } from "./core/config";
|
||||
import { GameState } from "./core/game_state";
|
||||
import { GLOBAL_APP, setGlobalApp } from "./core/globals";
|
||||
import { InputDistributor } from "./core/input_distributor";
|
||||
import { Loader } from "./core/loader";
|
||||
import { createLogger, logSection } from "./core/logging";
|
||||
import { StateManager } from "./core/state_manager";
|
||||
import { TrackedState } from "./core/tracked_state";
|
||||
import { getPlatformName, waitNextFrame } from "./core/utils";
|
||||
import { Vector } from "./core/vector";
|
||||
import { AdProviderInterface } from "./platform/ad_provider";
|
||||
import { NoAdProvider } from "./platform/ad_providers/no_ad_provider";
|
||||
import { NoAchievementProvider } from "./platform/browser/no_achievement_provider";
|
||||
import { AnalyticsInterface } from "./platform/analytics";
|
||||
import { GoogleAnalyticsImpl } from "./platform/browser/google_analytics";
|
||||
import { SoundImplBrowser } from "./platform/browser/sound";
|
||||
import { PlatformWrapperImplBrowser } from "./platform/browser/wrapper";
|
||||
import { PlatformWrapperImplElectron } from "./platform/electron/wrapper";
|
||||
import { PlatformWrapperInterface } from "./platform/wrapper";
|
||||
import { ApplicationSettings } from "./profile/application_settings";
|
||||
import { SavegameManager } from "./savegame/savegame_manager";
|
||||
import { AboutState } from "./states/about";
|
||||
import { ChangelogState } from "./states/changelog";
|
||||
import { InGameState } from "./states/ingame";
|
||||
import { KeybindingsState } from "./states/keybindings";
|
||||
import { MainMenuState } from "./states/main_menu";
|
||||
import { MobileWarningState } from "./states/mobile_warning";
|
||||
import { PreloadState } from "./states/preload";
|
||||
import { SettingsState } from "./states/settings";
|
||||
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
|
||||
import { RestrictionManager } from "./core/restriction_manager";
|
||||
import { PuzzleMenuState } from "./states/puzzle_menu";
|
||||
import { ClientAPI } from "./platform/api";
|
||||
import { LoginState } from "./states/login";
|
||||
import { WegameSplashState } from "./states/wegame_splash";
|
||||
import { MODS } from "./mods/modloader";
|
||||
import { MOD_SIGNALS } from "./mods/mod_signals";
|
||||
import { ModsState } from "./states/mods";
|
||||
export type AchievementProviderInterface = import("./platform/achievement_provider").AchievementProviderInterface;
|
||||
export type SoundInterface = import("./platform/sound").SoundInterface;
|
||||
export type StorageInterface = import("./platform/storage").StorageInterface;
|
||||
|
||||
const logger: any = createLogger("application");
|
||||
// Set the name of the hidden property and the change event for visibility
|
||||
let pageHiddenPropName: any, pageVisibilityEventName: any;
|
||||
if (typeof document.hidden !== "undefined") {
|
||||
// Opera 12.10 and Firefox 18 and later support
|
||||
pageHiddenPropName = "hidden";
|
||||
pageVisibilityEventName = "visibilitychange";
|
||||
// @ts-ignore
|
||||
}
|
||||
else if (typeof document.msHidden !== "undefined") {
|
||||
pageHiddenPropName = "msHidden";
|
||||
pageVisibilityEventName = "msvisibilitychange";
|
||||
// @ts-ignore
|
||||
}
|
||||
else if (typeof document.webkitHidden !== "undefined") {
|
||||
pageHiddenPropName = "webkitHidden";
|
||||
pageVisibilityEventName = "webkitvisibilitychange";
|
||||
}
|
||||
export class Application {
|
||||
/**
|
||||
* Boots the application
|
||||
*/
|
||||
async boot(): any {
|
||||
console.log("Booting ...");
|
||||
assert(!GLOBAL_APP, "Tried to construct application twice");
|
||||
logger.log("Creating application, platform =", getPlatformName());
|
||||
setGlobalApp(this);
|
||||
MODS.app = this;
|
||||
// MODS
|
||||
try {
|
||||
await MODS.initMods();
|
||||
}
|
||||
catch (ex: any) {
|
||||
alert("Failed to load mods (launch with --dev for more info): \n\n" + ex);
|
||||
}
|
||||
this.unloaded = false;
|
||||
// Global stuff
|
||||
this.settings = new ApplicationSettings(this);
|
||||
this.ticker = new AnimationFrame();
|
||||
this.stateMgr = new StateManager(this);
|
||||
this.savegameMgr = new SavegameManager(this);
|
||||
this.inputMgr = new InputDistributor(this);
|
||||
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
|
||||
this.clientApi = new ClientAPI(this);
|
||||
// Restrictions (Like demo etc)
|
||||
this.restrictionMgr = new RestrictionManager(this);
|
||||
// Platform dependent stuff
|
||||
this.storage = null;
|
||||
this.sound = null;
|
||||
this.platformWrapper = null;
|
||||
this.achievementProvider = null;
|
||||
this.adProvider = null;
|
||||
this.analytics = null;
|
||||
this.gameAnalytics = null;
|
||||
this.initPlatformDependentInstances();
|
||||
// Track if the window is focused (only relevant for browser)
|
||||
this.focused = true;
|
||||
// Track if the window is visible
|
||||
this.pageVisible = true;
|
||||
// Track if the app is paused (cordova)
|
||||
this.applicationPaused = false;
|
||||
this.trackedIsRenderable = new TrackedState(this.onAppRenderableStateChanged, this);
|
||||
this.trackedIsPlaying = new TrackedState(this.onAppPlayingStateChanged, this);
|
||||
// Dimensions
|
||||
this.screenWidth = 0;
|
||||
this.screenHeight = 0;
|
||||
// Store the timestamp where we last checked for a screen resize, since orientationchange is unreliable with cordova
|
||||
this.lastResizeCheck = null;
|
||||
// Store the mouse position, or null if not available
|
||||
this.mousePosition = null;
|
||||
this.registerStates();
|
||||
this.registerEventListeners();
|
||||
Loader.linkAppAfterBoot(this);
|
||||
// Check for mobile
|
||||
if (IS_MOBILE) {
|
||||
this.stateMgr.moveToState("MobileWarningState");
|
||||
}
|
||||
else {
|
||||
this.stateMgr.moveToState("PreloadState");
|
||||
}
|
||||
// Starting rendering
|
||||
this.ticker.frameEmitted.add(this.onFrameEmitted, this);
|
||||
this.ticker.bgFrameEmitted.add(this.onBackgroundFrame, this);
|
||||
this.ticker.start();
|
||||
window.focus();
|
||||
MOD_SIGNALS.appBooted.dispatch();
|
||||
}
|
||||
/**
|
||||
* Initializes all platform instances
|
||||
*/
|
||||
initPlatformDependentInstances(): any {
|
||||
logger.log("Creating platform dependent instances (standalone=", G_IS_STANDALONE, ")");
|
||||
if (G_IS_STANDALONE) {
|
||||
this.platformWrapper = new PlatformWrapperImplElectron(this);
|
||||
}
|
||||
else {
|
||||
this.platformWrapper = new PlatformWrapperImplBrowser(this);
|
||||
}
|
||||
// Start with empty ad provider
|
||||
this.adProvider = new NoAdProvider(this);
|
||||
this.sound = new SoundImplBrowser(this);
|
||||
this.analytics = new GoogleAnalyticsImpl(this);
|
||||
this.gameAnalytics = new ShapezGameAnalytics(this);
|
||||
this.achievementProvider = new NoAchievementProvider(this);
|
||||
}
|
||||
/**
|
||||
* Registers all game states
|
||||
*/
|
||||
registerStates(): any {
|
||||
const states: Array<typeof GameState> = [
|
||||
WegameSplashState,
|
||||
PreloadState,
|
||||
MobileWarningState,
|
||||
MainMenuState,
|
||||
InGameState,
|
||||
SettingsState,
|
||||
KeybindingsState,
|
||||
AboutState,
|
||||
ChangelogState,
|
||||
PuzzleMenuState,
|
||||
LoginState,
|
||||
ModsState,
|
||||
];
|
||||
for (let i: any = 0; i < states.length; ++i) {
|
||||
this.stateMgr.register(states[i]);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Registers all event listeners
|
||||
*/
|
||||
registerEventListeners(): any {
|
||||
window.addEventListener("focus", this.onFocus.bind(this));
|
||||
window.addEventListener("blur", this.onBlur.bind(this));
|
||||
window.addEventListener("resize", (): any => this.checkResize(), true);
|
||||
window.addEventListener("orientationchange", (): any => this.checkResize(), true);
|
||||
window.addEventListener("mousemove", this.handleMousemove.bind(this));
|
||||
window.addEventListener("mouseout", this.handleMousemove.bind(this));
|
||||
window.addEventListener("mouseover", this.handleMousemove.bind(this));
|
||||
window.addEventListener("mouseleave", this.handleMousemove.bind(this));
|
||||
// Unload events
|
||||
window.addEventListener("beforeunload", this.onBeforeUnload.bind(this), true);
|
||||
document.addEventListener(pageVisibilityEventName, this.handleVisibilityChange.bind(this), false);
|
||||
// Track touches so we can update the focus appropriately
|
||||
document.addEventListener("touchstart", this.updateFocusAfterUserInteraction.bind(this), true);
|
||||
document.addEventListener("touchend", this.updateFocusAfterUserInteraction.bind(this), true);
|
||||
}
|
||||
/**
|
||||
* Checks the focus after a touch
|
||||
*/
|
||||
updateFocusAfterUserInteraction(event: TouchEvent): any {
|
||||
const target: any = (event.target as HTMLElement);
|
||||
if (!target || !target.tagName) {
|
||||
// Safety check
|
||||
logger.warn("Invalid touchstart/touchend event:", event);
|
||||
return;
|
||||
}
|
||||
// When clicking an element which is not the currently focused one, defocus it
|
||||
if (target !== document.activeElement) {
|
||||
// @ts-ignore
|
||||
if (document.activeElement.blur) {
|
||||
// @ts-ignore
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}
|
||||
// If we click an input field, focus it now
|
||||
if (target.tagName.toLowerCase() === "input") {
|
||||
// We *really* need the focus
|
||||
waitNextFrame().then((): any => target.focus());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handles a page visibility change event
|
||||
*/
|
||||
handleVisibilityChange(event: Event): any {
|
||||
window.focus();
|
||||
const pageVisible: any = !document[pageHiddenPropName];
|
||||
if (pageVisible !== this.pageVisible) {
|
||||
this.pageVisible = pageVisible;
|
||||
logger.log("Visibility changed:", this.pageVisible);
|
||||
this.trackedIsRenderable.set(this.isRenderable());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handles a mouse move event
|
||||
*/
|
||||
handleMousemove(event: MouseEvent): any {
|
||||
this.mousePosition = new Vector(event.clientX, event.clientY);
|
||||
}
|
||||
/**
|
||||
* Internal on focus handler
|
||||
*/
|
||||
onFocus(): any {
|
||||
this.focused = true;
|
||||
}
|
||||
/**
|
||||
* Internal blur handler
|
||||
*/
|
||||
onBlur(): any {
|
||||
this.focused = false;
|
||||
}
|
||||
/**
|
||||
* Returns if the app is currently visible
|
||||
*/
|
||||
isRenderable(): any {
|
||||
return !this.applicationPaused && this.pageVisible;
|
||||
}
|
||||
onAppRenderableStateChanged(renderable: any): any {
|
||||
logger.log("Application renderable:", renderable);
|
||||
window.focus();
|
||||
const currentState: any = this.stateMgr.getCurrentState();
|
||||
if (!renderable) {
|
||||
if (currentState) {
|
||||
currentState.onAppPause();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (currentState) {
|
||||
currentState.onAppResume();
|
||||
}
|
||||
this.checkResize();
|
||||
}
|
||||
this.sound.onPageRenderableStateChanged(renderable);
|
||||
}
|
||||
onAppPlayingStateChanged(playing: any): any {
|
||||
try {
|
||||
this.adProvider.setPlayStatus(playing);
|
||||
}
|
||||
catch (ex: any) {
|
||||
console.warn("Play status changed");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Internal before-unload handler
|
||||
*/
|
||||
onBeforeUnload(event: any): any {
|
||||
logSection("BEFORE UNLOAD HANDLER", "#f77");
|
||||
const currentState: any = this.stateMgr.getCurrentState();
|
||||
if (!G_IS_DEV && currentState && currentState.getHasUnloadConfirmation()) {
|
||||
if (!G_IS_STANDALONE) {
|
||||
// Need to show a "Are you sure you want to exit"
|
||||
event.preventDefault();
|
||||
event.returnValue = "Are you sure you want to exit?";
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Deinitializes the application
|
||||
*/
|
||||
deinitialize(): any {
|
||||
return this.sound.deinitialize();
|
||||
}
|
||||
/**
|
||||
* Background frame update callback
|
||||
*/
|
||||
onBackgroundFrame(dt: number): any {
|
||||
if (this.isRenderable()) {
|
||||
return;
|
||||
}
|
||||
const currentState: any = this.stateMgr.getCurrentState();
|
||||
if (currentState) {
|
||||
currentState.onBackgroundTick(dt);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Frame update callback
|
||||
*/
|
||||
onFrameEmitted(dt: number): any {
|
||||
if (!this.isRenderable()) {
|
||||
return;
|
||||
}
|
||||
const time: any = performance.now();
|
||||
// Periodically check for resizes, this is expensive (takes 2-3ms so only do it once in a while!)
|
||||
if (!this.lastResizeCheck || time - this.lastResizeCheck > 1000) {
|
||||
this.checkResize();
|
||||
this.lastResizeCheck = time;
|
||||
}
|
||||
const currentState: any = this.stateMgr.getCurrentState();
|
||||
this.trackedIsPlaying.set(currentState && currentState.getIsIngame());
|
||||
if (currentState) {
|
||||
currentState.onRender(dt);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks if the app resized. Only does this once in a while
|
||||
*/
|
||||
checkResize(forceUpdate: boolean = false): any {
|
||||
const w: any = window.innerWidth;
|
||||
const h: any = window.innerHeight;
|
||||
if (this.screenWidth !== w || this.screenHeight !== h || forceUpdate) {
|
||||
this.screenWidth = w;
|
||||
this.screenHeight = h;
|
||||
const currentState: any = this.stateMgr.getCurrentState();
|
||||
if (currentState) {
|
||||
currentState.onResized(this.screenWidth, this.screenHeight);
|
||||
}
|
||||
const scale: any = this.getEffectiveUiScale();
|
||||
waitNextFrame().then((): any => document.documentElement.style.setProperty("--ui-scale", `${scale}`));
|
||||
window.focus();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the effective ui sclae
|
||||
*/
|
||||
getEffectiveUiScale(): any {
|
||||
return this.platformWrapper.getUiScale() * this.settings.getInterfaceScaleValue();
|
||||
}
|
||||
/**
|
||||
* Callback after ui scale has changed
|
||||
*/
|
||||
updateAfterUiScaleChanged(): any {
|
||||
this.checkResize(true);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import { Signal } from "./signal";
|
||||
// @ts-ignore
|
||||
import BackgroundAnimationFrameEmitterWorker from "../webworkers/background_animation_frame_emittter.worker";
|
||||
import { createLogger } from "./logging";
|
||||
const logger: any = createLogger("animation_frame");
|
||||
const maxDtMs: any = 1000;
|
||||
const resetDtMs: any = 16;
|
||||
export class AnimationFrame {
|
||||
public frameEmitted = new Signal();
|
||||
public bgFrameEmitted = new Signal();
|
||||
public lastTime = performance.now();
|
||||
public bgLastTime = performance.now();
|
||||
public boundMethod = this.handleAnimationFrame.bind(this);
|
||||
public backgroundWorker = new BackgroundAnimationFrameEmitterWorker();
|
||||
|
||||
constructor() {
|
||||
this.backgroundWorker.addEventListener("error", (err: any): any => {
|
||||
logger.error("Error in background fps worker:", err);
|
||||
});
|
||||
this.backgroundWorker.addEventListener("message", this.handleBackgroundTick.bind(this));
|
||||
}
|
||||
handleBackgroundTick(): any {
|
||||
const time: any = performance.now();
|
||||
let dt: any = time - this.bgLastTime;
|
||||
if (dt > maxDtMs) {
|
||||
dt = resetDtMs;
|
||||
}
|
||||
this.bgFrameEmitted.dispatch(dt);
|
||||
this.bgLastTime = time;
|
||||
}
|
||||
start(): any {
|
||||
assertAlways(window.requestAnimationFrame, "requestAnimationFrame is not supported!");
|
||||
this.handleAnimationFrame();
|
||||
}
|
||||
handleAnimationFrame(time: any): any {
|
||||
let dt: any = time - this.lastTime;
|
||||
if (dt > maxDtMs) {
|
||||
dt = resetDtMs;
|
||||
}
|
||||
try {
|
||||
this.frameEmitted.dispatch(dt);
|
||||
}
|
||||
catch (ex: any) {
|
||||
console.error(ex);
|
||||
}
|
||||
this.lastTime = time;
|
||||
window.requestAnimationFrame(this.boundMethod);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { createLogger } from "./logging";
|
||||
const logger: any = createLogger("assert");
|
||||
let assertionErrorShown: any = false;
|
||||
function initAssert(): any {
|
||||
/**
|
||||
* Expects a given condition to be true
|
||||
* @param {} failureMessage
|
||||
*/
|
||||
// @ts-ignore
|
||||
window.assert = function (condition: Boolean, ...failureMessage: ...String): any {
|
||||
if (!condition) {
|
||||
logger.error("assertion failed:", ...failureMessage);
|
||||
if (!assertionErrorShown) {
|
||||
// alert("Assertion failed (the game will try to continue to run): \n\n" + failureMessage);
|
||||
assertionErrorShown = true;
|
||||
}
|
||||
throw new Error("AssertionError: " + failureMessage.join(" "));
|
||||
}
|
||||
};
|
||||
}
|
||||
initAssert();
|
@ -0,0 +1,95 @@
|
||||
// @ts-ignore
|
||||
import CompressionWorker from "../webworkers/compression.worker";
|
||||
import { createLogger } from "./logging";
|
||||
import { round2Digits } from "./utils";
|
||||
const logger: any = createLogger("async_compression");
|
||||
export let compressionPrefix: any = String.fromCodePoint(1);
|
||||
function checkCryptPrefix(prefix: any): any {
|
||||
try {
|
||||
window.localStorage.setItem("prefix_test", prefix);
|
||||
window.localStorage.removeItem("prefix_test");
|
||||
return true;
|
||||
}
|
||||
catch (ex: any) {
|
||||
logger.warn("Prefix '" + prefix + "' not available");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!checkCryptPrefix(compressionPrefix)) {
|
||||
logger.warn("Switching to basic prefix");
|
||||
compressionPrefix = " ";
|
||||
if (!checkCryptPrefix(compressionPrefix)) {
|
||||
logger.warn("Prefix not available, ls seems to be unavailable");
|
||||
}
|
||||
}
|
||||
export type JobEntry = {
|
||||
errorHandler: function(: void):void;
|
||||
resolver: function(: void):void;
|
||||
startTime: number;
|
||||
};
|
||||
|
||||
class AsynCompression {
|
||||
public worker = new CompressionWorker();
|
||||
public currentJobId = 1000;
|
||||
public currentJobs: {
|
||||
[idx: number]: JobEntry;
|
||||
} = {};
|
||||
|
||||
constructor() {
|
||||
this.worker.addEventListener("message", (event: any): any => {
|
||||
const { jobId, result }: any = event.data;
|
||||
const jobData: any = this.currentJobs[jobId];
|
||||
if (!jobData) {
|
||||
logger.error("Failed to resolve job result, job id", jobId, "is not known");
|
||||
return;
|
||||
}
|
||||
const duration: any = performance.now() - jobData.startTime;
|
||||
logger.log("Got job", jobId, "response within", round2Digits(duration), "ms: ", result.length, "bytes");
|
||||
const resolver: any = jobData.resolver;
|
||||
delete this.currentJobs[jobId];
|
||||
resolver(result);
|
||||
});
|
||||
this.worker.addEventListener("error", (err: any): any => {
|
||||
logger.error("Got error from webworker:", err, "aborting all jobs");
|
||||
const failureCalls: any = [];
|
||||
for (const jobId: any in this.currentJobs) {
|
||||
failureCalls.push(this.currentJobs[jobId].errorHandler);
|
||||
}
|
||||
this.currentJobs = {};
|
||||
for (let i: any = 0; i < failureCalls.length; ++i) {
|
||||
failureCalls[i](err);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Compresses any object
|
||||
*/
|
||||
compressObjectAsync(obj: any): any {
|
||||
logger.log("Compressing object async (optimized)");
|
||||
return this.internalQueueJob("compressObject", {
|
||||
obj,
|
||||
compressionPrefix,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Queues a new job
|
||||
* {}
|
||||
*/
|
||||
internalQueueJob(job: string, data: any): Promise<any> {
|
||||
const jobId: any = ++this.currentJobId;
|
||||
return new Promise((resolve: any, reject: any): any => {
|
||||
const errorHandler: any = (err: any): any => {
|
||||
logger.error("Failed to compress job", jobId, ":", err);
|
||||
reject(err);
|
||||
};
|
||||
this.currentJobs[jobId] = {
|
||||
errorHandler,
|
||||
resolver: resolve,
|
||||
startTime: performance.now(),
|
||||
};
|
||||
logger.log("Posting job", job, "/", jobId);
|
||||
this.worker.postMessage({ jobId, job, data });
|
||||
});
|
||||
}
|
||||
}
|
||||
export const asyncCompressor: any = new AsynCompression();
|
@ -0,0 +1,47 @@
|
||||
|
||||
export type Size = {
|
||||
w: number;
|
||||
h: number;
|
||||
};
|
||||
export type Position = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
export type SpriteDefinition = {
|
||||
frame: Position & Size;
|
||||
rotated: boolean;
|
||||
spriteSourceSize: Position & Size;
|
||||
sourceSize: Size;
|
||||
trimmed: boolean;
|
||||
};
|
||||
export type AtlasMeta = {
|
||||
app: string;
|
||||
version: string;
|
||||
image: string;
|
||||
format: string;
|
||||
size: Size;
|
||||
scale: string;
|
||||
smartupdate: string;
|
||||
};
|
||||
export type SourceData = {
|
||||
frames: Object<string, SpriteDefinition>;
|
||||
meta: AtlasMeta;
|
||||
};
|
||||
export class AtlasDefinition {
|
||||
public meta = meta;
|
||||
public sourceData = frames;
|
||||
public sourceFileName = meta.image;
|
||||
|
||||
constructor({ frames, meta }) {
|
||||
}
|
||||
getFullSourcePath(): any {
|
||||
return this.sourceFileName;
|
||||
}
|
||||
}
|
||||
export const atlasFiles: AtlasDefinition[] = require
|
||||
// @ts-ignore
|
||||
.context("../../../res_built/atlas/", false, /.*\.json/i)
|
||||
.keys()
|
||||
.map((f: any): any => f.replace(/^\.\//gi, ""))
|
||||
.map((f: any): any => require("../../../res_built/atlas/" + f))
|
||||
.map((data: any): any => new AtlasDefinition(data));
|
@ -0,0 +1,192 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end */
|
||||
import { initSpriteCache } from "../game/meta_building_registry";
|
||||
import { MUSIC, SOUNDS } from "../platform/sound";
|
||||
import { T } from "../translations";
|
||||
import { AtlasDefinition, atlasFiles } from "./atlas_definitions";
|
||||
import { cachebust } from "./cachebust";
|
||||
import { Loader } from "./loader";
|
||||
import { createLogger } from "./logging";
|
||||
import { Signal } from "./signal";
|
||||
import { clamp, getLogoSprite, timeoutPromise } from "./utils";
|
||||
const logger: any = createLogger("background_loader");
|
||||
const MAIN_MENU_ASSETS: any = {
|
||||
sprites: [getLogoSprite()],
|
||||
sounds: [SOUNDS.uiClick, SOUNDS.uiError, SOUNDS.dialogError, SOUNDS.dialogOk],
|
||||
atlas: [],
|
||||
css: [],
|
||||
};
|
||||
const INGAME_ASSETS: any = {
|
||||
sprites: [],
|
||||
sounds: [
|
||||
...Array.from(Object.values(MUSIC)),
|
||||
...Array.from(Object.values(SOUNDS)).filter((sound: any): any => !MAIN_MENU_ASSETS.sounds.includes(sound)),
|
||||
],
|
||||
atlas: atlasFiles,
|
||||
css: ["async-resources.css"],
|
||||
};
|
||||
if (G_IS_STANDALONE) {
|
||||
MAIN_MENU_ASSETS.sounds = [...Array.from(Object.values(MUSIC)), ...Array.from(Object.values(SOUNDS))];
|
||||
INGAME_ASSETS.sounds = [];
|
||||
}
|
||||
const LOADER_TIMEOUT_PER_RESOURCE: any = 180000;
|
||||
// Cloudflare does not send content-length headers with brotli compression,
|
||||
// so store the actual (compressed) file sizes so we can show a progress bar.
|
||||
const HARDCODED_FILE_SIZES: any = {
|
||||
"async-resources.css": 2216145,
|
||||
};
|
||||
export class BackgroundResourcesLoader {
|
||||
public app = app;
|
||||
public mainMenuPromise = null;
|
||||
public ingamePromise = null;
|
||||
public resourceStateChangedSignal = new Signal();
|
||||
|
||||
constructor(app) {
|
||||
}
|
||||
getMainMenuPromise(): any {
|
||||
if (this.mainMenuPromise) {
|
||||
return this.mainMenuPromise;
|
||||
}
|
||||
logger.log("⏰ Loading main menu assets");
|
||||
return (this.mainMenuPromise = this.loadAssets(MAIN_MENU_ASSETS));
|
||||
}
|
||||
getIngamePromise(): any {
|
||||
if (this.ingamePromise) {
|
||||
return this.ingamePromise;
|
||||
}
|
||||
logger.log("⏰ Loading ingame assets");
|
||||
const promise: any = this.loadAssets(INGAME_ASSETS).then((): any => initSpriteCache());
|
||||
return (this.ingamePromise = promise);
|
||||
}
|
||||
async loadAssets({ sprites, sounds, atlas, css }: {
|
||||
sprites: string[];
|
||||
sounds: string[];
|
||||
atlas: AtlasDefinition[];
|
||||
css: string[];
|
||||
}): any {
|
||||
let promiseFunctions: ((progressHandler: (progress: number) => void) => Promise<void>)[] = [];
|
||||
// CSS
|
||||
for (let i: any = 0; i < css.length; ++i) {
|
||||
promiseFunctions.push((progress: any): any => timeoutPromise(this.internalPreloadCss(css[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch((err: any): any => {
|
||||
logger.error("Failed to load css:", css[i], err);
|
||||
throw new Error("HUD Stylesheet " + css[i] + " failed to load: " + err);
|
||||
}));
|
||||
}
|
||||
// ATLAS FILES
|
||||
for (let i: any = 0; i < atlas.length; ++i) {
|
||||
promiseFunctions.push((progress: any): any => timeoutPromise(Loader.preloadAtlas(atlas[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch((err: any): any => {
|
||||
logger.error("Failed to load atlas:", atlas[i].sourceFileName, err);
|
||||
throw new Error("Atlas " + atlas[i].sourceFileName + " failed to load: " + err);
|
||||
}));
|
||||
}
|
||||
// HUD Sprites
|
||||
for (let i: any = 0; i < sprites.length; ++i) {
|
||||
promiseFunctions.push((progress: any): any => timeoutPromise(Loader.preloadCSSSprite(sprites[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch((err: any): any => {
|
||||
logger.error("Failed to load css sprite:", sprites[i], err);
|
||||
throw new Error("HUD Sprite " + sprites[i] + " failed to load: " + err);
|
||||
}));
|
||||
}
|
||||
// SFX & Music
|
||||
for (let i: any = 0; i < sounds.length; ++i) {
|
||||
promiseFunctions.push((progress: any): any => timeoutPromise(this.app.sound.loadSound(sounds[i]), LOADER_TIMEOUT_PER_RESOURCE).catch((err: any): any => {
|
||||
logger.warn("Failed to load sound, will not be available:", sounds[i], err);
|
||||
}));
|
||||
}
|
||||
const originalAmount: any = promiseFunctions.length;
|
||||
const start: any = performance.now();
|
||||
logger.log("⏰ Preloading", originalAmount, "assets");
|
||||
let progress: any = 0;
|
||||
this.resourceStateChangedSignal.dispatch({ progress });
|
||||
let promises: any = [];
|
||||
for (let i: any = 0; i < promiseFunctions.length; i++) {
|
||||
let lastIndividualProgress: any = 0;
|
||||
const progressHandler: any = (individualProgress: any): any => {
|
||||
const delta: any = clamp(individualProgress) - lastIndividualProgress;
|
||||
lastIndividualProgress = clamp(individualProgress);
|
||||
progress += delta / originalAmount;
|
||||
this.resourceStateChangedSignal.dispatch({ progress });
|
||||
};
|
||||
promises.push(promiseFunctions[i](progressHandler).then((): any => {
|
||||
progressHandler(1);
|
||||
}));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
logger.log("⏰ Preloaded assets in", Math.round(performance.now() - start), "ms");
|
||||
}
|
||||
/**
|
||||
* Shows an error when a resource failed to load and allows to reload the game
|
||||
*/
|
||||
showLoaderError(dialogs: any, err: any): any {
|
||||
if (G_IS_STANDALONE) {
|
||||
dialogs
|
||||
.showWarning(T.dialogs.resourceLoadFailed.title, T.dialogs.resourceLoadFailed.descSteamDemo + "<br>" + err, ["retry"])
|
||||
.retry.add((): any => window.location.reload());
|
||||
}
|
||||
else {
|
||||
dialogs
|
||||
.showWarning(T.dialogs.resourceLoadFailed.title, T.dialogs.resourceLoadFailed.descWeb.replace("<demoOnSteamLinkText>", `<a href="https://get.shapez.io/resource_timeout" target="_blank">${T.dialogs.resourceLoadFailed.demoLinkText}</a>`) +
|
||||
"<br>" +
|
||||
err, ["retry"])
|
||||
.retry.add((): any => window.location.reload());
|
||||
}
|
||||
}
|
||||
preloadWithProgress(src: any, progressHandler: any): any {
|
||||
return new Promise((resolve: any, reject: any): any => {
|
||||
const xhr: any = new XMLHttpRequest();
|
||||
let notifiedNotComputable: any = false;
|
||||
const fullUrl: any = cachebust(src);
|
||||
xhr.open("GET", fullUrl, true);
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.onprogress = function (ev: any): any {
|
||||
if (ev.lengthComputable) {
|
||||
progressHandler(ev.loaded / ev.total);
|
||||
}
|
||||
else {
|
||||
if (window.location.search.includes("alwaysLogFileSize")) {
|
||||
console.warn("Progress:", src, ev.loaded);
|
||||
}
|
||||
if (HARDCODED_FILE_SIZES[src]) {
|
||||
progressHandler(clamp(ev.loaded / HARDCODED_FILE_SIZES[src]));
|
||||
}
|
||||
else {
|
||||
if (!notifiedNotComputable) {
|
||||
notifiedNotComputable = true;
|
||||
console.warn("Progress not computable:", src, ev.loaded);
|
||||
progressHandler(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.onloadend = function (): any {
|
||||
if (!xhr.status.toString().match(/^2/)) {
|
||||
reject(fullUrl + ": " + xhr.status + " " + xhr.statusText);
|
||||
}
|
||||
else {
|
||||
if (!notifiedNotComputable) {
|
||||
progressHandler(1);
|
||||
}
|
||||
const options: any = {};
|
||||
const headers: any = xhr.getAllResponseHeaders();
|
||||
const contentType: any = headers.match(/^Content-Type:\s*(.*?)$/im);
|
||||
if (contentType && contentType[1]) {
|
||||
options.type = contentType[1].split(";")[0];
|
||||
}
|
||||
const blob: any = new Blob([this.response], options);
|
||||
resolve(window.URL.createObjectURL(blob));
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
internalPreloadCss(src: any, progressHandler: any): any {
|
||||
return this.preloadWithProgress(src, progressHandler).then((blobSrc: any): any => {
|
||||
var styleElement: any = document.createElement("link");
|
||||
styleElement.href = blobSrc;
|
||||
styleElement.rel = "stylesheet";
|
||||
styleElement.setAttribute("media", "all");
|
||||
styleElement.type = "text/css";
|
||||
document.head.appendChild(styleElement);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
import { GameRoot } from "../game/root";
|
||||
import { clearBufferBacklog, freeCanvas, getBufferStats, makeOffscreenBuffer } from "./buffer_utils";
|
||||
import { createLogger } from "./logging";
|
||||
import { round1Digit } from "./utils";
|
||||
export type CacheEntry = {
|
||||
canvas: HTMLCanvasElement;
|
||||
context: CanvasRenderingContext2D;
|
||||
lastUse: number;
|
||||
};
|
||||
|
||||
const logger: any = createLogger("buffers");
|
||||
const bufferGcDurationSeconds: any = 0.5;
|
||||
export class BufferMaintainer {
|
||||
public root = root;
|
||||
public cache: Map<string, Map<string, CacheEntry>> = new Map();
|
||||
public iterationIndex = 1;
|
||||
public lastIteration = 0;
|
||||
|
||||
constructor(root) {
|
||||
this.root.signals.gameFrameStarted.add(this.update, this);
|
||||
}
|
||||
/**
|
||||
* Returns the buffer stats
|
||||
*/
|
||||
getStats(): any {
|
||||
let stats: any = {
|
||||
rootKeys: 0,
|
||||
subKeys: 0,
|
||||
vramBytes: 0,
|
||||
};
|
||||
this.cache.forEach((subCache: any, key: any): any => {
|
||||
++stats.rootKeys;
|
||||
subCache.forEach((cacheEntry: any, subKey: any): any => {
|
||||
++stats.subKeys;
|
||||
const canvas: any = cacheEntry.canvas;
|
||||
stats.vramBytes += canvas.width * canvas.height * 4;
|
||||
});
|
||||
});
|
||||
return stats;
|
||||
}
|
||||
/**
|
||||
* Goes to the next buffer iteration, clearing all buffers which were not used
|
||||
* for a few iterations
|
||||
*/
|
||||
garbargeCollect(): any {
|
||||
let totalKeys: any = 0;
|
||||
let deletedKeys: any = 0;
|
||||
const minIteration: any = this.iterationIndex;
|
||||
this.cache.forEach((subCache: any, key: any): any => {
|
||||
let unusedSubKeys: any = [];
|
||||
// Filter sub cache
|
||||
subCache.forEach((cacheEntry: any, subKey: any): any => {
|
||||
if (cacheEntry.lastUse < minIteration ||
|
||||
// @ts-ignore
|
||||
cacheEntry.canvas._contextLost) {
|
||||
unusedSubKeys.push(subKey);
|
||||
freeCanvas(cacheEntry.canvas);
|
||||
++deletedKeys;
|
||||
}
|
||||
else {
|
||||
++totalKeys;
|
||||
}
|
||||
});
|
||||
// Delete unused sub keys
|
||||
for (let i: any = 0; i < unusedSubKeys.length; ++i) {
|
||||
subCache.delete(unusedSubKeys[i]);
|
||||
}
|
||||
});
|
||||
// Make sure our backlog never gets too big
|
||||
clearBufferBacklog();
|
||||
// if (G_IS_DEV) {
|
||||
// const bufferStats = getBufferStats();
|
||||
// const mbUsed = round1Digit(bufferStats.vramUsage / (1024 * 1024));
|
||||
// logger.log(
|
||||
// "GC: Remove",
|
||||
// (deletedKeys + "").padStart(4),
|
||||
// ", Remain",
|
||||
// (totalKeys + "").padStart(4),
|
||||
// "(",
|
||||
// (bufferStats.bufferCount + "").padStart(4),
|
||||
// "total",
|
||||
// ")",
|
||||
// "(",
|
||||
// (bufferStats.backlogSize + "").padStart(4),
|
||||
// "backlog",
|
||||
// ")",
|
||||
// "VRAM:",
|
||||
// mbUsed,
|
||||
// "MB"
|
||||
// );
|
||||
// }
|
||||
++this.iterationIndex;
|
||||
}
|
||||
update(): any {
|
||||
const now: any = this.root.time.realtimeNow();
|
||||
if (now - this.lastIteration > bufferGcDurationSeconds) {
|
||||
this.lastIteration = now;
|
||||
this.garbargeCollect();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*
|
||||
*/
|
||||
getForKey({ key, subKey, w, h, dpi, redrawMethod, additionalParams }: {
|
||||
key: string;
|
||||
subKey: string;
|
||||
w: number;
|
||||
h: number;
|
||||
dpi: number;
|
||||
redrawMethod: function(: void, : void, : void, : void, : void, : void):void;
|
||||
additionalParams: object=;
|
||||
}): HTMLCanvasElement {
|
||||
// First, create parent key
|
||||
let parent: any = this.cache.get(key);
|
||||
if (!parent) {
|
||||
parent = new Map();
|
||||
this.cache.set(key, parent);
|
||||
}
|
||||
// Now search for sub key
|
||||
const cacheHit: any = parent.get(subKey);
|
||||
if (cacheHit) {
|
||||
cacheHit.lastUse = this.iterationIndex;
|
||||
return cacheHit.canvas;
|
||||
}
|
||||
// Need to generate new buffer
|
||||
const effectiveWidth: any = w * dpi;
|
||||
const effectiveHeight: any = h * dpi;
|
||||
const [canvas, context]: any = makeOffscreenBuffer(effectiveWidth, effectiveHeight, {
|
||||
reusable: true,
|
||||
label: "buffer-" + key + "/" + subKey,
|
||||
smooth: true,
|
||||
});
|
||||
redrawMethod(canvas, context, w, h, dpi, additionalParams);
|
||||
parent.set(subKey, {
|
||||
canvas,
|
||||
context,
|
||||
lastUse: this.iterationIndex,
|
||||
});
|
||||
return canvas;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*
|
||||
*/
|
||||
getForKeyOrNullNoUpdate({ key, subKey }: {
|
||||
key: string;
|
||||
subKey: string;
|
||||
}): ?HTMLCanvasElement {
|
||||
let parent: any = this.cache.get(key);
|
||||
if (!parent) {
|
||||
return null;
|
||||
}
|
||||
// Now search for sub key
|
||||
const cacheHit: any = parent.get(subKey);
|
||||
if (cacheHit) {
|
||||
return cacheHit.canvas;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
import { globalConfig } from "./config";
|
||||
import { fastArrayDelete } from "./utils";
|
||||
import { createLogger } from "./logging";
|
||||
const logger: any = createLogger("buffer_utils");
|
||||
/**
|
||||
* Enables images smoothing on a context
|
||||
*/
|
||||
export function enableImageSmoothing(context: CanvasRenderingContext2D): any {
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.webkitImageSmoothingEnabled = true;
|
||||
// @ts-ignore
|
||||
context.imageSmoothingQuality = globalConfig.smoothing.quality;
|
||||
}
|
||||
/**
|
||||
* Disables image smoothing on a context
|
||||
*/
|
||||
export function disableImageSmoothing(context: CanvasRenderingContext2D): any {
|
||||
context.imageSmoothingEnabled = false;
|
||||
context.webkitImageSmoothingEnabled = false;
|
||||
}
|
||||
export type CanvasCacheEntry = {
|
||||
canvas: HTMLCanvasElement;
|
||||
context: CanvasRenderingContext2D;
|
||||
};
|
||||
|
||||
const registeredCanvas: Array<CanvasCacheEntry> = [];
|
||||
/**
|
||||
* Buckets for each width * height combination
|
||||
*/
|
||||
const freeCanvasBuckets: Map<number, Array<CanvasCacheEntry>> = new Map();
|
||||
/**
|
||||
* Track statistics
|
||||
*/
|
||||
const stats: any = {
|
||||
vramUsage: 0,
|
||||
backlogVramUsage: 0,
|
||||
bufferCount: 0,
|
||||
numReused: 0,
|
||||
numCreated: 0,
|
||||
};
|
||||
export function getBufferVramUsageBytes(canvas: HTMLCanvasElement): any {
|
||||
assert(canvas, "no canvas given");
|
||||
assert(Number.isFinite(canvas.width), "bad canvas width: " + canvas.width);
|
||||
assert(Number.isFinite(canvas.height), "bad canvas height" + canvas.height);
|
||||
return canvas.width * canvas.height * 4;
|
||||
}
|
||||
/**
|
||||
* Returns stats on the allocated buffers
|
||||
*/
|
||||
export function getBufferStats(): any {
|
||||
let numBuffersFree: any = 0;
|
||||
freeCanvasBuckets.forEach((bucket: any): any => {
|
||||
numBuffersFree += bucket.length;
|
||||
});
|
||||
return {
|
||||
...stats,
|
||||
backlogKeys: freeCanvasBuckets.size,
|
||||
backlogSize: numBuffersFree,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Clears the backlog buffers if they grew too much
|
||||
*/
|
||||
export function clearBufferBacklog(): any {
|
||||
freeCanvasBuckets.forEach((bucket: any): any => {
|
||||
while (bucket.length > 500) {
|
||||
const entry: any = bucket[bucket.length - 1];
|
||||
stats.backlogVramUsage -= getBufferVramUsageBytes(entry.canvas);
|
||||
delete entry.canvas;
|
||||
delete entry.context;
|
||||
bucket.pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Creates a new offscreen buffer
|
||||
* {}
|
||||
*/
|
||||
export function makeOffscreenBuffer(w: Number, h: Number, { smooth = true, reusable = true, label = "buffer" }: any): [
|
||||
HTMLCanvasElement,
|
||||
CanvasRenderingContext2D
|
||||
] {
|
||||
assert(w > 0 && h > 0, "W or H < 0");
|
||||
if (w % 1 !== 0 || h % 1 !== 0) {
|
||||
// console.warn("Subpixel offscreen buffer size:", w, h);
|
||||
}
|
||||
if (w < 1 || h < 1) {
|
||||
logger.error("Offscreen buffer size < 0:", w, "x", h);
|
||||
w = Math.max(1, w);
|
||||
h = Math.max(1, h);
|
||||
}
|
||||
const recommendedSize: any = 1024 * 1024;
|
||||
if (w * h > recommendedSize) {
|
||||
logger.warn("Creating huge buffer:", w, "x", h, "with label", label);
|
||||
}
|
||||
w = Math.floor(w);
|
||||
h = Math.floor(h);
|
||||
let canvas: any = null;
|
||||
let context: any = null;
|
||||
// Ok, search in cache first
|
||||
const bucket: any = freeCanvasBuckets.get(w * h) || [];
|
||||
for (let i: any = 0; i < bucket.length; ++i) {
|
||||
const { canvas: useableCanvas, context: useableContext }: any = bucket[i];
|
||||
if (useableCanvas.width === w && useableCanvas.height === h) {
|
||||
// Ok we found one
|
||||
canvas = useableCanvas;
|
||||
context = useableContext;
|
||||
// Restore past state
|
||||
context.restore();
|
||||
context.save();
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
delete canvas.style.width;
|
||||
delete canvas.style.height;
|
||||
stats.numReused++;
|
||||
stats.backlogVramUsage -= getBufferVramUsageBytes(canvas);
|
||||
fastArrayDelete(bucket, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// None found , create new one
|
||||
if (!canvas) {
|
||||
canvas = document.createElement("canvas");
|
||||
context = canvas.getContext("2d" /*, { alpha } */);
|
||||
stats.numCreated++;
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
// Initial state
|
||||
context.save();
|
||||
canvas.addEventListener("webglcontextlost", (): any => {
|
||||
console.warn("canvas::webglcontextlost", canvas);
|
||||
// @ts-ignore
|
||||
canvas._contextLost = true;
|
||||
});
|
||||
canvas.addEventListener("contextlost", (): any => {
|
||||
console.warn("canvas::contextlost", canvas);
|
||||
// @ts-ignore
|
||||
canvas._contextLost = true;
|
||||
});
|
||||
}
|
||||
// @ts-ignore
|
||||
canvas._contextLost = false;
|
||||
// @ts-ignore
|
||||
canvas.label = label;
|
||||
if (smooth) {
|
||||
enableImageSmoothing(context);
|
||||
}
|
||||
else {
|
||||
disableImageSmoothing(context);
|
||||
}
|
||||
if (reusable) {
|
||||
registerCanvas(canvas, context);
|
||||
}
|
||||
return [canvas, context];
|
||||
}
|
||||
/**
|
||||
* Frees a canvas
|
||||
*/
|
||||
export function registerCanvas(canvas: HTMLCanvasElement, context: any): any {
|
||||
registeredCanvas.push({ canvas, context });
|
||||
stats.bufferCount += 1;
|
||||
const bytesUsed: any = getBufferVramUsageBytes(canvas);
|
||||
stats.vramUsage += bytesUsed;
|
||||
}
|
||||
/**
|
||||
* Frees a canvas
|
||||
*/
|
||||
export function freeCanvas(canvas: HTMLCanvasElement): any {
|
||||
assert(canvas, "Canvas is empty");
|
||||
let index: any = -1;
|
||||
let data: any = null;
|
||||
for (let i: any = 0; i < registeredCanvas.length; ++i) {
|
||||
if (registeredCanvas[i].canvas === canvas) {
|
||||
index = i;
|
||||
data = registeredCanvas[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index < 0) {
|
||||
logger.error("Tried to free unregistered canvas of size", canvas.width, canvas.height);
|
||||
return;
|
||||
}
|
||||
fastArrayDelete(registeredCanvas, index);
|
||||
const key: any = canvas.width * canvas.height;
|
||||
const bucket: any = freeCanvasBuckets.get(key);
|
||||
if (bucket) {
|
||||
bucket.push(data);
|
||||
}
|
||||
else {
|
||||
freeCanvasBuckets.set(key, [data]);
|
||||
}
|
||||
stats.bufferCount -= 1;
|
||||
const bytesUsed: any = getBufferVramUsageBytes(canvas);
|
||||
stats.vramUsage -= bytesUsed;
|
||||
stats.backlogVramUsage += bytesUsed;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Generates a cachebuster string. This only modifies the path in the browser version
|
||||
*/
|
||||
export function cachebust(path: string): any {
|
||||
if (G_IS_BROWSER && !G_IS_STANDALONE && !G_IS_DEV) {
|
||||
return "/v/" + G_BUILD_COMMIT_HASH + "/" + path;
|
||||
}
|
||||
return path;
|
||||
}
|
@ -0,0 +1,351 @@
|
||||
import { createLogger } from "./logging";
|
||||
import { Signal } from "./signal";
|
||||
import { fastArrayDelete, fastArrayDeleteValueIfContained } from "./utils";
|
||||
import { Vector } from "./vector";
|
||||
import { IS_MOBILE, SUPPORT_TOUCH } from "./config";
|
||||
import { SOUNDS } from "../platform/sound";
|
||||
import { GLOBAL_APP } from "./globals";
|
||||
const logger: any = createLogger("click_detector");
|
||||
export const MAX_MOVE_DISTANCE_PX: any = IS_MOBILE ? 20 : 80;
|
||||
// For debugging
|
||||
const registerClickDetectors: any = G_IS_DEV && true;
|
||||
if (registerClickDetectors) {
|
||||
window.activeClickDetectors = [];
|
||||
}
|
||||
// Store active click detectors so we can cancel them
|
||||
const ongoingClickDetectors: Array<ClickDetector> = [];
|
||||
// Store when the last touch event was registered, to avoid accepting a touch *and* a click event
|
||||
export let clickDetectorGlobals: any = {
|
||||
lastTouchTime: -1000,
|
||||
};
|
||||
export type ClickDetectorConstructorArgs = {
|
||||
consumeEvents?: boolean;
|
||||
preventDefault?: boolean;
|
||||
applyCssClass?: string;
|
||||
captureTouchmove?: boolean;
|
||||
targetOnly?: boolean;
|
||||
maxDistance?: number;
|
||||
clickSound?: string;
|
||||
preventClick?: boolean;
|
||||
};
|
||||
|
||||
// Detects clicks
|
||||
export class ClickDetector {
|
||||
public clickDownPosition = null;
|
||||
public consumeEvents = consumeEvents;
|
||||
public preventDefault = preventDefault;
|
||||
public applyCssClass = applyCssClass;
|
||||
public captureTouchmove = captureTouchmove;
|
||||
public targetOnly = targetOnly;
|
||||
public clickSound = clickSound;
|
||||
public maxDistance = maxDistance;
|
||||
public preventClick = preventClick;
|
||||
public click = new Signal();
|
||||
public rightClick = new Signal();
|
||||
public touchstart = new Signal();
|
||||
public touchmove = new Signal();
|
||||
public touchend = new Signal();
|
||||
public touchcancel = new Signal();
|
||||
public touchstartSimple = new Signal();
|
||||
public touchmoveSimple = new Signal();
|
||||
public touchendSimple = new Signal();
|
||||
public clickStartTime = null;
|
||||
public cancelled = false;
|
||||
|
||||
constructor(element, { consumeEvents = false, preventDefault = true, applyCssClass = "pressed", captureTouchmove = false, targetOnly = false, maxDistance = MAX_MOVE_DISTANCE_PX, clickSound = SOUNDS.uiClick, preventClick = false, }) {
|
||||
assert(element, "No element given!");
|
||||
this.internalBindTo(element as HTMLElement));
|
||||
}
|
||||
/**
|
||||
* Cleans up all event listeners of this detector
|
||||
*/
|
||||
cleanup(): any {
|
||||
if (this.element) {
|
||||
if (registerClickDetectors) {
|
||||
const index: any = window.activeClickDetectors.indexOf(this);
|
||||
if (index < 0) {
|
||||
logger.error("Click detector cleanup but is not active");
|
||||
}
|
||||
else {
|
||||
window.activeClickDetectors.splice(index, 1);
|
||||
}
|
||||
}
|
||||
const options: any = this.internalGetEventListenerOptions();
|
||||
if (SUPPORT_TOUCH) {
|
||||
this.element.removeEventListener("touchstart", this.handlerTouchStart, options);
|
||||
this.element.removeEventListener("touchend", this.handlerTouchEnd, options);
|
||||
this.element.removeEventListener("touchcancel", this.handlerTouchCancel, options);
|
||||
}
|
||||
this.element.removeEventListener("mouseup", this.handlerTouchStart, options);
|
||||
this.element.removeEventListener("mousedown", this.handlerTouchEnd, options);
|
||||
this.element.removeEventListener("mouseout", this.handlerTouchCancel, options);
|
||||
if (this.captureTouchmove) {
|
||||
if (SUPPORT_TOUCH) {
|
||||
this.element.removeEventListener("touchmove", this.handlerTouchMove, options);
|
||||
}
|
||||
this.element.removeEventListener("mousemove", this.handlerTouchMove, options);
|
||||
}
|
||||
if (this.preventClick) {
|
||||
this.element.removeEventListener("click", this.handlerPreventClick, options);
|
||||
}
|
||||
this.click.removeAll();
|
||||
this.touchstart.removeAll();
|
||||
this.touchmove.removeAll();
|
||||
this.touchend.removeAll();
|
||||
this.touchcancel.removeAll();
|
||||
this.element = null;
|
||||
}
|
||||
}
|
||||
// INTERNAL METHODS
|
||||
internalPreventClick(event: Event): any {
|
||||
window.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
/**
|
||||
* Internal method to get the options to pass to an event listener
|
||||
*/
|
||||
internalGetEventListenerOptions(): any {
|
||||
return {
|
||||
capture: this.consumeEvents,
|
||||
passive: !this.preventDefault,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Binds the click detector to an element
|
||||
*/
|
||||
internalBindTo(element: HTMLElement): any {
|
||||
const options: any = this.internalGetEventListenerOptions();
|
||||
this.handlerTouchStart = this.internalOnPointerDown.bind(this);
|
||||
this.handlerTouchEnd = this.internalOnPointerEnd.bind(this);
|
||||
this.handlerTouchMove = this.internalOnPointerMove.bind(this);
|
||||
this.handlerTouchCancel = this.internalOnTouchCancel.bind(this);
|
||||
if (this.preventClick) {
|
||||
this.handlerPreventClick = this.internalPreventClick.bind(this);
|
||||
element.addEventListener("click", this.handlerPreventClick, options);
|
||||
}
|
||||
if (SUPPORT_TOUCH) {
|
||||
element.addEventListener("touchstart", this.handlerTouchStart, options);
|
||||
element.addEventListener("touchend", this.handlerTouchEnd, options);
|
||||
element.addEventListener("touchcancel", this.handlerTouchCancel, options);
|
||||
}
|
||||
element.addEventListener("mousedown", this.handlerTouchStart, options);
|
||||
element.addEventListener("mouseup", this.handlerTouchEnd, options);
|
||||
element.addEventListener("mouseout", this.handlerTouchCancel, options);
|
||||
if (this.captureTouchmove) {
|
||||
if (SUPPORT_TOUCH) {
|
||||
element.addEventListener("touchmove", this.handlerTouchMove, options);
|
||||
}
|
||||
element.addEventListener("mousemove", this.handlerTouchMove, options);
|
||||
}
|
||||
if (registerClickDetectors) {
|
||||
window.activeClickDetectors.push(this);
|
||||
}
|
||||
this.element = element;
|
||||
}
|
||||
/**
|
||||
* Returns if the bound element is currently in the DOM.
|
||||
*/
|
||||
internalIsDomElementAttached(): any {
|
||||
return this.element && document.documentElement.contains(this.element);
|
||||
}
|
||||
/**
|
||||
* Checks if the given event is relevant for this detector
|
||||
*/
|
||||
internalEventPreHandler(event: TouchEvent | MouseEvent, expectedRemainingTouches: any = 1): any {
|
||||
if (!this.element) {
|
||||
// Already cleaned up
|
||||
return false;
|
||||
}
|
||||
if (this.targetOnly && event.target !== this.element) {
|
||||
// Clicked a child element
|
||||
return false;
|
||||
}
|
||||
// Stop any propagation and defaults if configured
|
||||
if (this.consumeEvents && event.cancelable) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (this.preventDefault && event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (window.TouchEvent && event instanceof TouchEvent) {
|
||||
clickDetectorGlobals.lastTouchTime = performance.now();
|
||||
// console.log("Got touches", event.targetTouches.length, "vs", expectedRemainingTouches);
|
||||
if (event.targetTouches.length !== expectedRemainingTouches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (event instanceof MouseEvent) {
|
||||
if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Extracts the mous position from an event
|
||||
* {} The client space position
|
||||
*/
|
||||
static extractPointerPosition(event: TouchEvent | MouseEvent): Vector {
|
||||
if (window.TouchEvent && event instanceof TouchEvent) {
|
||||
if (event.changedTouches.length !== 1) {
|
||||
logger.warn("Got unexpected target touches:", event.targetTouches.length, "->", event.targetTouches);
|
||||
return new Vector(0, 0);
|
||||
}
|
||||
const touch: any = event.changedTouches[0];
|
||||
return new Vector(touch.clientX, touch.clientY);
|
||||
}
|
||||
if (event instanceof MouseEvent) {
|
||||
return new Vector(event.clientX, event.clientY);
|
||||
}
|
||||
assertAlways(false, "Got unknown event: " + event);
|
||||
return new Vector(0, 0);
|
||||
}
|
||||
/**
|
||||
* Cacnels all ongoing events on this detector
|
||||
*/
|
||||
cancelOngoingEvents(): any {
|
||||
if (this.applyCssClass && this.element) {
|
||||
this.element.classList.remove(this.applyCssClass);
|
||||
}
|
||||
this.clickDownPosition = null;
|
||||
this.clickStartTime = null;
|
||||
this.cancelled = true;
|
||||
fastArrayDeleteValueIfContained(ongoingClickDetectors, this);
|
||||
}
|
||||
/**
|
||||
* Internal pointer down handler
|
||||
*/
|
||||
internalOnPointerDown(event: TouchEvent | MouseEvent): any {
|
||||
window.focus();
|
||||
if (!this.internalEventPreHandler(event, 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const position: any = (this.constructor as typeof ClickDetector).extractPointerPosition(event);
|
||||
if (event instanceof MouseEvent) {
|
||||
const isRightClick: any = event.button === 2;
|
||||
if (isRightClick) {
|
||||
// Ignore right clicks
|
||||
this.rightClick.dispatch(position, event);
|
||||
this.cancelled = true;
|
||||
this.clickDownPosition = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.clickDownPosition) {
|
||||
logger.warn("Ignoring double click");
|
||||
return false;
|
||||
}
|
||||
this.cancelled = false;
|
||||
this.touchstart.dispatch(event);
|
||||
// Store where the touch started
|
||||
this.clickDownPosition = position;
|
||||
this.clickStartTime = performance.now();
|
||||
this.touchstartSimple.dispatch(this.clickDownPosition.x, this.clickDownPosition.y);
|
||||
// If we are not currently within a click, register it
|
||||
if (ongoingClickDetectors.indexOf(this) < 0) {
|
||||
ongoingClickDetectors.push(this);
|
||||
}
|
||||
else {
|
||||
logger.warn("Click detector got pointer down of active pointer twice");
|
||||
}
|
||||
// If we should apply any classes, do this now
|
||||
if (this.applyCssClass) {
|
||||
this.element.classList.add(this.applyCssClass);
|
||||
}
|
||||
// If we should play any sound, do this
|
||||
if (this.clickSound) {
|
||||
GLOBAL_APP.sound.playUiSound(this.clickSound);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Internal pointer move handler
|
||||
*/
|
||||
internalOnPointerMove(event: TouchEvent | MouseEvent): any {
|
||||
if (!this.internalEventPreHandler(event, 1)) {
|
||||
return false;
|
||||
}
|
||||
this.touchmove.dispatch(event);
|
||||
|
||||
const pos: any = (this.constructor as typeof ClickDetector).extractPointerPosition(event);
|
||||
this.touchmoveSimple.dispatch(pos.x, pos.y);
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Internal pointer end handler
|
||||
*/
|
||||
internalOnPointerEnd(event: TouchEvent | MouseEvent): any {
|
||||
window.focus();
|
||||
if (!this.internalEventPreHandler(event, 0)) {
|
||||
return false;
|
||||
}
|
||||
if (this.cancelled) {
|
||||
// warn(this, "Not dispatching touchend on cancelled listener");
|
||||
return false;
|
||||
}
|
||||
if (event instanceof MouseEvent) {
|
||||
const isRightClick: any = event.button === 2;
|
||||
if (isRightClick) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const index: any = ongoingClickDetectors.indexOf(this);
|
||||
if (index < 0) {
|
||||
logger.warn("Got pointer end but click detector is not in pressed state");
|
||||
}
|
||||
else {
|
||||
fastArrayDelete(ongoingClickDetectors, index);
|
||||
}
|
||||
let dispatchClick: any = false;
|
||||
let dispatchClickPos: any = null;
|
||||
// Check for correct down position, otherwise must have pinched or so
|
||||
if (this.clickDownPosition) {
|
||||
|
||||
const pos: any = (this.constructor as typeof ClickDetector).extractPointerPosition(event);
|
||||
const distance: any = pos.distance(this.clickDownPosition);
|
||||
if (!IS_MOBILE || distance <= this.maxDistance) {
|
||||
dispatchClick = true;
|
||||
dispatchClickPos = pos;
|
||||
}
|
||||
else {
|
||||
console.warn("[ClickDetector] Touch does not count as click:", "(was", distance, ")");
|
||||
}
|
||||
}
|
||||
this.clickDownPosition = null;
|
||||
this.clickStartTime = null;
|
||||
if (this.applyCssClass) {
|
||||
this.element.classList.remove(this.applyCssClass);
|
||||
}
|
||||
// Dispatch in the end to avoid the element getting invalidated
|
||||
// Also make sure that the element is still in the dom
|
||||
if (this.internalIsDomElementAttached()) {
|
||||
this.touchend.dispatch(event);
|
||||
this.touchendSimple.dispatch();
|
||||
if (dispatchClick) {
|
||||
const detectors: any = ongoingClickDetectors.slice();
|
||||
for (let i: any = 0; i < detectors.length; ++i) {
|
||||
detectors[i].cancelOngoingEvents();
|
||||
}
|
||||
this.click.dispatch(dispatchClickPos, event);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Internal touch cancel handler
|
||||
*/
|
||||
internalOnTouchCancel(event: TouchEvent | MouseEvent): any {
|
||||
if (!this.internalEventPreHandler(event, 0)) {
|
||||
return false;
|
||||
}
|
||||
if (this.cancelled) {
|
||||
// warn(this, "Not dispatching touchcancel on cancelled listener");
|
||||
return false;
|
||||
}
|
||||
this.cancelOngoingEvents();
|
||||
this.touchcancel.dispatch(event);
|
||||
this.touchendSimple.dispatch(event);
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
export default {
|
||||
// You can set any debug options here!
|
||||
/* dev:start */
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Quickly enters the game and skips the main menu - good for fast iterating
|
||||
// fastGameEnter: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Skips any delays like transitions between states and such
|
||||
// noArtificialDelays: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables writing of savegames, useful for testing the same savegame over and over
|
||||
// disableSavegameWrite: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Shows bounds of all entities
|
||||
// showEntityBounds: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Shows arrows for every ejector / acceptor
|
||||
// showAcceptorEjectors: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the music (Overrides any setting, can cause weird behaviour)
|
||||
// disableMusic: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Do not render static map entities (=most buildings)
|
||||
// doNotRenderStatics: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allow to zoom freely without limits
|
||||
// disableZoomLimits: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// All rewards can be unlocked by passing just 1 of any shape
|
||||
// rewardsInstant: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Unlocks all buildings
|
||||
// allBuildingsUnlocked: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables cost of blueprints
|
||||
// blueprintsNoCost: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables cost of upgrades
|
||||
// upgradesNoCost: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the dialog when completing a level
|
||||
// disableUnlockDialog: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the simulation - This effectively pauses the game.
|
||||
// disableLogicTicks: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Test the rendering if everything is clipped out properly
|
||||
// testClipping: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allows to render slower, useful for recording at half speed to avoid stuttering
|
||||
// framePausesBetweenTicks: 250,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Replace all translations with emojis to see which texts are translateable
|
||||
// testTranslations: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Enables an inspector which shows information about the entity below the cursor
|
||||
// enableEntityInspector: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Enables ads in the local build (normally they are deactivated there)
|
||||
// testAds: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allows unlocked achievements to be logged to console in the local build
|
||||
// testAchievements: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Enables use of (some) existing flags within the puzzle mode context
|
||||
// testPuzzleMode: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the automatic switch to an overview when zooming out
|
||||
// disableMapOverview: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the notification when there are new entries in the changelog since last played
|
||||
// disableUpgradeNotification: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Makes belts almost infinitely fast
|
||||
// instantBelts: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Makes item processors almost infinitely fast
|
||||
// instantProcessors: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Makes miners almost infinitely fast
|
||||
// instantMiners: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// When using fastGameEnter, controls whether a new game is started or the last one is resumed
|
||||
// resumeGameOnFastEnter: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Special option used to render the trailer
|
||||
// renderForTrailer: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Whether to render changes
|
||||
// renderChanges: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Whether to render belt paths
|
||||
// renderBeltPaths: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Whether to check belt paths
|
||||
// checkBeltPaths: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Whether to items / s instead of items / m in stats
|
||||
// detailedStatistics: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Shows detailed information about which atlas is used
|
||||
// showAtlasInfo: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Renders the rotation of all wires
|
||||
// renderWireRotations: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Renders information about wire networks
|
||||
// renderWireNetworkInfos: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables ejector animations and processing
|
||||
// disableEjectorProcessing: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allows manual ticking
|
||||
// manualTickOnly: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables slow asserts, useful for debugging performance
|
||||
// disableSlowAsserts: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Allows to load a mod from an external source for developing it
|
||||
// externalModUrl: "http://localhost:3005/combined.js",
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Visualizes the shape grouping on belts
|
||||
// showShapeGrouping: true
|
||||
// -----------------------------------------------------------------------------------
|
||||
/* dev:end */
|
||||
};
|
@ -0,0 +1,133 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end */
|
||||
export const IS_DEBUG: any = G_IS_DEV &&
|
||||
typeof window !== "undefined" &&
|
||||
window.location.port === "3005" &&
|
||||
(window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) &&
|
||||
window.location.search.indexOf("nodebug") < 0;
|
||||
export const SUPPORT_TOUCH: any = false;
|
||||
const smoothCanvas: any = true;
|
||||
export const THIRDPARTY_URLS: any = {
|
||||
discord: "https://discord.gg/HN7EVzV",
|
||||
github: "https://github.com/tobspr-games/shapez.io",
|
||||
reddit: "https://www.reddit.com/r/shapezio",
|
||||
shapeViewer: "https://viewer.shapez.io",
|
||||
twitter: "https://twitter.com/tobspr",
|
||||
patreon: "https://www.patreon.com/tobsprgames",
|
||||
privacyPolicy: "https://tobspr.io/privacy.html",
|
||||
standaloneCampaignLink: "https://get.shapez.io/bundle/$campaign",
|
||||
puzzleDlcStorePage: "https://get.shapez.io/mm_puzzle_dlc?target=dlc",
|
||||
levelTutorialVideos: {
|
||||
21: "https://www.youtube.com/watch?v=0nUfRLMCcgo&",
|
||||
25: "https://www.youtube.com/watch?v=7OCV1g40Iew&",
|
||||
26: "https://www.youtube.com/watch?v=gfm6dS1dCoY",
|
||||
},
|
||||
modBrowser: "https://shapez.mod.io/",
|
||||
};
|
||||
export function openStandaloneLink(app: Application, campaign: string): any {
|
||||
const discount: any = globalConfig.currentDiscount > 0 ? "_discount" + globalConfig.currentDiscount : "";
|
||||
const event: any = campaign + discount;
|
||||
app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneCampaignLink.replace("$campaign", event));
|
||||
app.gameAnalytics.noteMinor("g.stdlink." + event);
|
||||
}
|
||||
export const globalConfig: any = {
|
||||
// Size of a single tile in Pixels.
|
||||
// NOTICE: Update webpack.production.config too!
|
||||
tileSize: 32,
|
||||
halfTileSize: 16,
|
||||
// Which dpi the assets have
|
||||
assetsDpi: 192 / 32,
|
||||
assetsSharpness: 1.5,
|
||||
shapesSharpness: 1.3,
|
||||
// Achievements
|
||||
achievementSliceDuration: 10,
|
||||
// Production analytics
|
||||
statisticsGraphDpi: 2.5,
|
||||
statisticsGraphSlices: 100,
|
||||
analyticsSliceDurationSeconds: G_IS_DEV ? 1 : 10,
|
||||
minimumTickRate: 25,
|
||||
maximumTickRate: 500,
|
||||
// Map
|
||||
mapChunkSize: 16,
|
||||
chunkAggregateSize: 4,
|
||||
mapChunkOverviewMinZoom: 0.9,
|
||||
mapChunkWorldSize: null,
|
||||
maxBeltShapeBundleSize: 20,
|
||||
// Belt speeds
|
||||
// NOTICE: Update webpack.production.config too!
|
||||
beltSpeedItemsPerSecond: 2,
|
||||
minerSpeedItemsPerSecond: 0,
|
||||
defaultItemDiameter: 20,
|
||||
itemSpacingOnBelts: 0.63,
|
||||
wiresSpeedItemsPerSecond: 6,
|
||||
undergroundBeltMaxTilesByTier: [5, 9],
|
||||
readerAnalyzeIntervalSeconds: 10,
|
||||
goalAcceptorItemsRequired: 12,
|
||||
goalAcceptorsPerProducer: 5,
|
||||
puzzleModeSpeed: 3,
|
||||
puzzleMinBoundsSize: 2,
|
||||
puzzleMaxBoundsSize: 20,
|
||||
puzzleValidationDurationSeconds: 30,
|
||||
buildingSpeeds: {
|
||||
cutter: 1 / 4,
|
||||
cutterQuad: 1 / 4,
|
||||
rotater: 1 / 1,
|
||||
rotaterCCW: 1 / 1,
|
||||
rotater180: 1 / 1,
|
||||
painter: 1 / 6,
|
||||
painterDouble: 1 / 8,
|
||||
painterQuad: 1 / 2,
|
||||
mixer: 1 / 5,
|
||||
stacker: 1 / 8,
|
||||
},
|
||||
// Zooming
|
||||
initialZoom: 1.9,
|
||||
minZoomLevel: 0.1,
|
||||
maxZoomLevel: 3,
|
||||
// Global game speed
|
||||
gameSpeed: 1,
|
||||
warmupTimeSecondsFast: 0.25,
|
||||
warmupTimeSecondsRegular: 0.25,
|
||||
smoothing: {
|
||||
smoothMainCanvas: smoothCanvas && true,
|
||||
quality: "low", // Low is CRUCIAL for mobile performance!
|
||||
},
|
||||
rendering: {},
|
||||
debug: require("./config.local").default,
|
||||
currentDiscount: 0,
|
||||
// Secret vars
|
||||
info: {
|
||||
// Binary file salt
|
||||
file: "Ec'])@^+*9zMevK3uMV4432x9%iK'=",
|
||||
// Savegame salt
|
||||
sgSalt: "}95Q3%8/.837Lqym_BJx%q7)pAHJbF",
|
||||
// Analytics key
|
||||
analyticsApiKey: "baf6a50f0cc7dfdec5a0e21c88a1c69a4b34bc4a",
|
||||
},
|
||||
};
|
||||
export const IS_MOBILE: any = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
// Automatic calculations
|
||||
globalConfig.minerSpeedItemsPerSecond = globalConfig.beltSpeedItemsPerSecond / 5;
|
||||
globalConfig.mapChunkWorldSize = globalConfig.mapChunkSize * globalConfig.tileSize;
|
||||
// Dynamic calculations
|
||||
if (globalConfig.debug.disableMapOverview) {
|
||||
globalConfig.mapChunkOverviewMinZoom = 0;
|
||||
}
|
||||
// Stuff for making the trailer
|
||||
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
|
||||
globalConfig.debug.framePausesBetweenTicks = 32;
|
||||
// globalConfig.mapChunkOverviewMinZoom = 0.0;
|
||||
// globalConfig.debug.instantBelts = true;
|
||||
// globalConfig.debug.instantProcessors = true;
|
||||
// globalConfig.debug.instantMiners = true;
|
||||
globalConfig.debug.disableSavegameWrite = true;
|
||||
// globalConfig.beltSpeedItemsPerSecond *= 2;
|
||||
}
|
||||
if (globalConfig.debug.fastGameEnter) {
|
||||
globalConfig.debug.noArtificialDelays = true;
|
||||
}
|
||||
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
|
||||
globalConfig.warmupTimeSecondsFast = 0;
|
||||
globalConfig.warmupTimeSecondsRegular = 0;
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
import { globalConfig } from "./config";
|
||||
import { round1Digit, round2Digits } from "./utils";
|
||||
/**
|
||||
* Returns the current dpi
|
||||
* {}
|
||||
*/
|
||||
export function getDeviceDPI(): number {
|
||||
return window.devicePixelRatio || 1;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* {} Smoothed dpi
|
||||
*/
|
||||
export function smoothenDpi(dpi: number): number {
|
||||
if (dpi < 0.05) {
|
||||
return 0.05;
|
||||
}
|
||||
else if (dpi < 0.2) {
|
||||
return round2Digits(Math.round(dpi / 0.04) * 0.04);
|
||||
}
|
||||
else if (dpi < 1) {
|
||||
return round1Digit(Math.round(dpi / 0.1) * 0.1);
|
||||
}
|
||||
else if (dpi < 4) {
|
||||
return round1Digit(Math.round(dpi / 0.5) * 0.5);
|
||||
}
|
||||
else {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
// Initial dpi
|
||||
// setDPIMultiplicator(1);
|
||||
/**
|
||||
* Prepares a context for hihg dpi rendering
|
||||
*/
|
||||
export function prepareHighDPIContext(context: CanvasRenderingContext2D, smooth: any = true): any {
|
||||
const dpi: any = getDeviceDPI();
|
||||
context.scale(dpi, dpi);
|
||||
if (smooth) {
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.webkitImageSmoothingEnabled = true;
|
||||
// @ts-ignore
|
||||
context.imageSmoothingQuality = globalConfig.smoothing.quality;
|
||||
}
|
||||
else {
|
||||
context.imageSmoothingEnabled = false;
|
||||
context.webkitImageSmoothingEnabled = false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resizes a high dpi canvas
|
||||
*/
|
||||
export function resizeHighDPICanvas(canvas: HTMLCanvasElement, w: number, h: number, smooth: any = true): any {
|
||||
const dpi: any = getDeviceDPI();
|
||||
const wNumber: any = Math.floor(w);
|
||||
const hNumber: any = Math.floor(h);
|
||||
const targetW: any = Math.floor(wNumber * dpi);
|
||||
const targetH: any = Math.floor(hNumber * dpi);
|
||||
if (targetW !== canvas.width || targetH !== canvas.height) {
|
||||
// console.log("Resize Canvas from", canvas.width, canvas.height, "to", targetW, targetH)
|
||||
canvas.width = targetW;
|
||||
canvas.height = targetH;
|
||||
canvas.style.width = wNumber + "px";
|
||||
canvas.style.height = hNumber + "px";
|
||||
prepareHighDPIContext(canvas.getContext("2d"), smooth);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resizes a canvas
|
||||
*/
|
||||
export function resizeCanvas(canvas: HTMLCanvasElement, w: number, h: number, setStyle: any = true): any {
|
||||
const actualW: any = Math.ceil(w);
|
||||
const actualH: any = Math.ceil(h);
|
||||
if (actualW !== canvas.width || actualH !== canvas.height) {
|
||||
canvas.width = actualW;
|
||||
canvas.height = actualH;
|
||||
if (setStyle) {
|
||||
canvas.style.width = actualW + "px";
|
||||
canvas.style.height = actualH + "px";
|
||||
}
|
||||
// console.log("Resizing canvas to", actualW, "x", actualH);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resizes a canvas and makes sure its cleared
|
||||
*/
|
||||
export function resizeCanvasAndClear(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, w: number, h: number): any {
|
||||
const actualW: any = Math.ceil(w);
|
||||
const actualH: any = Math.ceil(h);
|
||||
if (actualW !== canvas.width || actualH !== canvas.height) {
|
||||
canvas.width = actualW;
|
||||
canvas.height = actualH;
|
||||
canvas.style.width = actualW + "px";
|
||||
canvas.style.height = actualH + "px";
|
||||
// console.log("Resizing canvas to", actualW, "x", actualH);
|
||||
}
|
||||
else {
|
||||
context.clearRect(0, 0, actualW, actualH);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { globalConfig } from "./config";
|
||||
export type GameRoot = import("../game/root").GameRoot;
|
||||
export type Rectangle = import("./rectangle").Rectangle;
|
||||
|
||||
export class DrawParameters {
|
||||
public context: CanvasRenderingContext2D = context;
|
||||
public visibleRect: Rectangle = visibleRect;
|
||||
public desiredAtlasScale: string = desiredAtlasScale;
|
||||
public zoomLevel: number = zoomLevel;
|
||||
public root: GameRoot = root;
|
||||
|
||||
constructor({ context, visibleRect, desiredAtlasScale, zoomLevel, root }) {
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
|
||||
export type AtlasSprite = import("./sprites").AtlasSprite;
|
||||
export type DrawParameters = import("./draw_parameters").DrawParameters;
|
||||
import { globalConfig } from "./config";
|
||||
import { createLogger } from "./logging";
|
||||
import { Rectangle } from "./rectangle";
|
||||
const logger: any = createLogger("draw_utils");
|
||||
export function initDrawUtils(): any {
|
||||
CanvasRenderingContext2D.prototype.beginRoundedRect = function (x: any, y: any, w: any, h: any, r: any): any {
|
||||
this.beginPath();
|
||||
if (r < 0.05) {
|
||||
this.rect(x, y, w, h);
|
||||
return;
|
||||
}
|
||||
if (w < 2 * r) {
|
||||
r = w / 2;
|
||||
}
|
||||
if (h < 2 * r) {
|
||||
r = h / 2;
|
||||
}
|
||||
this.moveTo(x + r, y);
|
||||
this.arcTo(x + w, y, x + w, y + h, r);
|
||||
this.arcTo(x + w, y + h, x, y + h, r);
|
||||
this.arcTo(x, y + h, x, y, r);
|
||||
this.arcTo(x, y, x + w, y, r);
|
||||
};
|
||||
CanvasRenderingContext2D.prototype.beginCircle = function (x: any, y: any, r: any): any {
|
||||
this.beginPath();
|
||||
if (r < 0.05) {
|
||||
this.rect(x, y, 1, 1);
|
||||
return;
|
||||
}
|
||||
this.arc(x, y, r, 0, 2.0 * Math.PI);
|
||||
};
|
||||
}
|
||||
export function drawRotatedSprite({ parameters, sprite, x, y, angle, size, offsetX = 0, offsetY = 0 }: {
|
||||
parameters: DrawParameters;
|
||||
sprite: AtlasSprite;
|
||||
x: number;
|
||||
y: number;
|
||||
angle: number;
|
||||
size: number;
|
||||
offsetX: number=;
|
||||
offsetY: number=;
|
||||
}): any {
|
||||
if (angle === 0) {
|
||||
sprite.drawCachedCentered(parameters, x + offsetX, y + offsetY, size);
|
||||
return;
|
||||
}
|
||||
parameters.context.translate(x, y);
|
||||
parameters.context.rotate(angle);
|
||||
sprite.drawCachedCentered(parameters, offsetX, offsetY, size, false);
|
||||
parameters.context.rotate(-angle);
|
||||
parameters.context.translate(-x, -y);
|
||||
}
|
||||
let warningsShown: any = 0;
|
||||
/**
|
||||
* Draws a sprite with clipping
|
||||
*/
|
||||
export function drawSpriteClipped({ parameters, sprite, x, y, w, h, originalW, originalH }: {
|
||||
parameters: DrawParameters;
|
||||
sprite: HTMLCanvasElement;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
originalW: number;
|
||||
originalH: number;
|
||||
}): any {
|
||||
const rect: any = new Rectangle(x, y, w, h);
|
||||
const intersection: any = rect.getIntersection(parameters.visibleRect);
|
||||
if (!intersection) {
|
||||
// Clipped
|
||||
if (++warningsShown % 200 === 1) {
|
||||
logger.warn("Sprite drawn clipped but it's not on screen - perform culling before (", warningsShown, "warnings)");
|
||||
}
|
||||
if (G_IS_DEV && globalConfig.debug.testClipping) {
|
||||
parameters.context.fillStyle = "yellow";
|
||||
parameters.context.fillRect(x, y, w, h);
|
||||
}
|
||||
return;
|
||||
}
|
||||
parameters.context.drawImage(sprite,
|
||||
// src pos and size
|
||||
((intersection.x - x) / w) * originalW, ((intersection.y - y) / h) * originalH, (originalW * intersection.w) / w, (originalH * intersection.h) / h,
|
||||
// dest pos and size
|
||||
intersection.x, intersection.y, intersection.w, intersection.h);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
export class ExplainedResult {
|
||||
public result: boolean = result;
|
||||
public reason: string = reason;
|
||||
|
||||
constructor(result = true, reason = null, additionalProps = {}) {
|
||||
// Copy additional props
|
||||
for (const key: any in additionalProps) {
|
||||
this[key] = additionalProps[key];
|
||||
}
|
||||
}
|
||||
isGood(): any {
|
||||
return !!this.result;
|
||||
}
|
||||
isBad(): any {
|
||||
return !this.result;
|
||||
}
|
||||
static good(): any {
|
||||
return new ExplainedResult(true);
|
||||
}
|
||||
static bad(reason: any, additionalProps: any): any {
|
||||
return new ExplainedResult(false, reason, additionalProps);
|
||||
}
|
||||
static requireAll(...args: any): any {
|
||||
for (let i: any = 0; i < args.length; ++i) {
|
||||
const subResult: any = args[i].call();
|
||||
if (!subResult.isGood()) {
|
||||
return subResult;
|
||||
}
|
||||
}
|
||||
return this.good();
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import { createLogger } from "./logging";
|
||||
const logger: any = createLogger("factory");
|
||||
// simple factory pattern
|
||||
export class Factory {
|
||||
public id = id;
|
||||
public entries = [];
|
||||
public entryIds = [];
|
||||
public idToEntry = {};
|
||||
|
||||
constructor(id) {
|
||||
}
|
||||
getId(): any {
|
||||
return this.id;
|
||||
}
|
||||
register(entry: any): any {
|
||||
// Extract id
|
||||
const id: any = entry.getId();
|
||||
assert(id, "Factory: Invalid id for class: " + entry);
|
||||
// Check duplicates
|
||||
assert(!this.idToEntry[id], "Duplicate factory entry for " + id);
|
||||
// Insert
|
||||
this.entries.push(entry);
|
||||
this.entryIds.push(id);
|
||||
this.idToEntry[id] = entry;
|
||||
}
|
||||
/**
|
||||
* Checks if a given id is registered
|
||||
* {}
|
||||
*/
|
||||
hasId(id: string): boolean {
|
||||
return !!this.idToEntry[id];
|
||||
}
|
||||
/**
|
||||
* Finds an instance by a given id
|
||||
* {}
|
||||
*/
|
||||
findById(id: string): object {
|
||||
const entry: any = this.idToEntry[id];
|
||||
if (!entry) {
|
||||
logger.error("Object with id", id, "is not registered on factory", this.id, "!");
|
||||
assert(false, "Factory: Object with id '" + id + "' is not registered!");
|
||||
return null;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
/**
|
||||
* Returns all entries
|
||||
* {}
|
||||
*/
|
||||
getEntries(): Array<object> {
|
||||
return this.entries;
|
||||
}
|
||||
/**
|
||||
* Returns all registered ids
|
||||
* {}
|
||||
*/
|
||||
getAllIds(): Array<string> {
|
||||
return this.entryIds;
|
||||
}
|
||||
/**
|
||||
* Returns amount of stored entries
|
||||
* {}
|
||||
*/
|
||||
getNumEntries(): number {
|
||||
return this.entries.length;
|
||||
}
|
||||
}
|
@ -0,0 +1,280 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
import type { StateManager } from "./state_manager";
|
||||
/* typehints:end */
|
||||
import { globalConfig } from "./config";
|
||||
import { ClickDetector } from "./click_detector";
|
||||
import { logSection, createLogger } from "./logging";
|
||||
import { InputReceiver } from "./input_receiver";
|
||||
import { waitNextFrame } from "./utils";
|
||||
import { RequestChannel } from "./request_channel";
|
||||
import { MUSIC } from "../platform/sound";
|
||||
const logger: any = createLogger("game_state");
|
||||
/**
|
||||
* Basic state of the game state machine. This is the base of the whole game
|
||||
*/
|
||||
export class GameState {
|
||||
public key = key;
|
||||
public stateManager: StateManager = null;
|
||||
public app: Application = null;
|
||||
public fadingOut = false;
|
||||
public clickDetectors: Array<ClickDetector> = [];
|
||||
public inputReciever = new InputReceiver("state-" + key);
|
||||
public asyncChannel = new RequestChannel();
|
||||
/**
|
||||
* Constructs a new state with the given id
|
||||
*/
|
||||
|
||||
constructor(key) {
|
||||
this.inputReciever.backButton.add(this.onBackButton, this);
|
||||
}
|
||||
//// GETTERS / HELPER METHODS ////
|
||||
/**
|
||||
* Returns the states key
|
||||
* {}
|
||||
*/
|
||||
getKey(): string {
|
||||
return this.key;
|
||||
}
|
||||
/**
|
||||
* Returns the html element of the state
|
||||
* {}
|
||||
*/
|
||||
getDivElement(): HTMLElement {
|
||||
return document.getElementById("state_" + this.key);
|
||||
}
|
||||
/**
|
||||
* Transfers to a new state
|
||||
*/
|
||||
moveToState(stateKey: string, payload: any = {}, skipFadeOut: any = false): any {
|
||||
if (this.fadingOut) {
|
||||
logger.warn("Skipping move to '" + stateKey + "' since already fading out");
|
||||
return;
|
||||
}
|
||||
// Clean up event listeners
|
||||
this.internalCleanUpClickDetectors();
|
||||
// Fading
|
||||
const fadeTime: any = this.internalGetFadeInOutTime();
|
||||
const doFade: any = !skipFadeOut && this.getHasFadeOut() && fadeTime !== 0;
|
||||
logger.log("Moving to", stateKey, "(fading=", doFade, ")");
|
||||
if (doFade) {
|
||||
this.htmlElement.classList.remove("arrived");
|
||||
this.fadingOut = true;
|
||||
setTimeout((): any => {
|
||||
this.stateManager.moveToState(stateKey, payload);
|
||||
}, fadeTime);
|
||||
}
|
||||
else {
|
||||
this.stateManager.moveToState(stateKey, payload);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Tracks clicks on a given element and calls the given callback *on this state*.
|
||||
* If you want to call another function wrap it inside a lambda.
|
||||
*/
|
||||
trackClicks(element: Element, handler: function():void, args: import("./click_detector").ClickDetectorConstructorArgs= = {}): any {
|
||||
const detector: any = new ClickDetector(element, args);
|
||||
detector.click.add(handler, this);
|
||||
if (G_IS_DEV) {
|
||||
// Append a source so we can check where the click detector is from
|
||||
// @ts-ignore
|
||||
detector._src = "state-" + this.key;
|
||||
}
|
||||
this.clickDetectors.push(detector);
|
||||
}
|
||||
/**
|
||||
* Cancels all promises on the api as well as our async channel
|
||||
*/
|
||||
cancelAllAsyncOperations(): any {
|
||||
this.asyncChannel.cancelAll();
|
||||
}
|
||||
//// CALLBACKS ////
|
||||
/**
|
||||
* Callback when entering the state, to be overriddemn
|
||||
*/
|
||||
onEnter(payload: any): any { }
|
||||
/**
|
||||
* Callback when leaving the state
|
||||
*/
|
||||
onLeave(): any { }
|
||||
/**
|
||||
* Callback when the app got paused (on android, this means in background)
|
||||
*/
|
||||
onAppPause(): any { }
|
||||
/**
|
||||
* Callback when the app got resumed (on android, this means in foreground again)
|
||||
*/
|
||||
onAppResume(): any { }
|
||||
/**
|
||||
* Render callback
|
||||
*/
|
||||
onRender(dt: number): any { }
|
||||
/**
|
||||
* Background tick callback, called while the game is inactiev
|
||||
*/
|
||||
onBackgroundTick(dt: number): any { }
|
||||
/**
|
||||
* Called when the screen resized
|
||||
*/
|
||||
onResized(w: number, h: number): any { }
|
||||
/**
|
||||
* Internal backbutton handler, called when the hardware back button is pressed or
|
||||
* the escape key is pressed
|
||||
*/
|
||||
onBackButton(): any { }
|
||||
//// INTERFACE ////
|
||||
/**
|
||||
* Should return how many mulliseconds to fade in / out the state. Not recommended to override!
|
||||
* {} Time in milliseconds to fade out
|
||||
*/
|
||||
getInOutFadeTime(): number {
|
||||
if (globalConfig.debug.noArtificialDelays) {
|
||||
return 0;
|
||||
}
|
||||
return 200;
|
||||
}
|
||||
/**
|
||||
* Should return whether to fade in the game state. This will then apply the right css classes
|
||||
* for the fadein.
|
||||
* {}
|
||||
*/
|
||||
getHasFadeIn(): boolean {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Should return whether to fade out the game state. This will then apply the right css classes
|
||||
* for the fadeout and wait the delay before moving states
|
||||
* {}
|
||||
*/
|
||||
getHasFadeOut(): boolean {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Returns if this state should get paused if it does not have focus
|
||||
* {} true to pause the updating of the game
|
||||
*/
|
||||
getPauseOnFocusLost(): boolean {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Should return the html code of the state.
|
||||
* {}
|
||||
* @abstract
|
||||
*/
|
||||
getInnerHTML(): string {
|
||||
abstract;
|
||||
return "";
|
||||
}
|
||||
/**
|
||||
* Returns if the state has an unload confirmation, this is the
|
||||
* "Are you sure you want to leave the page" message.
|
||||
*/
|
||||
getHasUnloadConfirmation(): any {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Should return the theme music for this state
|
||||
* {}
|
||||
*/
|
||||
getThemeMusic(): string | null {
|
||||
return MUSIC.menu;
|
||||
}
|
||||
/**
|
||||
* Should return true if the player is currently ingame
|
||||
* {}
|
||||
*/
|
||||
getIsIngame(): boolean {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Should return whether to clear the whole body content before entering the state.
|
||||
* {}
|
||||
*/
|
||||
getRemovePreviousContent(): boolean {
|
||||
return true;
|
||||
}
|
||||
////////////////////
|
||||
//// INTERNAL ////
|
||||
/**
|
||||
* Internal callback from the manager. Do not override!
|
||||
*/
|
||||
internalRegisterCallback(stateManager: StateManager, app: any): any {
|
||||
assert(stateManager, "No state manager");
|
||||
assert(app, "No app");
|
||||
this.stateManager = stateManager;
|
||||
this.app = app;
|
||||
}
|
||||
/**
|
||||
* Internal callback when entering the state. Do not override!
|
||||
*/
|
||||
internalEnterCallback(payload: any, callCallback: boolean = true): any {
|
||||
logSection(this.key, "#26a69a");
|
||||
this.app.inputMgr.pushReciever(this.inputReciever);
|
||||
this.htmlElement = this.getDivElement();
|
||||
this.htmlElement.classList.add("active");
|
||||
// Apply classes in the next frame so the css transition keeps up
|
||||
waitNextFrame().then((): any => {
|
||||
if (this.htmlElement) {
|
||||
this.htmlElement.classList.remove("fadingOut");
|
||||
this.htmlElement.classList.remove("fadingIn");
|
||||
}
|
||||
});
|
||||
// Call handler
|
||||
if (callCallback) {
|
||||
this.onEnter(payload);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Internal callback when the state is left. Do not override!
|
||||
*/
|
||||
internalLeaveCallback(): any {
|
||||
this.onLeave();
|
||||
this.htmlElement.classList.remove("active");
|
||||
this.app.inputMgr.popReciever(this.inputReciever);
|
||||
this.internalCleanUpClickDetectors();
|
||||
this.asyncChannel.cancelAll();
|
||||
}
|
||||
/**
|
||||
* Internal app pause callback
|
||||
*/
|
||||
internalOnAppPauseCallback(): any {
|
||||
this.onAppPause();
|
||||
}
|
||||
/**
|
||||
* Internal app resume callback
|
||||
*/
|
||||
internalOnAppResumeCallback(): any {
|
||||
this.onAppResume();
|
||||
}
|
||||
/**
|
||||
* Cleans up all click detectors
|
||||
*/
|
||||
internalCleanUpClickDetectors(): any {
|
||||
if (this.clickDetectors) {
|
||||
for (let i: any = 0; i < this.clickDetectors.length; ++i) {
|
||||
this.clickDetectors[i].cleanup();
|
||||
}
|
||||
this.clickDetectors = [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Internal method to get the HTML of the game state.
|
||||
* {}
|
||||
*/
|
||||
internalGetFullHtml(): string {
|
||||
return this.getInnerHTML();
|
||||
}
|
||||
/**
|
||||
* Internal method to compute the time to fade in / out
|
||||
* {} time to fade in / out in ms
|
||||
*/
|
||||
internalGetFadeInOutTime(): number {
|
||||
if (G_IS_DEV && globalConfig.debug.fastGameEnter) {
|
||||
return 1;
|
||||
}
|
||||
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
|
||||
return 1;
|
||||
}
|
||||
return this.getInOutFadeTime();
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { SingletonFactory } from "./singleton_factory";
|
||||
import { Factory } from "./factory";
|
||||
export type BaseGameSpeed = import("../game/time/base_game_speed").BaseGameSpeed;
|
||||
export type Component = import("../game/component").Component;
|
||||
export type BaseItem = import("../game/base_item").BaseItem;
|
||||
export type GameMode = import("../game/game_mode").GameMode;
|
||||
export type MetaBuilding = import("../game/meta_building").MetaBuilding;
|
||||
|
||||
export let gMetaBuildingRegistry: SingletonFactoryTemplate<MetaBuilding> = new SingletonFactory();
|
||||
export let gBuildingsByCategory: {
|
||||
[idx: string]: Array<Class<MetaBuilding>>;
|
||||
} = null;
|
||||
export let gComponentRegistry: FactoryTemplate<Component> = new Factory("component");
|
||||
export let gGameModeRegistry: FactoryTemplate<GameMode> = new Factory("gameMode");
|
||||
export let gGameSpeedRegistry: FactoryTemplate<BaseGameSpeed> = new Factory("gamespeed");
|
||||
export let gItemRegistry: FactoryTemplate<BaseItem> = new Factory("item");
|
||||
// Helpers
|
||||
export function initBuildingsByCategory(buildings: {
|
||||
[idx: string]: Array<Class<MetaBuilding>>;
|
||||
}): any {
|
||||
gBuildingsByCategory = buildings;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end */
|
||||
/**
|
||||
* Used for the bug reporter, and the click detector which both have no handles to this.
|
||||
* It would be nicer to have no globals, but this is the only one. I promise!
|
||||
*/
|
||||
export let GLOBAL_APP: Application = null;
|
||||
export function setGlobalApp(app: Application): any {
|
||||
assert(!GLOBAL_APP, "Create application twice!");
|
||||
GLOBAL_APP = app;
|
||||
}
|
||||
export const BUILD_OPTIONS: any = {
|
||||
HAVE_ASSERT: G_HAVE_ASSERT,
|
||||
APP_ENVIRONMENT: G_APP_ENVIRONMENT,
|
||||
IS_DEV: G_IS_DEV,
|
||||
IS_RELEASE: G_IS_RELEASE,
|
||||
IS_BROWSER: G_IS_BROWSER,
|
||||
IS_STANDALONE: G_IS_STANDALONE,
|
||||
BUILD_TIME: G_BUILD_TIME,
|
||||
BUILD_COMMIT_HASH: G_BUILD_COMMIT_HASH,
|
||||
BUILD_VERSION: G_BUILD_VERSION,
|
||||
ALL_UI_IMAGES: G_ALL_UI_IMAGES,
|
||||
};
|
@ -0,0 +1,161 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
import type { InputReceiver } from "./input_receiver";
|
||||
/* typehints:end */
|
||||
import { Signal, STOP_PROPAGATION } from "./signal";
|
||||
import { createLogger } from "./logging";
|
||||
import { arrayDeleteValue, fastArrayDeleteValue } from "./utils";
|
||||
const logger: any = createLogger("input_distributor");
|
||||
export class InputDistributor {
|
||||
public app = app;
|
||||
public recieverStack: Array<InputReceiver> = [];
|
||||
public filters: Array<function(: boolean):boolean> = [];
|
||||
public keysDown = new Set();
|
||||
|
||||
constructor(app) {
|
||||
this.bindToEvents();
|
||||
}
|
||||
/**
|
||||
* Attaches a new filter which can filter and reject events
|
||||
*/
|
||||
installFilter(filter: function(: boolean):boolean): any {
|
||||
this.filters.push(filter);
|
||||
}
|
||||
/**
|
||||
* Removes an attached filter
|
||||
*/
|
||||
dismountFilter(filter: function(: boolean):boolean): any {
|
||||
fastArrayDeleteValue(this.filters, filter);
|
||||
}
|
||||
pushReciever(reciever: InputReceiver): any {
|
||||
if (this.isRecieverAttached(reciever)) {
|
||||
assert(false, "Can not add reciever " + reciever.context + " twice");
|
||||
logger.error("Can not add reciever", reciever.context, "twice");
|
||||
return;
|
||||
}
|
||||
this.recieverStack.push(reciever);
|
||||
if (this.recieverStack.length > 10) {
|
||||
logger.error("Reciever stack is huge, probably some dead receivers arround:", this.recieverStack.map((x: any): any => x.context));
|
||||
}
|
||||
}
|
||||
popReciever(reciever: InputReceiver): any {
|
||||
if (this.recieverStack.indexOf(reciever) < 0) {
|
||||
assert(false, "Can not pop reciever " + reciever.context + " since its not contained");
|
||||
logger.error("Can not pop reciever", reciever.context, "since its not contained");
|
||||
return;
|
||||
}
|
||||
if (this.recieverStack[this.recieverStack.length - 1] !== reciever) {
|
||||
logger.warn("Popping reciever", reciever.context, "which is not on top of the stack. Stack is: ", this.recieverStack.map((x: any): any => x.context));
|
||||
}
|
||||
arrayDeleteValue(this.recieverStack, reciever);
|
||||
}
|
||||
isRecieverAttached(reciever: InputReceiver): any {
|
||||
return this.recieverStack.indexOf(reciever) >= 0;
|
||||
}
|
||||
isRecieverOnTop(reciever: InputReceiver): any {
|
||||
return (this.isRecieverAttached(reciever) &&
|
||||
this.recieverStack[this.recieverStack.length - 1] === reciever);
|
||||
}
|
||||
makeSureAttachedAndOnTop(reciever: InputReceiver): any {
|
||||
this.makeSureDetached(reciever);
|
||||
this.pushReciever(reciever);
|
||||
}
|
||||
makeSureDetached(reciever: InputReceiver): any {
|
||||
if (this.isRecieverAttached(reciever)) {
|
||||
arrayDeleteValue(this.recieverStack, reciever);
|
||||
}
|
||||
}
|
||||
destroyReceiver(reciever: InputReceiver): any {
|
||||
this.makeSureDetached(reciever);
|
||||
reciever.cleanup();
|
||||
}
|
||||
// Internal
|
||||
getTopReciever(): any {
|
||||
if (this.recieverStack.length > 0) {
|
||||
return this.recieverStack[this.recieverStack.length - 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
bindToEvents(): any {
|
||||
window.addEventListener("popstate", this.handleBackButton.bind(this), false);
|
||||
document.addEventListener("backbutton", this.handleBackButton.bind(this), false);
|
||||
window.addEventListener("keydown", this.handleKeyMouseDown.bind(this));
|
||||
window.addEventListener("keyup", this.handleKeyMouseUp.bind(this));
|
||||
window.addEventListener("mousedown", this.handleKeyMouseDown.bind(this));
|
||||
window.addEventListener("mouseup", this.handleKeyMouseUp.bind(this));
|
||||
window.addEventListener("blur", this.handleBlur.bind(this));
|
||||
document.addEventListener("paste", this.handlePaste.bind(this));
|
||||
}
|
||||
forwardToReceiver(eventId: any, payload: any = null): any {
|
||||
// Check filters
|
||||
for (let i: any = 0; i < this.filters.length; ++i) {
|
||||
if (!this.filters[i](eventId)) {
|
||||
return STOP_PROPAGATION;
|
||||
}
|
||||
}
|
||||
const reciever: any = this.getTopReciever();
|
||||
if (!reciever) {
|
||||
logger.warn("Dismissing event because not reciever was found:", eventId);
|
||||
return;
|
||||
}
|
||||
const signal: any = reciever[eventId];
|
||||
assert(signal instanceof Signal, "Not a valid event id");
|
||||
return signal.dispatch(payload);
|
||||
}
|
||||
handleBackButton(event: Event): any {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.forwardToReceiver("backButton");
|
||||
}
|
||||
/**
|
||||
* Handles when the page got blurred
|
||||
*/
|
||||
handleBlur(): any {
|
||||
this.forwardToReceiver("pageBlur", {});
|
||||
this.keysDown.clear();
|
||||
}
|
||||
|
||||
handlePaste(ev: any): any {
|
||||
this.forwardToReceiver("paste", ev);
|
||||
}
|
||||
handleKeyMouseDown(event: KeyboardEvent | MouseEvent): any {
|
||||
const keyCode: any = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
|
||||
if (keyCode === 4 || // MB4
|
||||
keyCode === 5 || // MB5
|
||||
keyCode === 9 || // TAB
|
||||
keyCode === 16 || // SHIFT
|
||||
keyCode === 17 || // CTRL
|
||||
keyCode === 18 || // ALT
|
||||
(keyCode >= 112 && keyCode < 122) // F1 - F10
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
const isInitial: any = !this.keysDown.has(keyCode);
|
||||
this.keysDown.add(keyCode);
|
||||
if (this.forwardToReceiver("keydown", {
|
||||
keyCode: keyCode,
|
||||
shift: event.shiftKey,
|
||||
alt: event.altKey,
|
||||
ctrl: event.ctrlKey,
|
||||
initial: isInitial,
|
||||
event,
|
||||
}) === STOP_PROPAGATION) {
|
||||
return;
|
||||
}
|
||||
if (keyCode === 27) {
|
||||
// Escape key
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return this.forwardToReceiver("backButton");
|
||||
}
|
||||
}
|
||||
handleKeyMouseUp(event: KeyboardEvent | MouseEvent): any {
|
||||
const keyCode: any = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
|
||||
this.keysDown.delete(keyCode);
|
||||
this.forwardToReceiver("keyup", {
|
||||
keyCode: keyCode,
|
||||
shift: event.shiftKey,
|
||||
alt: event.altKey,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { Signal } from "./signal";
|
||||
export class InputReceiver {
|
||||
public context = context;
|
||||
public backButton = new Signal();
|
||||
public keydown = new Signal();
|
||||
public keyup = new Signal();
|
||||
public pageBlur = new Signal();
|
||||
public destroyed = new Signal();
|
||||
public paste = new Signal();
|
||||
|
||||
constructor(context = "unknown") {
|
||||
}
|
||||
cleanup(): any {
|
||||
this.backButton.removeAll();
|
||||
this.keydown.removeAll();
|
||||
this.keyup.removeAll();
|
||||
this.paste.removeAll();
|
||||
this.destroyed.dispatch();
|
||||
}
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
import { makeOffscreenBuffer } from "./buffer_utils";
|
||||
import { AtlasSprite, BaseSprite, RegularSprite, SpriteAtlasLink } from "./sprites";
|
||||
import { cachebust } from "./cachebust";
|
||||
import { createLogger } from "./logging";
|
||||
export type Application = import("../application").Application;
|
||||
export type AtlasDefinition = import("./atlas_definitions").AtlasDefinition;
|
||||
|
||||
const logger: any = createLogger("loader");
|
||||
const missingSpriteIds: any = {};
|
||||
class LoaderImpl {
|
||||
public app = null;
|
||||
public sprites: Map<string, BaseSprite> = new Map();
|
||||
public rawImages = [];
|
||||
|
||||
constructor() {
|
||||
}
|
||||
linkAppAfterBoot(app: Application): any {
|
||||
this.app = app;
|
||||
this.makeSpriteNotFoundCanvas();
|
||||
}
|
||||
/**
|
||||
* Fetches a given sprite from the cache
|
||||
* {}
|
||||
*/
|
||||
getSpriteInternal(key: string): BaseSprite {
|
||||
const sprite: any = this.sprites.get(key);
|
||||
if (!sprite) {
|
||||
if (!missingSpriteIds[key]) {
|
||||
// Only show error once
|
||||
missingSpriteIds[key] = true;
|
||||
logger.error("Sprite '" + key + "' not found!");
|
||||
}
|
||||
return this.spriteNotFoundSprite;
|
||||
}
|
||||
return sprite;
|
||||
}
|
||||
/**
|
||||
* Returns an atlas sprite from the cache
|
||||
* {}
|
||||
*/
|
||||
getSprite(key: string): AtlasSprite {
|
||||
const sprite: any = this.getSpriteInternal(key);
|
||||
assert(sprite instanceof AtlasSprite || sprite === this.spriteNotFoundSprite, "Not an atlas sprite");
|
||||
return sprite as AtlasSprite);
|
||||
}
|
||||
/**
|
||||
* Returns a regular sprite from the cache
|
||||
* {}
|
||||
*/
|
||||
getRegularSprite(key: string): RegularSprite {
|
||||
const sprite: any = this.getSpriteInternal(key);
|
||||
assert(sprite instanceof RegularSprite || sprite === this.spriteNotFoundSprite, "Not a regular sprite");
|
||||
return sprite as RegularSprite);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* {}
|
||||
*/
|
||||
internalPreloadImage(key: string, progressHandler: (progress: number) => void): Promise<HTMLImageElement | null> {
|
||||
return this.app.backgroundResourceLoader
|
||||
.preloadWithProgress("res/" + key, (progress: any): any => {
|
||||
progressHandler(progress);
|
||||
})
|
||||
.then((url: any): any => {
|
||||
return new Promise((resolve: any, reject: any): any => {
|
||||
const image: any = new Image();
|
||||
image.addEventListener("load", (): any => resolve(image));
|
||||
image.addEventListener("error", (err: any): any => reject("Failed to load sprite " + key + ": " + err));
|
||||
image.src = url;
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Preloads a sprite
|
||||
* {}
|
||||
*/
|
||||
preloadCSSSprite(key: string, progressHandler: (progress: number) => void): Promise<void> {
|
||||
return this.internalPreloadImage(key, progressHandler).then((image: any): any => {
|
||||
if (key.indexOf("game_misc") >= 0) {
|
||||
// Allow access to regular sprites
|
||||
this.sprites.set(key, new RegularSprite(image, image.width, image.height));
|
||||
}
|
||||
this.rawImages.push(image);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Preloads an atlas
|
||||
* {}
|
||||
*/
|
||||
preloadAtlas(atlas: AtlasDefinition, progressHandler: (progress: number) => void): Promise<void> {
|
||||
return this.internalPreloadImage(atlas.getFullSourcePath(), progressHandler).then((image: any): any => {
|
||||
// @ts-ignore
|
||||
image.label = atlas.sourceFileName;
|
||||
return this.internalParseAtlas(atlas, image);
|
||||
});
|
||||
}
|
||||
internalParseAtlas({ meta: { scale }, sourceData }: AtlasDefinition, loadedImage: HTMLImageElement): any {
|
||||
this.rawImages.push(loadedImage);
|
||||
for (const spriteName: any in sourceData) {
|
||||
const { frame, sourceSize, spriteSourceSize }: any = sourceData[spriteName];
|
||||
let sprite: any = (this.sprites.get(spriteName) as AtlasSprite);
|
||||
if (!sprite) {
|
||||
sprite = new AtlasSprite(spriteName);
|
||||
this.sprites.set(spriteName, sprite);
|
||||
}
|
||||
if (sprite.frozen) {
|
||||
continue;
|
||||
}
|
||||
const link: any = new SpriteAtlasLink({
|
||||
packedX: frame.x,
|
||||
packedY: frame.y,
|
||||
packedW: frame.w,
|
||||
packedH: frame.h,
|
||||
packOffsetX: spriteSourceSize.x,
|
||||
packOffsetY: spriteSourceSize.y,
|
||||
atlas: loadedImage,
|
||||
w: sourceSize.w,
|
||||
h: sourceSize.h,
|
||||
});
|
||||
sprite.linksByResolution[scale] = link;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Makes the canvas which shows the question mark, shown when a sprite was not found
|
||||
*/
|
||||
makeSpriteNotFoundCanvas(): any {
|
||||
const dims: any = 128;
|
||||
const [canvas, context]: any = makeOffscreenBuffer(dims, dims, {
|
||||
smooth: false,
|
||||
label: "not-found-sprite",
|
||||
});
|
||||
context.fillStyle = "#f77";
|
||||
context.fillRect(0, 0, dims, dims);
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
context.fillStyle = "#eee";
|
||||
context.font = "25px Arial";
|
||||
context.fillText("???", dims / 2, dims / 2);
|
||||
// TODO: Not sure why this is set here
|
||||
// @ts-ignore
|
||||
canvas.src = "not-found";
|
||||
const sprite: any = new AtlasSprite("not-found");
|
||||
["0.1", "0.25", "0.5", "0.75", "1"].forEach((resolution: any): any => {
|
||||
sprite.linksByResolution[resolution] = new SpriteAtlasLink({
|
||||
packedX: 0,
|
||||
packedY: 0,
|
||||
w: dims,
|
||||
h: dims,
|
||||
packOffsetX: 0,
|
||||
packOffsetY: 0,
|
||||
packedW: dims,
|
||||
packedH: dims,
|
||||
atlas: canvas,
|
||||
});
|
||||
});
|
||||
this.spriteNotFoundSprite = sprite;
|
||||
}
|
||||
}
|
||||
export const Loader: any = new LoaderImpl();
|
@ -0,0 +1,212 @@
|
||||
import { globalConfig } from "./config";
|
||||
const circularJson: any = require("circular-json");
|
||||
/*
|
||||
Logging functions
|
||||
- To be extended
|
||||
*/
|
||||
/**
|
||||
* Base logger class
|
||||
*/
|
||||
class Logger {
|
||||
public context = context;
|
||||
|
||||
constructor(context) {
|
||||
}
|
||||
debug(...args: any): any {
|
||||
globalDebug(this.context, ...args);
|
||||
}
|
||||
log(...args: any): any {
|
||||
globalLog(this.context, ...args);
|
||||
}
|
||||
warn(...args: any): any {
|
||||
globalWarn(this.context, ...args);
|
||||
}
|
||||
error(...args: any): any {
|
||||
globalError(this.context, ...args);
|
||||
}
|
||||
}
|
||||
export function createLogger(context: any): any {
|
||||
return new Logger(context);
|
||||
}
|
||||
function prepareObjectForLogging(obj: any, maxDepth: any = 1): any {
|
||||
if (!window.Sentry) {
|
||||
// Not required without sentry
|
||||
return obj;
|
||||
}
|
||||
if (typeof obj !== "object" && !Array.isArray(obj)) {
|
||||
return obj;
|
||||
}
|
||||
const result: any = {};
|
||||
for (const key: any in obj) {
|
||||
const val: any = obj[key];
|
||||
if (typeof val === "object") {
|
||||
if (maxDepth > 0) {
|
||||
result[key] = prepareObjectForLogging(val, maxDepth - 1);
|
||||
}
|
||||
else {
|
||||
result[key] = "[object]";
|
||||
}
|
||||
}
|
||||
else {
|
||||
result[key] = val;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Serializes an error
|
||||
*/
|
||||
export function serializeError(err: Error | ErrorEvent): any {
|
||||
if (!err) {
|
||||
return null;
|
||||
}
|
||||
const result: any = {
|
||||
|
||||
type: err.constructor.name,
|
||||
};
|
||||
if (err instanceof Error) {
|
||||
result.message = err.message;
|
||||
result.name = err.name;
|
||||
result.stack = err.stack;
|
||||
result.type = "{type.Error}";
|
||||
}
|
||||
else if (err instanceof ErrorEvent) {
|
||||
result.filename = err.filename;
|
||||
result.message = err.message;
|
||||
result.lineno = err.lineno;
|
||||
result.colno = err.colno;
|
||||
result.type = "{type.ErrorEvent}";
|
||||
if (err.error) {
|
||||
result.error = serializeError(err.error);
|
||||
}
|
||||
else {
|
||||
result.error = "{not-provided}";
|
||||
}
|
||||
}
|
||||
else {
|
||||
result.type = "{unkown-type:" + typeof err + "}";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Serializes an event
|
||||
*/
|
||||
function serializeEvent(event: Event): any {
|
||||
let result: any = {
|
||||
type: "{type.Event:" + typeof event + "}",
|
||||
};
|
||||
result.eventType = event.type;
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Prepares a json payload
|
||||
*/
|
||||
function preparePayload(key: string, value: any): any {
|
||||
if (value instanceof Error || value instanceof ErrorEvent) {
|
||||
return serializeError(value);
|
||||
}
|
||||
if (value instanceof Event) {
|
||||
return serializeEvent(value);
|
||||
}
|
||||
if (typeof value === "undefined") {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
/**
|
||||
* Stringifies an object containing circular references and errors
|
||||
*/
|
||||
export function stringifyObjectContainingErrors(payload: any): any {
|
||||
return circularJson.stringify(payload, preparePayload);
|
||||
}
|
||||
export function globalDebug(context: any, ...args: any): any {
|
||||
if (G_IS_DEV) {
|
||||
logInternal(context, console.log, prepareArgsForLogging(args));
|
||||
}
|
||||
}
|
||||
export function globalLog(context: any, ...args: any): any {
|
||||
// eslint-disable-next-line no-console
|
||||
logInternal(context, console.log, prepareArgsForLogging(args));
|
||||
}
|
||||
export function globalWarn(context: any, ...args: any): any {
|
||||
// eslint-disable-next-line no-console
|
||||
logInternal(context, console.warn, prepareArgsForLogging(args));
|
||||
}
|
||||
export function globalError(context: any, ...args: any): any {
|
||||
args = prepareArgsForLogging(args);
|
||||
// eslint-disable-next-line no-console
|
||||
logInternal(context, console.error, args);
|
||||
if (window.Sentry) {
|
||||
window.Sentry.withScope((scope: any): any => {
|
||||
scope.setExtra("args", args);
|
||||
window.Sentry.captureMessage(internalBuildStringFromArgs(args), "error");
|
||||
});
|
||||
}
|
||||
}
|
||||
function prepareArgsForLogging(args: any): any {
|
||||
let result: any = [];
|
||||
for (let i: any = 0; i < args.length; ++i) {
|
||||
result.push(prepareObjectForLogging(args[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function internalBuildStringFromArgs(args: Array<any>): any {
|
||||
let result: any = [];
|
||||
for (let i: any = 0; i < args.length; ++i) {
|
||||
let arg: any = args[i];
|
||||
if (typeof arg === "string" ||
|
||||
typeof arg === "number" ||
|
||||
typeof arg === "boolean" ||
|
||||
arg === null ||
|
||||
arg === undefined) {
|
||||
result.push("" + arg);
|
||||
}
|
||||
else if (arg instanceof Error) {
|
||||
result.push(arg.message);
|
||||
}
|
||||
else {
|
||||
result.push("[object]");
|
||||
}
|
||||
}
|
||||
return result.join(" ");
|
||||
}
|
||||
export function logSection(name: any, color: any): any {
|
||||
while (name.length <= 14) {
|
||||
name = " " + name + " ";
|
||||
}
|
||||
name = name.padEnd(19, " ");
|
||||
const lineCss: any = "letter-spacing: -3px; color: " + color + "; font-size: 6px; background: #eee; color: #eee;";
|
||||
const line: any = "%c----------------------------";
|
||||
console.log("\n" + line + " %c" + name + " " + line + "\n", lineCss, "color: " + color, lineCss);
|
||||
}
|
||||
function extractHandleContext(handle: any): any {
|
||||
let context: any = handle || "unknown";
|
||||
|
||||
|
||||
if (handle && handle.constructor && handle.constructor.name) {
|
||||
|
||||
context = handle.constructor.name;
|
||||
if (context === "String") {
|
||||
context = handle;
|
||||
}
|
||||
}
|
||||
if (handle && handle.name) {
|
||||
context = handle.name;
|
||||
}
|
||||
return context + "";
|
||||
}
|
||||
function logInternal(handle: any, consoleMethod: any, args: any): any {
|
||||
const context: any = extractHandleContext(handle).padEnd(20, " ");
|
||||
const labelColor: any = handle && handle.LOG_LABEL_COLOR ? handle.LOG_LABEL_COLOR : "#aaa";
|
||||
if (G_IS_DEV && globalConfig.debug.logTimestamps) {
|
||||
const timestamp: any = "⏱ %c" + (Math.floor(performance.now()) + "").padEnd(6, " ") + "";
|
||||
consoleMethod.call(console, timestamp + " %c" + context, "color: #7f7;", "color: " + labelColor + ";", ...args);
|
||||
}
|
||||
else {
|
||||
// if (G_IS_DEV && !globalConfig.debug.disableLoggingLogSources) {
|
||||
consoleMethod.call(console, "%c" + context, "color: " + labelColor, ...args);
|
||||
// } else {
|
||||
// consoleMethod.call(console, ...args);
|
||||
// }
|
||||
}
|
||||
}
|
@ -0,0 +1,453 @@
|
||||
// Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
|
||||
// This work is free. You can redistribute it and/or modify it
|
||||
// under the terms of the WTFPL, Version 2
|
||||
// For more information see LICENSE.txt or http://www.wtfpl.net/
|
||||
//
|
||||
// For more information, the home page:
|
||||
// http://pieroxy.net/blog/pages/lz-string/testing.html
|
||||
//
|
||||
// LZ-based compression algorithm, version 1.4.4
|
||||
const fromCharCode: any = String.fromCharCode;
|
||||
const hasOwnProperty: any = Object.prototype.hasOwnProperty;
|
||||
const keyStrUriSafe: any = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
|
||||
const baseReverseDic: any = {};
|
||||
function getBaseValue(alphabet: any, character: any): any {
|
||||
if (!baseReverseDic[alphabet]) {
|
||||
baseReverseDic[alphabet] = {};
|
||||
for (let i: any = 0; i < alphabet.length; i++) {
|
||||
baseReverseDic[alphabet][alphabet.charAt(i)] = i;
|
||||
}
|
||||
}
|
||||
return baseReverseDic[alphabet][character];
|
||||
}
|
||||
//compress into uint8array (UCS-2 big endian format)
|
||||
export function compressU8(uncompressed: any): any {
|
||||
let compressed: any = compress(uncompressed);
|
||||
let buf: any = new Uint8Array(compressed.length * 2); // 2 bytes per character
|
||||
for (let i: any = 0, TotalLen: any = compressed.length; i < TotalLen; i++) {
|
||||
let current_value: any = compressed.charCodeAt(i);
|
||||
buf[i * 2] = current_value >>> 8;
|
||||
buf[i * 2 + 1] = current_value % 256;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
// Compreses with header
|
||||
export function compressU8WHeader(uncompressed: string, header: number): any {
|
||||
let compressed: any = compress(uncompressed);
|
||||
let buf: any = new Uint8Array(2 + compressed.length * 2); // 2 bytes per character
|
||||
buf[0] = header >>> 8;
|
||||
buf[1] = header % 256;
|
||||
for (let i: any = 0, TotalLen: any = compressed.length; i < TotalLen; i++) {
|
||||
let current_value: any = compressed.charCodeAt(i);
|
||||
buf[2 + i * 2] = current_value >>> 8;
|
||||
buf[2 + i * 2 + 1] = current_value % 256;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
//decompress from uint8array (UCS-2 big endian format)
|
||||
export function decompressU8WHeader(compressed: Uint8Array): any {
|
||||
// let buf = new Array(compressed.length / 2); // 2 bytes per character
|
||||
// for (let i = 0, TotalLen = buf.length; i < TotalLen; i++) {
|
||||
// buf[i] = compressed[i * 2] * 256 + compressed[i * 2 + 1];
|
||||
// }
|
||||
// let result = [];
|
||||
// buf.forEach(function (c) {
|
||||
// result.push(fromCharCode(c));
|
||||
// });
|
||||
let result: any = [];
|
||||
for (let i: any = 2, n: any = compressed.length; i < n; i += 2) {
|
||||
const code: any = compressed[i] * 256 + compressed[i + 1];
|
||||
result.push(fromCharCode(code));
|
||||
}
|
||||
return decompress(result.join(""));
|
||||
}
|
||||
//compress into a string that is already URI encoded
|
||||
export function compressX64(input: any): any {
|
||||
if (input == null)
|
||||
return "";
|
||||
return _compress(input, 6, function (a: any): any {
|
||||
return keyStrUriSafe.charAt(a);
|
||||
});
|
||||
}
|
||||
//decompress from an output of compressToEncodedURIComponent
|
||||
export function decompressX64(input: any): any {
|
||||
if (input == null)
|
||||
return "";
|
||||
if (input == "")
|
||||
return null;
|
||||
input = input.replace(/ /g, "+");
|
||||
return _decompress(input.length, 32, function (index: any): any {
|
||||
return getBaseValue(keyStrUriSafe, input.charAt(index));
|
||||
});
|
||||
}
|
||||
function compress(uncompressed: any): any {
|
||||
return _compress(uncompressed, 16, function (a: any): any {
|
||||
return fromCharCode(a);
|
||||
});
|
||||
}
|
||||
function _compress(uncompressed: any, bitsPerChar: any, getCharFromInt: any): any {
|
||||
if (uncompressed == null)
|
||||
return "";
|
||||
let i: any, value: any, context_dictionary: any = {}, context_dictionaryToCreate: any = {}, context_c: any = "", context_wc: any = "", context_w: any = "", context_enlargeIn: any = 2, // Compensate for the first entry which should not count
|
||||
context_dictSize: any = 3, context_numBits: any = 2, context_data: any = [], context_data_val: any = 0, context_data_position: any = 0, ii: any;
|
||||
for (ii = 0; ii < uncompressed.length; ii += 1) {
|
||||
context_c = uncompressed.charAt(ii);
|
||||
if (!hasOwnProperty.call(context_dictionary, context_c)) {
|
||||
context_dictionary[context_c] = context_dictSize++;
|
||||
context_dictionaryToCreate[context_c] = true;
|
||||
}
|
||||
context_wc = context_w + context_c;
|
||||
if (hasOwnProperty.call(context_dictionary, context_wc)) {
|
||||
context_w = context_wc;
|
||||
}
|
||||
else {
|
||||
if (hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
|
||||
if (context_w.charCodeAt(0) < 256) {
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
context_data_val = context_data_val << 1;
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
context_data_position++;
|
||||
}
|
||||
}
|
||||
value = context_w.charCodeAt(0);
|
||||
for (i = 0; i < 8; i++) {
|
||||
context_data_val = (context_data_val << 1) | (value & 1);
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
value = 1;
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
context_data_val = (context_data_val << 1) | value;
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = 0;
|
||||
}
|
||||
value = context_w.charCodeAt(0);
|
||||
for (i = 0; i < 16; i++) {
|
||||
context_data_val = (context_data_val << 1) | (value & 1);
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
}
|
||||
}
|
||||
context_enlargeIn--;
|
||||
if (context_enlargeIn == 0) {
|
||||
context_enlargeIn = Math.pow(2, context_numBits);
|
||||
context_numBits++;
|
||||
}
|
||||
delete context_dictionaryToCreate[context_w];
|
||||
}
|
||||
else {
|
||||
value = context_dictionary[context_w];
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
context_data_val = (context_data_val << 1) | (value & 1);
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
}
|
||||
}
|
||||
context_enlargeIn--;
|
||||
if (context_enlargeIn == 0) {
|
||||
context_enlargeIn = Math.pow(2, context_numBits);
|
||||
context_numBits++;
|
||||
}
|
||||
// Add wc to the dictionary.
|
||||
context_dictionary[context_wc] = context_dictSize++;
|
||||
context_w = String(context_c);
|
||||
}
|
||||
}
|
||||
// Output the code for w.
|
||||
if (context_w !== "") {
|
||||
if (hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
|
||||
if (context_w.charCodeAt(0) < 256) {
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
context_data_val = context_data_val << 1;
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
context_data_position++;
|
||||
}
|
||||
}
|
||||
value = context_w.charCodeAt(0);
|
||||
for (i = 0; i < 8; i++) {
|
||||
context_data_val = (context_data_val << 1) | (value & 1);
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
value = 1;
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
context_data_val = (context_data_val << 1) | value;
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = 0;
|
||||
}
|
||||
value = context_w.charCodeAt(0);
|
||||
for (i = 0; i < 16; i++) {
|
||||
context_data_val = (context_data_val << 1) | (value & 1);
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
}
|
||||
}
|
||||
context_enlargeIn--;
|
||||
if (context_enlargeIn == 0) {
|
||||
context_enlargeIn = Math.pow(2, context_numBits);
|
||||
context_numBits++;
|
||||
}
|
||||
delete context_dictionaryToCreate[context_w];
|
||||
}
|
||||
else {
|
||||
value = context_dictionary[context_w];
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
context_data_val = (context_data_val << 1) | (value & 1);
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
}
|
||||
}
|
||||
context_enlargeIn--;
|
||||
if (context_enlargeIn == 0) {
|
||||
context_enlargeIn = Math.pow(2, context_numBits);
|
||||
context_numBits++;
|
||||
}
|
||||
}
|
||||
// Mark the end of the stream
|
||||
value = 2;
|
||||
for (i = 0; i < context_numBits; i++) {
|
||||
context_data_val = (context_data_val << 1) | (value & 1);
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data_position = 0;
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
context_data_val = 0;
|
||||
}
|
||||
else {
|
||||
context_data_position++;
|
||||
}
|
||||
value = value >> 1;
|
||||
}
|
||||
// Flush the last char
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
context_data_val = context_data_val << 1;
|
||||
if (context_data_position == bitsPerChar - 1) {
|
||||
context_data.push(getCharFromInt(context_data_val));
|
||||
break;
|
||||
}
|
||||
else
|
||||
context_data_position++;
|
||||
}
|
||||
return context_data.join("");
|
||||
}
|
||||
function decompress(compressed: any): any {
|
||||
if (compressed == null)
|
||||
return "";
|
||||
if (compressed == "")
|
||||
return null;
|
||||
return _decompress(compressed.length, 32768, function (index: any): any {
|
||||
return compressed.charCodeAt(index);
|
||||
});
|
||||
}
|
||||
function _decompress(length: any, resetValue: any, getNextValue: any): any {
|
||||
let dictionary: any = [], next: any, enlargeIn: any = 4, dictSize: any = 4, numBits: any = 3, entry: any = "", result: any = [], i: any, w: any, bits: any, resb: any, maxpower: any, power: any, c: any, data: any = { val: getNextValue(0), position: resetValue, index: 1 };
|
||||
for (i = 0; i < 3; i += 1) {
|
||||
dictionary[i] = i;
|
||||
}
|
||||
bits = 0;
|
||||
maxpower = Math.pow(2, 2);
|
||||
power = 1;
|
||||
while (power != maxpower) {
|
||||
resb = data.val & data.position;
|
||||
data.position >>= 1;
|
||||
if (data.position == 0) {
|
||||
data.position = resetValue;
|
||||
data.val = getNextValue(data.index++);
|
||||
}
|
||||
bits |= (resb > 0 ? 1 : 0) * power;
|
||||
power <<= 1;
|
||||
}
|
||||
switch ((next = bits)) {
|
||||
case 0:
|
||||
bits = 0;
|
||||
maxpower = Math.pow(2, 8);
|
||||
power = 1;
|
||||
while (power != maxpower) {
|
||||
resb = data.val & data.position;
|
||||
data.position >>= 1;
|
||||
if (data.position == 0) {
|
||||
data.position = resetValue;
|
||||
data.val = getNextValue(data.index++);
|
||||
}
|
||||
bits |= (resb > 0 ? 1 : 0) * power;
|
||||
power <<= 1;
|
||||
}
|
||||
c = fromCharCode(bits);
|
||||
break;
|
||||
case 1:
|
||||
bits = 0;
|
||||
maxpower = Math.pow(2, 16);
|
||||
power = 1;
|
||||
while (power != maxpower) {
|
||||
resb = data.val & data.position;
|
||||
data.position >>= 1;
|
||||
if (data.position == 0) {
|
||||
data.position = resetValue;
|
||||
data.val = getNextValue(data.index++);
|
||||
}
|
||||
bits |= (resb > 0 ? 1 : 0) * power;
|
||||
power <<= 1;
|
||||
}
|
||||
c = fromCharCode(bits);
|
||||
break;
|
||||
case 2:
|
||||
return "";
|
||||
}
|
||||
dictionary[3] = c;
|
||||
w = c;
|
||||
result.push(c);
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
if (data.index > length) {
|
||||
return "";
|
||||
}
|
||||
bits = 0;
|
||||
maxpower = Math.pow(2, numBits);
|
||||
power = 1;
|
||||
while (power != maxpower) {
|
||||
resb = data.val & data.position;
|
||||
data.position >>= 1;
|
||||
if (data.position == 0) {
|
||||
data.position = resetValue;
|
||||
data.val = getNextValue(data.index++);
|
||||
}
|
||||
bits |= (resb > 0 ? 1 : 0) * power;
|
||||
power <<= 1;
|
||||
}
|
||||
switch ((c = bits)) {
|
||||
case 0:
|
||||
bits = 0;
|
||||
maxpower = Math.pow(2, 8);
|
||||
power = 1;
|
||||
while (power != maxpower) {
|
||||
resb = data.val & data.position;
|
||||
data.position >>= 1;
|
||||
if (data.position == 0) {
|
||||
data.position = resetValue;
|
||||
data.val = getNextValue(data.index++);
|
||||
}
|
||||
bits |= (resb > 0 ? 1 : 0) * power;
|
||||
power <<= 1;
|
||||
}
|
||||
dictionary[dictSize++] = fromCharCode(bits);
|
||||
c = dictSize - 1;
|
||||
enlargeIn--;
|
||||
break;
|
||||
case 1:
|
||||
bits = 0;
|
||||
maxpower = Math.pow(2, 16);
|
||||
power = 1;
|
||||
while (power != maxpower) {
|
||||
resb = data.val & data.position;
|
||||
data.position >>= 1;
|
||||
if (data.position == 0) {
|
||||
data.position = resetValue;
|
||||
data.val = getNextValue(data.index++);
|
||||
}
|
||||
bits |= (resb > 0 ? 1 : 0) * power;
|
||||
power <<= 1;
|
||||
}
|
||||
dictionary[dictSize++] = fromCharCode(bits);
|
||||
c = dictSize - 1;
|
||||
enlargeIn--;
|
||||
break;
|
||||
case 2:
|
||||
return result.join("");
|
||||
}
|
||||
if (enlargeIn == 0) {
|
||||
enlargeIn = Math.pow(2, numBits);
|
||||
numBits++;
|
||||
}
|
||||
if (dictionary[c]) {
|
||||
// @ts-ignore
|
||||
entry = dictionary[c];
|
||||
}
|
||||
else {
|
||||
if (c === dictSize) {
|
||||
entry = w + w.charAt(0);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
result.push(entry);
|
||||
// Add w+entry[0] to the dictionary.
|
||||
dictionary[dictSize++] = w + entry.charAt(0);
|
||||
enlargeIn--;
|
||||
w = entry;
|
||||
if (enlargeIn == 0) {
|
||||
enlargeIn = Math.pow(2, numBits);
|
||||
numBits++;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,362 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end */
|
||||
import { Signal, STOP_PROPAGATION } from "./signal";
|
||||
import { arrayDeleteValue, waitNextFrame } from "./utils";
|
||||
import { ClickDetector } from "./click_detector";
|
||||
import { SOUNDS } from "../platform/sound";
|
||||
import { InputReceiver } from "./input_receiver";
|
||||
import { FormElement } from "./modal_dialog_forms";
|
||||
import { globalConfig } from "./config";
|
||||
import { getStringForKeyCode } from "../game/key_action_mapper";
|
||||
import { createLogger } from "./logging";
|
||||
import { T } from "../translations";
|
||||
/*
|
||||
* ***************************************************
|
||||
*
|
||||
* LEGACY CODE WARNING
|
||||
*
|
||||
* This is old code from yorg3.io and needs to be refactored
|
||||
* @TODO
|
||||
*
|
||||
* ***************************************************
|
||||
*/
|
||||
const kbEnter: any = 13;
|
||||
const kbCancel: any = 27;
|
||||
const logger: any = createLogger("dialogs");
|
||||
/**
|
||||
* Basic text based dialog
|
||||
*/
|
||||
export class Dialog {
|
||||
public app = app;
|
||||
public title = title;
|
||||
public contentHTML = contentHTML;
|
||||
public type = type;
|
||||
public buttonIds = buttons;
|
||||
public closeButton = closeButton;
|
||||
public closeRequested = new Signal();
|
||||
public buttonSignals = {};
|
||||
public valueChosen = new Signal();
|
||||
public timeouts = [];
|
||||
public clickDetectors = [];
|
||||
public inputReciever = new InputReceiver("dialog-" + this.title);
|
||||
public enterHandler = null;
|
||||
public escapeHandler = null;
|
||||
/**
|
||||
*
|
||||
* Constructs a new dialog with the given options
|
||||
*/
|
||||
|
||||
constructor({ app, title, contentHTML, buttons, type = "info", closeButton = false }) {
|
||||
for (let i: any = 0; i < buttons.length; ++i) {
|
||||
if (G_IS_DEV && globalConfig.debug.disableTimedButtons) {
|
||||
this.buttonIds[i] = this.buttonIds[i].replace(":timeout", "");
|
||||
}
|
||||
const buttonId: any = this.buttonIds[i].split(":")[0];
|
||||
this.buttonSignals[buttonId] = new Signal();
|
||||
}
|
||||
this.inputReciever.keydown.add(this.handleKeydown, this);
|
||||
}
|
||||
/**
|
||||
* Internal keydown handler
|
||||
*/
|
||||
handleKeydown({ keyCode, shift, alt, ctrl }: {
|
||||
keyCode: number;
|
||||
shift: boolean;
|
||||
alt: boolean;
|
||||
ctrl: boolean;
|
||||
}): any {
|
||||
if (keyCode === kbEnter && this.enterHandler) {
|
||||
this.internalButtonHandler(this.enterHandler);
|
||||
return STOP_PROPAGATION;
|
||||
}
|
||||
if (keyCode === kbCancel && this.escapeHandler) {
|
||||
this.internalButtonHandler(this.escapeHandler);
|
||||
return STOP_PROPAGATION;
|
||||
}
|
||||
}
|
||||
internalButtonHandler(id: any, ...payload: any): any {
|
||||
this.app.inputMgr.popReciever(this.inputReciever);
|
||||
if (id !== "close-button") {
|
||||
this.buttonSignals[id].dispatch(...payload);
|
||||
}
|
||||
this.closeRequested.dispatch();
|
||||
}
|
||||
createElement(): any {
|
||||
const elem: any = document.createElement("div");
|
||||
elem.classList.add("ingameDialog");
|
||||
this.dialogElem = document.createElement("div");
|
||||
this.dialogElem.classList.add("dialogInner");
|
||||
if (this.type) {
|
||||
this.dialogElem.classList.add(this.type);
|
||||
}
|
||||
elem.appendChild(this.dialogElem);
|
||||
const title: any = document.createElement("h1");
|
||||
title.innerText = this.title;
|
||||
title.classList.add("title");
|
||||
this.dialogElem.appendChild(title);
|
||||
if (this.closeButton) {
|
||||
this.dialogElem.classList.add("hasCloseButton");
|
||||
const closeBtn: any = document.createElement("button");
|
||||
closeBtn.classList.add("closeButton");
|
||||
this.trackClicks(closeBtn, (): any => this.internalButtonHandler("close-button"), {
|
||||
applyCssClass: "pressedSmallElement",
|
||||
});
|
||||
title.appendChild(closeBtn);
|
||||
this.inputReciever.backButton.add((): any => this.internalButtonHandler("close-button"));
|
||||
}
|
||||
const content: any = document.createElement("div");
|
||||
content.classList.add("content");
|
||||
content.innerHTML = this.contentHTML;
|
||||
this.dialogElem.appendChild(content);
|
||||
if (this.buttonIds.length > 0) {
|
||||
const buttons: any = document.createElement("div");
|
||||
buttons.classList.add("buttons");
|
||||
// Create buttons
|
||||
for (let i: any = 0; i < this.buttonIds.length; ++i) {
|
||||
const [buttonId, buttonStyle, rawParams]: any = this.buttonIds[i].split(":");
|
||||
const button: any = document.createElement("button");
|
||||
button.classList.add("button");
|
||||
button.classList.add("styledButton");
|
||||
button.classList.add(buttonStyle);
|
||||
button.innerText = T.dialogs.buttons[buttonId];
|
||||
const params: any = (rawParams || "").split("/");
|
||||
const useTimeout: any = params.indexOf("timeout") >= 0;
|
||||
const isEnter: any = params.indexOf("enter") >= 0;
|
||||
const isEscape: any = params.indexOf("escape") >= 0;
|
||||
if (isEscape && this.closeButton) {
|
||||
logger.warn("Showing dialog with close button, and additional cancel button");
|
||||
}
|
||||
if (useTimeout) {
|
||||
button.classList.add("timedButton");
|
||||
const timeout: any = setTimeout((): any => {
|
||||
button.classList.remove("timedButton");
|
||||
arrayDeleteValue(this.timeouts, timeout);
|
||||
}, 1000);
|
||||
this.timeouts.push(timeout);
|
||||
}
|
||||
if (isEnter || isEscape) {
|
||||
// if (this.app.settings.getShowKeyboardShortcuts()) {
|
||||
// Show keybinding
|
||||
const spacer: any = document.createElement("code");
|
||||
spacer.classList.add("keybinding");
|
||||
spacer.innerHTML = getStringForKeyCode(isEnter ? kbEnter : kbCancel);
|
||||
button.appendChild(spacer);
|
||||
// }
|
||||
if (isEnter) {
|
||||
this.enterHandler = buttonId;
|
||||
}
|
||||
if (isEscape) {
|
||||
this.escapeHandler = buttonId;
|
||||
}
|
||||
}
|
||||
this.trackClicks(button, (): any => this.internalButtonHandler(buttonId));
|
||||
buttons.appendChild(button);
|
||||
}
|
||||
this.dialogElem.appendChild(buttons);
|
||||
}
|
||||
else {
|
||||
this.dialogElem.classList.add("buttonless");
|
||||
}
|
||||
this.element = elem;
|
||||
this.app.inputMgr.pushReciever(this.inputReciever);
|
||||
return this.element;
|
||||
}
|
||||
setIndex(index: any): any {
|
||||
this.element.style.zIndex = index;
|
||||
}
|
||||
destroy(): any {
|
||||
if (!this.element) {
|
||||
assert(false, "Tried to destroy dialog twice");
|
||||
return;
|
||||
}
|
||||
// We need to do this here, because if the backbutton event gets
|
||||
// dispatched to the modal dialogs, it will not call the internalButtonHandler,
|
||||
// and thus our receiver stays attached the whole time
|
||||
this.app.inputMgr.destroyReceiver(this.inputReciever);
|
||||
for (let i: any = 0; i < this.clickDetectors.length; ++i) {
|
||||
this.clickDetectors[i].cleanup();
|
||||
}
|
||||
this.clickDetectors = [];
|
||||
this.element.remove();
|
||||
this.element = null;
|
||||
for (let i: any = 0; i < this.timeouts.length; ++i) {
|
||||
clearTimeout(this.timeouts[i]);
|
||||
}
|
||||
this.timeouts = [];
|
||||
}
|
||||
hide(): any {
|
||||
this.element.classList.remove("visible");
|
||||
}
|
||||
show(): any {
|
||||
this.element.classList.add("visible");
|
||||
}
|
||||
/**
|
||||
* Helper method to track clicks on an element
|
||||
* {}
|
||||
*/
|
||||
trackClicks(elem: Element, handler: function():void, args: import("./click_detector").ClickDetectorConstructorArgs= = {}): ClickDetector {
|
||||
const detector: any = new ClickDetector(elem, args);
|
||||
detector.click.add(handler, this);
|
||||
this.clickDetectors.push(detector);
|
||||
return detector;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Dialog which simply shows a loading spinner
|
||||
*/
|
||||
export class DialogLoading extends Dialog {
|
||||
public text = text;
|
||||
|
||||
constructor(app, text = "") {
|
||||
super({
|
||||
app,
|
||||
title: "",
|
||||
contentHTML: "",
|
||||
buttons: [],
|
||||
type: "loading",
|
||||
});
|
||||
// Loading dialog can not get closed with back button
|
||||
this.inputReciever.backButton.removeAll();
|
||||
this.inputReciever.context = "dialog-loading";
|
||||
}
|
||||
createElement(): any {
|
||||
const elem: any = document.createElement("div");
|
||||
elem.classList.add("ingameDialog");
|
||||
elem.classList.add("loadingDialog");
|
||||
this.element = elem;
|
||||
if (this.text) {
|
||||
const text: any = document.createElement("div");
|
||||
text.classList.add("text");
|
||||
text.innerText = this.text;
|
||||
elem.appendChild(text);
|
||||
}
|
||||
const loader: any = document.createElement("div");
|
||||
loader.classList.add("prefab_LoadingTextWithAnim");
|
||||
loader.classList.add("loadingIndicator");
|
||||
elem.appendChild(loader);
|
||||
this.app.inputMgr.pushReciever(this.inputReciever);
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
export class DialogOptionChooser extends Dialog {
|
||||
public options = options;
|
||||
public initialOption = options.active;
|
||||
|
||||
constructor({ app, title, options }) {
|
||||
let html: any = "<div class='optionParent'>";
|
||||
options.options.forEach(({ value, text, desc = null, iconPrefix = null }: any): any => {
|
||||
const descHtml: any = desc ? `<span class="desc">${desc}</span>` : "";
|
||||
let iconHtml: any = iconPrefix ? `<span class="icon icon-${iconPrefix}-${value}"></span>` : "";
|
||||
html += `
|
||||
<div class='option ${value === options.active ? "active" : ""} ${iconPrefix ? "hasIcon" : ""}' data-optionvalue='${value}'>
|
||||
${iconHtml}
|
||||
<span class='title'>${text}</span>
|
||||
${descHtml}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += "</div>";
|
||||
super({
|
||||
app,
|
||||
title,
|
||||
contentHTML: html,
|
||||
buttons: [],
|
||||
type: "info",
|
||||
closeButton: true,
|
||||
});
|
||||
this.buttonSignals.optionSelected = new Signal();
|
||||
}
|
||||
createElement(): any {
|
||||
const div: any = super.createElement();
|
||||
this.dialogElem.classList.add("optionChooserDialog");
|
||||
div.querySelectorAll("[data-optionvalue]").forEach((handle: any): any => {
|
||||
const value: any = handle.getAttribute("data-optionvalue");
|
||||
if (!handle) {
|
||||
logger.error("Failed to bind option value in dialog:", value);
|
||||
return;
|
||||
}
|
||||
// Need click detector here to forward elements, otherwise scrolling does not work
|
||||
const detector: any = new ClickDetector(handle, {
|
||||
consumeEvents: false,
|
||||
preventDefault: false,
|
||||
clickSound: null,
|
||||
applyCssClass: "pressedOption",
|
||||
targetOnly: true,
|
||||
});
|
||||
this.clickDetectors.push(detector);
|
||||
if (value !== this.initialOption) {
|
||||
detector.click.add((): any => {
|
||||
const selected: any = div.querySelector(".option.active");
|
||||
if (selected) {
|
||||
selected.classList.remove("active");
|
||||
}
|
||||
else {
|
||||
logger.warn("No selected option");
|
||||
}
|
||||
handle.classList.add("active");
|
||||
this.app.sound.playUiSound(SOUNDS.uiClick);
|
||||
this.internalButtonHandler("optionSelected", value);
|
||||
});
|
||||
}
|
||||
});
|
||||
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, }) {
|
||||
let html: any = "";
|
||||
html += desc + "<br>";
|
||||
for (let i: any = 0; i < formElements.length; ++i) {
|
||||
html += formElements[i].getHtml();
|
||||
}
|
||||
super({
|
||||
app,
|
||||
title: title,
|
||||
contentHTML: html,
|
||||
buttons: buttons,
|
||||
type: "info",
|
||||
closeButton,
|
||||
});
|
||||
}
|
||||
internalButtonHandler(id: any, ...payload: any): any {
|
||||
if (id === this.confirmButtonId) {
|
||||
if (this.hasAnyInvalid()) {
|
||||
this.dialogElem.classList.remove("errorShake");
|
||||
waitNextFrame().then((): any => {
|
||||
if (this.dialogElem) {
|
||||
this.dialogElem.classList.add("errorShake");
|
||||
}
|
||||
});
|
||||
this.app.sound.playUiSound(SOUNDS.uiError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
super.internalButtonHandler(id, payload);
|
||||
}
|
||||
hasAnyInvalid(): any {
|
||||
for (let i: any = 0; i < this.formElements.length; ++i) {
|
||||
if (!this.formElements[i].isValid()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
createElement(): any {
|
||||
const div: any = super.createElement();
|
||||
for (let i: any = 0; i < this.formElements.length; ++i) {
|
||||
const elem: any = this.formElements[i];
|
||||
elem.bindEvents(div, this.clickDetectors);
|
||||
// elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested);
|
||||
elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen);
|
||||
}
|
||||
waitNextFrame().then((): any => {
|
||||
this.formElements[this.formElements.length - 1].focus();
|
||||
});
|
||||
return div;
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
import { BaseItem } from "../game/base_item";
|
||||
import { ClickDetector } from "./click_detector";
|
||||
import { Signal } from "./signal";
|
||||
/*
|
||||
* ***************************************************
|
||||
*
|
||||
* LEGACY CODE WARNING
|
||||
*
|
||||
* This is old code from yorg3.io and needs to be refactored
|
||||
* @TODO
|
||||
*
|
||||
* ***************************************************
|
||||
*/
|
||||
export class FormElement {
|
||||
public id = id;
|
||||
public label = label;
|
||||
public valueChosen = new Signal();
|
||||
|
||||
constructor(id, label) {
|
||||
}
|
||||
getHtml(): any {
|
||||
abstract;
|
||||
return "";
|
||||
}
|
||||
getFormElement(parent: any): any {
|
||||
return parent.querySelector("[data-formId='" + this.id + "']");
|
||||
}
|
||||
bindEvents(parent: any, clickTrackers: any): any {
|
||||
abstract;
|
||||
}
|
||||
focus(): any { }
|
||||
isValid(): any {
|
||||
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 }) {
|
||||
super(id, label);
|
||||
}
|
||||
getHtml(): any {
|
||||
let classes: any = [];
|
||||
let inputType: any = "text";
|
||||
let maxlength: any = 256;
|
||||
switch (this.inputType) {
|
||||
case "text": {
|
||||
classes.push("input-text");
|
||||
break;
|
||||
}
|
||||
case "email": {
|
||||
classes.push("input-email");
|
||||
inputType = "email";
|
||||
break;
|
||||
}
|
||||
case "token": {
|
||||
classes.push("input-token");
|
||||
inputType = "text";
|
||||
maxlength = 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return `
|
||||
<div class="formElement input">
|
||||
${this.label ? `<label>${this.label}</label>` : ""}
|
||||
<input
|
||||
type="${inputType}"
|
||||
value="${this.defaultValue.replace(/["\\]+/gi, "")}"
|
||||
maxlength="${maxlength}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
class="${classes.join(" ")}"
|
||||
placeholder="${this.placeholder.replace(/["\\]+/gi, "")}"
|
||||
data-formId="${this.id}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
bindEvents(parent: any, clickTrackers: any): any {
|
||||
this.element = this.getFormElement(parent);
|
||||
this.element.addEventListener("input", (event: any): any => this.updateErrorState());
|
||||
this.updateErrorState();
|
||||
}
|
||||
updateErrorState(): any {
|
||||
this.element.classList.toggle("errored", !this.isValid());
|
||||
}
|
||||
isValid(): any {
|
||||
return !this.validator || this.validator(this.element.value);
|
||||
}
|
||||
getValue(): any {
|
||||
return this.element.value;
|
||||
}
|
||||
setValue(value: any): any {
|
||||
this.element.value = value;
|
||||
this.updateErrorState();
|
||||
}
|
||||
focus(): any {
|
||||
this.element.focus();
|
||||
this.element.select();
|
||||
}
|
||||
}
|
||||
export class FormElementCheckbox extends FormElement {
|
||||
public defaultValue = defaultValue;
|
||||
public value = this.defaultValue;
|
||||
public element = null;
|
||||
|
||||
constructor({ id, label, defaultValue = true }) {
|
||||
super(id, label);
|
||||
}
|
||||
getHtml(): any {
|
||||
return `
|
||||
<div class="formElement checkBoxFormElem">
|
||||
${this.label ? `<label>${this.label}</label>` : ""}
|
||||
<div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
|
||||
<span class="knob"></span >
|
||||
</div >
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
bindEvents(parent: any, clickTrackers: any): any {
|
||||
this.element = this.getFormElement(parent);
|
||||
const detector: any = new ClickDetector(this.element, {
|
||||
consumeEvents: false,
|
||||
preventDefault: false,
|
||||
});
|
||||
clickTrackers.push(detector);
|
||||
detector.click.add(this.toggle, this);
|
||||
}
|
||||
getValue(): any {
|
||||
return this.value;
|
||||
}
|
||||
toggle(): any {
|
||||
this.value = !this.value;
|
||||
this.element.classList.toggle("checked", this.value);
|
||||
}
|
||||
focus(parent: any): any { }
|
||||
}
|
||||
export class FormElementItemChooser extends FormElement {
|
||||
public items = items;
|
||||
public element = null;
|
||||
public chosenItem: BaseItem = null;
|
||||
|
||||
constructor({ id, label, items = [] }) {
|
||||
super(id, label);
|
||||
}
|
||||
getHtml(): any {
|
||||
let classes: any = [];
|
||||
return `
|
||||
<div class="formElement">
|
||||
${this.label ? `<label>${this.label}</label>` : ""}
|
||||
<div class="ingameItemChooser input" data-formId="${this.id}"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
bindEvents(parent: HTMLElement, clickTrackers: Array<ClickDetector>): any {
|
||||
this.element = this.getFormElement(parent);
|
||||
for (let i: any = 0; i < this.items.length; ++i) {
|
||||
const item: any = this.items[i];
|
||||
const canvas: any = document.createElement("canvas");
|
||||
canvas.width = 128;
|
||||
canvas.height = 128;
|
||||
const context: any = canvas.getContext("2d");
|
||||
item.drawFullSizeOnCanvas(context, 128);
|
||||
this.element.appendChild(canvas);
|
||||
const detector: any = new ClickDetector(canvas, {});
|
||||
clickTrackers.push(detector);
|
||||
detector.click.add((): any => {
|
||||
this.chosenItem = item;
|
||||
this.valueChosen.dispatch(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
isValid(): any {
|
||||
return true;
|
||||
}
|
||||
getValue(): any {
|
||||
return null;
|
||||
}
|
||||
focus(): any { }
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
function mathPolyfills(): any {
|
||||
// Converts from degrees to radians.
|
||||
Math.radians = function (degrees: any): any {
|
||||
return (degrees * Math.PI) / 180.0;
|
||||
};
|
||||
// Converts from radians to degrees.
|
||||
Math.degrees = function (radians: any): any {
|
||||
return (radians * 180.0) / Math.PI;
|
||||
};
|
||||
}
|
||||
function stringPolyfills(): any {
|
||||
// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
|
||||
if (!String.prototype.padStart) {
|
||||
String.prototype.padStart = function padStart(targetLength: any, padString: any): any {
|
||||
targetLength = targetLength >> 0; //truncate if number, or convert non-number to 0;
|
||||
padString = String(typeof padString !== "undefined" ? padString : " ");
|
||||
if (this.length >= targetLength) {
|
||||
return String(this);
|
||||
}
|
||||
else {
|
||||
targetLength = targetLength - this.length;
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
|
||||
}
|
||||
return padString.slice(0, targetLength) + String(this);
|
||||
}
|
||||
};
|
||||
}
|
||||
// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd
|
||||
if (!String.prototype.padEnd) {
|
||||
String.prototype.padEnd = function padEnd(targetLength: any, padString: any): any {
|
||||
targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
|
||||
padString = String(typeof padString !== "undefined" ? padString : " ");
|
||||
if (this.length > targetLength) {
|
||||
return String(this);
|
||||
}
|
||||
else {
|
||||
targetLength = targetLength - this.length;
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
|
||||
}
|
||||
return String(this) + padString.slice(0, targetLength);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
function objectPolyfills(): any {
|
||||
// https://github.com/tc39/proposal-object-values-entries/blob/master/polyfill.js
|
||||
// @ts-ignore
|
||||
const reduce: any = Function.bind.call(Function.call, Array.prototype.reduce);
|
||||
// @ts-ignore
|
||||
const isEnumerable: any = Function.bind.call(Function.call, Object.prototype.propertyIsEnumerable);
|
||||
// @ts-ignore
|
||||
const concat: any = Function.bind.call(Function.call, Array.prototype.concat);
|
||||
const keys: any = Reflect.ownKeys;
|
||||
// @ts-ignore
|
||||
if (!Object.values) {
|
||||
// @ts-ignore
|
||||
Object.values = function values(O: any): any {
|
||||
return reduce(keys(O), (v: any, k: any): any => concat(v, typeof k === "string" && isEnumerable(O, k) ? [O[k]] : []), []);
|
||||
};
|
||||
}
|
||||
if (!Object.entries) {
|
||||
// @ts-ignore
|
||||
Object.entries = function entries(O: any): any {
|
||||
return reduce(keys(O), (e: any, k: any): any => concat(e, typeof k === "string" && isEnumerable(O, k) ? [[k, O[k]]] : []), []);
|
||||
};
|
||||
}
|
||||
}
|
||||
function domPolyfills(): any {
|
||||
// from:https://github.com/jserz/js_piece/blob/master/DOM/ChildNode/remove()/remove().md
|
||||
(function (arr: any): any {
|
||||
arr.forEach(function (item: any): any {
|
||||
if (item.hasOwnProperty("remove")) {
|
||||
return;
|
||||
}
|
||||
Object.defineProperty(item, "remove", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function remove(): any {
|
||||
this.parentNode.removeChild(this);
|
||||
},
|
||||
});
|
||||
});
|
||||
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
|
||||
}
|
||||
function initPolyfills(): any {
|
||||
mathPolyfills();
|
||||
stringPolyfills();
|
||||
objectPolyfills();
|
||||
domPolyfills();
|
||||
}
|
||||
function initExtensions(): any {
|
||||
String.prototype.replaceAll = function (search: any, replacement: any): any {
|
||||
var target: any = this;
|
||||
return target.split(search).join(replacement);
|
||||
};
|
||||
}
|
||||
// Fetch polyfill
|
||||
import "whatwg-fetch";
|
||||
// Other polyfills
|
||||
initPolyfills();
|
||||
initExtensions();
|
@ -0,0 +1,24 @@
|
||||
const queryString: any = require("query-string");
|
||||
const options: any = queryString.parse(location.search);
|
||||
export let queryParamOptions: any = {
|
||||
embedProvider: null,
|
||||
abtVariant: null,
|
||||
campaign: null,
|
||||
fbclid: null,
|
||||
gclid: null,
|
||||
};
|
||||
if (options.embed) {
|
||||
queryParamOptions.embedProvider = options.embed;
|
||||
}
|
||||
if (options.abtVariant) {
|
||||
queryParamOptions.abtVariant = options.abtVariant;
|
||||
}
|
||||
if (options.fbclid) {
|
||||
queryParamOptions.fbclid = options.fbclid;
|
||||
}
|
||||
if (options.gclid) {
|
||||
queryParamOptions.gclid = options.gclid;
|
||||
}
|
||||
if (options.utm_campaign) {
|
||||
queryParamOptions.campaign = options.utm_campaign;
|
||||
}
|
@ -0,0 +1,253 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end */
|
||||
import { sha1, CRC_PREFIX, computeCrc } from "./sensitive_utils.encrypt";
|
||||
import { createLogger } from "./logging";
|
||||
import { FILE_NOT_FOUND } from "../platform/storage";
|
||||
import { accessNestedPropertyReverse } from "./utils";
|
||||
import { IS_DEBUG, globalConfig } from "./config";
|
||||
import { ExplainedResult } from "./explained_result";
|
||||
import { decompressX64, compressX64 } from "./lzstring";
|
||||
import { asyncCompressor, compressionPrefix } from "./async_compression";
|
||||
import { compressObject, decompressObject } from "../savegame/savegame_compressor";
|
||||
const debounce: any = require("debounce-promise");
|
||||
const logger: any = createLogger("read_write_proxy");
|
||||
const salt: any = accessNestedPropertyReverse(globalConfig, ["file", "info"]);
|
||||
// Helper which only writes / reads if verify() works. Also performs migration
|
||||
export class ReadWriteProxy {
|
||||
public app: Application = app;
|
||||
public filename = filename;
|
||||
public currentData: object = null;
|
||||
public debouncedWrite = debounce(this.doWriteAsync.bind(this), 50);
|
||||
|
||||
constructor(app, filename) {
|
||||
// TODO: EXTREMELY HACKY! To verify we need to do this a step later
|
||||
if (G_IS_DEV && IS_DEBUG) {
|
||||
setTimeout((): any => {
|
||||
assert(this.verify(this.getDefaultData()).result, "Verify() failed for default data: " + this.verify(this.getDefaultData()).reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
// -- Methods to override
|
||||
/** {} */
|
||||
verify(data: any): ExplainedResult {
|
||||
abstract;
|
||||
return ExplainedResult.bad();
|
||||
}
|
||||
// Should return the default data
|
||||
getDefaultData(): any {
|
||||
abstract;
|
||||
return {};
|
||||
}
|
||||
// Should return the current version as an integer
|
||||
getCurrentVersion(): any {
|
||||
abstract;
|
||||
return 0;
|
||||
}
|
||||
// Should migrate the data (Modify in place)
|
||||
/** {} */
|
||||
migrate(data: any): ExplainedResult {
|
||||
abstract;
|
||||
return ExplainedResult.bad();
|
||||
}
|
||||
// -- / Methods
|
||||
// Resets whole data, returns promise
|
||||
resetEverythingAsync(): any {
|
||||
logger.warn("Reset data to default");
|
||||
this.currentData = this.getDefaultData();
|
||||
return this.writeAsync();
|
||||
}
|
||||
static serializeObject(obj: object): any {
|
||||
const jsonString: any = JSON.stringify(compressObject(obj));
|
||||
const checksum: any = computeCrc(jsonString + salt);
|
||||
return compressionPrefix + compressX64(checksum + jsonString);
|
||||
}
|
||||
static deserializeObject(text: object): any {
|
||||
const decompressed: any = decompressX64(text.substr(compressionPrefix.length));
|
||||
if (!decompressed) {
|
||||
// LZ string decompression failure
|
||||
throw new Error("bad-content / decompression-failed");
|
||||
}
|
||||
if (decompressed.length < 40) {
|
||||
// String too short
|
||||
throw new Error("bad-content / payload-too-small");
|
||||
}
|
||||
// Compare stored checksum with actual checksum
|
||||
const checksum: any = decompressed.substring(0, 40);
|
||||
const jsonString: any = decompressed.substr(40);
|
||||
const desiredChecksum: any = checksum.startsWith(CRC_PREFIX)
|
||||
? computeCrc(jsonString + salt)
|
||||
: sha1(jsonString + salt);
|
||||
if (desiredChecksum !== checksum) {
|
||||
// Checksum mismatch
|
||||
throw new Error("bad-content / checksum-mismatch");
|
||||
}
|
||||
const parsed: any = JSON.parse(jsonString);
|
||||
const decoded: any = decompressObject(parsed);
|
||||
return decoded;
|
||||
}
|
||||
/**
|
||||
* Writes the data asychronously, fails if verify() fails.
|
||||
* Debounces the operation by up to 50ms
|
||||
* {}
|
||||
*/
|
||||
writeAsync(): Promise<void> {
|
||||
const verifyResult: any = this.internalVerifyEntry(this.currentData);
|
||||
if (!verifyResult.result) {
|
||||
logger.error("Tried to write invalid data to", this.filename, "reason:", verifyResult.reason);
|
||||
return Promise.reject(verifyResult.reason);
|
||||
}
|
||||
return this.debouncedWrite();
|
||||
}
|
||||
/**
|
||||
* Actually writes the data asychronously
|
||||
* {}
|
||||
*/
|
||||
doWriteAsync(): Promise<void> {
|
||||
return asyncCompressor
|
||||
.compressObjectAsync(this.currentData)
|
||||
.then((compressed: any): any => {
|
||||
return this.app.storage.writeFileAsync(this.filename, compressed);
|
||||
})
|
||||
.then((): any => {
|
||||
logger.log("📄 Wrote", this.filename);
|
||||
})
|
||||
.catch((err: any): any => {
|
||||
logger.error("Failed to write", this.filename, ":", err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
// Reads the data asynchronously, fails if verify() fails
|
||||
readAsync(): any {
|
||||
// Start read request
|
||||
return (this.app.storage
|
||||
.readFileAsync(this.filename)
|
||||
// Check for errors during read
|
||||
.catch((err: any): any => {
|
||||
if (err === FILE_NOT_FOUND) {
|
||||
logger.log("File not found, using default data");
|
||||
// File not found or unreadable, assume default file
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return Promise.reject("file-error: " + err);
|
||||
})
|
||||
// Decrypt data (if its encrypted)
|
||||
// @ts-ignore
|
||||
.then((rawData: any): any => {
|
||||
if (rawData == null) {
|
||||
// So, the file has not been found, use default data
|
||||
return JSON.stringify(compressObject(this.getDefaultData()));
|
||||
}
|
||||
if (rawData.startsWith(compressionPrefix)) {
|
||||
const decompressed: any = decompressX64(rawData.substr(compressionPrefix.length));
|
||||
if (!decompressed) {
|
||||
// LZ string decompression failure
|
||||
return Promise.reject("bad-content / decompression-failed");
|
||||
}
|
||||
if (decompressed.length < 40) {
|
||||
// String too short
|
||||
return Promise.reject("bad-content / payload-too-small");
|
||||
}
|
||||
// Compare stored checksum with actual checksum
|
||||
const checksum: any = decompressed.substring(0, 40);
|
||||
const jsonString: any = decompressed.substr(40);
|
||||
const desiredChecksum: any = checksum.startsWith(CRC_PREFIX)
|
||||
? computeCrc(jsonString + salt)
|
||||
: sha1(jsonString + salt);
|
||||
if (desiredChecksum !== checksum) {
|
||||
// Checksum mismatch
|
||||
return Promise.reject("bad-content / checksum-mismatch: " + desiredChecksum + " vs " + checksum);
|
||||
}
|
||||
return jsonString;
|
||||
}
|
||||
else {
|
||||
if (!G_IS_DEV) {
|
||||
return Promise.reject("bad-content / missing-compression");
|
||||
}
|
||||
}
|
||||
return rawData;
|
||||
})
|
||||
// Parse JSON, this could throw but that's fine
|
||||
.then((res: any): any => {
|
||||
try {
|
||||
return JSON.parse(res);
|
||||
}
|
||||
catch (ex: any) {
|
||||
logger.error("Failed to parse file content of", this.filename, ":", ex, "(content was:", res, ")");
|
||||
throw new Error("invalid-serialized-data");
|
||||
}
|
||||
})
|
||||
// Decompress
|
||||
.then((compressed: any): any => decompressObject(compressed))
|
||||
// Verify basic structure
|
||||
.then((contents: any): any => {
|
||||
const result: any = this.internalVerifyBasicStructure(contents);
|
||||
if (!result.isGood()) {
|
||||
return Promise.reject("verify-failed: " + result.reason);
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
// Check version and migrate if required
|
||||
.then((contents: any): any => {
|
||||
if (contents.version > this.getCurrentVersion()) {
|
||||
return Promise.reject("stored-data-is-newer");
|
||||
}
|
||||
if (contents.version < this.getCurrentVersion()) {
|
||||
logger.log("Trying to migrate data object from version", contents.version, "to", this.getCurrentVersion());
|
||||
const migrationResult: any = this.migrate(contents); // modify in place
|
||||
if (migrationResult.isBad()) {
|
||||
return Promise.reject("migration-failed: " + migrationResult.reason);
|
||||
}
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
// Verify
|
||||
.then((contents: any): any => {
|
||||
const verifyResult: any = 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: any): any => {
|
||||
this.currentData = contents;
|
||||
logger.log("📄 Read data with version", this.currentData.version, "from", this.filename);
|
||||
return contents;
|
||||
})
|
||||
// Catchall
|
||||
.catch((err: any): any => {
|
||||
return Promise.reject("Failed to read " + this.filename + ": " + err);
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Deletes the file
|
||||
* {}
|
||||
*/
|
||||
deleteAsync(): Promise<void> {
|
||||
return this.app.storage.deleteFileAsync(this.filename);
|
||||
}
|
||||
// Internal
|
||||
/** {} */
|
||||
internalVerifyBasicStructure(data: any): ExplainedResult {
|
||||
if (!data) {
|
||||
return ExplainedResult.bad("Data is empty");
|
||||
}
|
||||
if (!Number.isInteger(data.version) || data.version < 0) {
|
||||
return ExplainedResult.bad(`Data has invalid version: ${data.version} (expected ${this.getCurrentVersion()})`);
|
||||
}
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
/** {} */
|
||||
internalVerifyEntry(data: any): ExplainedResult {
|
||||
if (data.version !== this.getCurrentVersion()) {
|
||||
return ExplainedResult.bad("Version mismatch, got " + data.version + " and expected " + this.getCurrentVersion());
|
||||
}
|
||||
const verifyStructureError: any = this.internalVerifyBasicStructure(data);
|
||||
if (!verifyStructureError.isGood()) {
|
||||
return verifyStructureError;
|
||||
}
|
||||
return this.verify(data);
|
||||
}
|
||||
}
|
@ -0,0 +1,283 @@
|
||||
import { globalConfig } from "./config";
|
||||
import { epsilonCompare, round2Digits } from "./utils";
|
||||
import { Vector } from "./vector";
|
||||
export class Rectangle {
|
||||
public x = x;
|
||||
public y = y;
|
||||
public w = w;
|
||||
public h = h;
|
||||
|
||||
constructor(x = 0, y = 0, w = 0, h = 0) {
|
||||
}
|
||||
/**
|
||||
* Creates a rectangle from top right bottom and left offsets
|
||||
*/
|
||||
static fromTRBL(top: number, right: number, bottom: number, left: number): any {
|
||||
return new Rectangle(left, top, right - left, bottom - top);
|
||||
}
|
||||
/**
|
||||
* Constructs a new square rectangle
|
||||
*/
|
||||
static fromSquare(x: number, y: number, size: number): any {
|
||||
return new Rectangle(x, y, size, size);
|
||||
}
|
||||
static fromTwoPoints(p1: Vector, p2: Vector): any {
|
||||
const left: any = Math.min(p1.x, p2.x);
|
||||
const top: any = Math.min(p1.y, p2.y);
|
||||
const right: any = Math.max(p1.x, p2.x);
|
||||
const bottom: any = Math.max(p1.y, p2.y);
|
||||
return new Rectangle(left, top, right - left, bottom - top);
|
||||
}
|
||||
static centered(width: number, height: number): any {
|
||||
return new Rectangle(-Math.ceil(width / 2), -Math.ceil(height / 2), width, height);
|
||||
}
|
||||
/**
|
||||
* Returns if a intersects b
|
||||
*/
|
||||
static intersects(a: Rectangle, b: Rectangle): any {
|
||||
return a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom;
|
||||
}
|
||||
/**
|
||||
* Copies this instance
|
||||
* {}
|
||||
*/
|
||||
clone(): Rectangle {
|
||||
return new Rectangle(this.x, this.y, this.w, this.h);
|
||||
}
|
||||
/**
|
||||
* Returns if this rectangle is empty
|
||||
* {}
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return epsilonCompare(this.w * this.h, 0);
|
||||
}
|
||||
/**
|
||||
* Returns if this rectangle is equal to the other while taking an epsilon into account
|
||||
*/
|
||||
equalsEpsilon(other: Rectangle, epsilon: number): any {
|
||||
return (epsilonCompare(this.x, other.x, epsilon) &&
|
||||
epsilonCompare(this.y, other.y, epsilon) &&
|
||||
epsilonCompare(this.w, other.w, epsilon) &&
|
||||
epsilonCompare(this.h, other.h, epsilon));
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
left(): number {
|
||||
return this.x;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
right(): number {
|
||||
return this.x + this.w;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
top(): number {
|
||||
return this.y;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
bottom(): number {
|
||||
return this.y + this.h;
|
||||
}
|
||||
/**
|
||||
* Returns Top, Right, Bottom, Left
|
||||
* {}
|
||||
*/
|
||||
trbl(): [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number
|
||||
] {
|
||||
return [this.y, this.right(), this.bottom(), this.x];
|
||||
}
|
||||
/**
|
||||
* Returns the center of the rect
|
||||
* {}
|
||||
*/
|
||||
getCenter(): Vector {
|
||||
return new Vector(this.x + this.w / 2, this.y + this.h / 2);
|
||||
}
|
||||
/**
|
||||
* Sets the right side of the rect without moving it
|
||||
*/
|
||||
setRight(right: number): any {
|
||||
this.w = right - this.x;
|
||||
}
|
||||
/**
|
||||
* Sets the bottom side of the rect without moving it
|
||||
*/
|
||||
setBottom(bottom: number): any {
|
||||
this.h = bottom - this.y;
|
||||
}
|
||||
/**
|
||||
* Sets the top side of the rect without scaling it
|
||||
*/
|
||||
setTop(top: number): any {
|
||||
const bottom: any = this.bottom();
|
||||
this.y = top;
|
||||
this.setBottom(bottom);
|
||||
}
|
||||
/**
|
||||
* Sets the left side of the rect without scaling it
|
||||
*/
|
||||
setLeft(left: number): any {
|
||||
const right: any = this.right();
|
||||
this.x = left;
|
||||
this.setRight(right);
|
||||
}
|
||||
/**
|
||||
* Returns the top left point
|
||||
* {}
|
||||
*/
|
||||
topLeft(): Vector {
|
||||
return new Vector(this.x, this.y);
|
||||
}
|
||||
/**
|
||||
* Returns the bottom left point
|
||||
* {}
|
||||
*/
|
||||
bottomRight(): Vector {
|
||||
return new Vector(this.right(), this.bottom());
|
||||
}
|
||||
/**
|
||||
* Moves the rectangle by the given parameters
|
||||
*/
|
||||
moveBy(x: number, y: number): any {
|
||||
this.x += x;
|
||||
this.y += y;
|
||||
}
|
||||
/**
|
||||
* Moves the rectangle by the given vector
|
||||
*/
|
||||
moveByVector(vec: Vector): any {
|
||||
this.x += vec.x;
|
||||
this.y += vec.y;
|
||||
}
|
||||
/**
|
||||
* Scales every parameter (w, h, x, y) by the given factor. Useful to transform from world to
|
||||
* tile space and vice versa
|
||||
*/
|
||||
allScaled(factor: number): any {
|
||||
return new Rectangle(this.x * factor, this.y * factor, this.w * factor, this.h * factor);
|
||||
}
|
||||
/**
|
||||
* Expands the rectangle in all directions
|
||||
* {} new rectangle
|
||||
*/
|
||||
expandedInAllDirections(amount: number): Rectangle {
|
||||
return new Rectangle(this.x - amount, this.y - amount, this.w + 2 * amount, this.h + 2 * amount);
|
||||
}
|
||||
/**
|
||||
* Returns if the given rectangle is contained
|
||||
* {}
|
||||
*/
|
||||
containsRect(rect: Rectangle): boolean {
|
||||
return (this.x <= rect.right() &&
|
||||
rect.x <= this.right() &&
|
||||
this.y <= rect.bottom() &&
|
||||
rect.y <= this.bottom());
|
||||
}
|
||||
/**
|
||||
* Returns if this rectangle contains the other rectangle specified by the parameters
|
||||
* {}
|
||||
*/
|
||||
containsRect4Params(x: number, y: number, w: number, h: number): boolean {
|
||||
return this.x <= x + w && x <= this.right() && this.y <= y + h && y <= this.bottom();
|
||||
}
|
||||
/**
|
||||
* Returns if the rectangle contains the given circle at (x, y) with the radius (radius)
|
||||
* {}
|
||||
*/
|
||||
containsCircle(x: number, y: number, radius: number): boolean {
|
||||
return (this.x <= x + radius &&
|
||||
x - radius <= this.right() &&
|
||||
this.y <= y + radius &&
|
||||
y - radius <= this.bottom());
|
||||
}
|
||||
/**
|
||||
* Returns if the rectangle contains the given point
|
||||
* {}
|
||||
*/
|
||||
containsPoint(x: number, y: number): boolean {
|
||||
return x >= this.x && x < this.right() && y >= this.y && y < this.bottom();
|
||||
}
|
||||
/**
|
||||
* Returns the shared area with another rectangle, or null if there is no intersection
|
||||
* {}
|
||||
*/
|
||||
getIntersection(rect: Rectangle): Rectangle | null {
|
||||
const left: any = Math.max(this.x, rect.x);
|
||||
const top: any = Math.max(this.y, rect.y);
|
||||
const right: any = Math.min(this.x + this.w, rect.x + rect.w);
|
||||
const bottom: any = Math.min(this.y + this.h, rect.y + rect.h);
|
||||
if (right <= left || bottom <= top) {
|
||||
return null;
|
||||
}
|
||||
return Rectangle.fromTRBL(top, right, bottom, left);
|
||||
}
|
||||
/**
|
||||
* Returns whether the rectangle fully intersects the given rectangle
|
||||
*/
|
||||
intersectsFully(rect: Rectangle): any {
|
||||
const intersection: any = this.getIntersection(rect);
|
||||
return intersection && Math.abs(intersection.w * intersection.h - rect.w * rect.h) < 0.001;
|
||||
}
|
||||
/**
|
||||
* Returns the union of this rectangle with another
|
||||
*/
|
||||
getUnion(rect: Rectangle): any {
|
||||
if (this.isEmpty()) {
|
||||
// If this is rect is empty, return the other one
|
||||
return rect.clone();
|
||||
}
|
||||
if (rect.isEmpty()) {
|
||||
// If the other is empty, return this one
|
||||
return this.clone();
|
||||
}
|
||||
// Find contained area
|
||||
const left: any = Math.min(this.x, rect.x);
|
||||
const top: any = Math.min(this.y, rect.y);
|
||||
const right: any = Math.max(this.right(), rect.right());
|
||||
const bottom: any = Math.max(this.bottom(), rect.bottom());
|
||||
return Rectangle.fromTRBL(top, right, bottom, left);
|
||||
}
|
||||
/**
|
||||
* Good for caching stuff
|
||||
*/
|
||||
toCompareableString(): any {
|
||||
return (round2Digits(this.x) +
|
||||
"/" +
|
||||
round2Digits(this.y) +
|
||||
"/" +
|
||||
round2Digits(this.w) +
|
||||
"/" +
|
||||
round2Digits(this.h));
|
||||
}
|
||||
/**
|
||||
* Good for printing stuff
|
||||
*/
|
||||
toString(): any {
|
||||
return ("[x:" +
|
||||
round2Digits(this.x) +
|
||||
"| y:" +
|
||||
round2Digits(this.y) +
|
||||
"| w:" +
|
||||
round2Digits(this.w) +
|
||||
"| h:" +
|
||||
round2Digits(this.h) +
|
||||
"]");
|
||||
}
|
||||
/**
|
||||
* Returns a new rectangle in tile space which includes all tiles which are visible in this rect
|
||||
* {}
|
||||
*/
|
||||
toTileCullRectangle(): Rectangle {
|
||||
return new Rectangle(Math.floor(this.x / globalConfig.tileSize), Math.floor(this.y / globalConfig.tileSize), Math.ceil(this.w / globalConfig.tileSize), Math.ceil(this.h / globalConfig.tileSize));
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import { createLogger } from "./logging";
|
||||
import { fastArrayDeleteValueIfContained } from "./utils";
|
||||
const logger: any = createLogger("request_channel");
|
||||
// Thrown when a request is aborted
|
||||
export const PROMISE_ABORTED: any = "promise-aborted";
|
||||
export class RequestChannel {
|
||||
public pendingPromises: Array<Promise> = [];
|
||||
|
||||
constructor() {
|
||||
}
|
||||
/**
|
||||
*
|
||||
* {}
|
||||
*/
|
||||
watch(promise: Promise<any>): Promise<any> {
|
||||
// log(this, "Added new promise:", promise, "(pending =", this.pendingPromises.length, ")");
|
||||
let cancelled: any = false;
|
||||
const wrappedPromise: any = new Promise((resolve: any, reject: any): any => {
|
||||
promise.then((result: any): any => {
|
||||
// Remove from pending promises
|
||||
fastArrayDeleteValueIfContained(this.pendingPromises, wrappedPromise);
|
||||
// If not cancelled, resolve promise with same payload
|
||||
if (!cancelled) {
|
||||
resolve.call(this, result);
|
||||
}
|
||||
else {
|
||||
logger.warn("Not resolving because promise got cancelled");
|
||||
// reject.call(this, PROMISE_ABORTED);
|
||||
}
|
||||
}, (err: any): any => {
|
||||
// Remove from pending promises
|
||||
fastArrayDeleteValueIfContained(this.pendingPromises, wrappedPromise);
|
||||
// If not cancelled, reject promise with same payload
|
||||
if (!cancelled) {
|
||||
reject.call(this, err);
|
||||
}
|
||||
else {
|
||||
logger.warn("Not rejecting because promise got cancelled");
|
||||
// reject.call(this, PROMISE_ABORTED);
|
||||
}
|
||||
});
|
||||
});
|
||||
// Add cancel handler
|
||||
// @ts-ignore
|
||||
wrappedPromise.cancel = function (): any {
|
||||
cancelled = true;
|
||||
};
|
||||
this.pendingPromises.push(wrappedPromise);
|
||||
return wrappedPromise;
|
||||
}
|
||||
cancelAll(): any {
|
||||
if (this.pendingPromises.length > 0) {
|
||||
logger.log("Cancel all pending promises (", this.pendingPromises.length, ")");
|
||||
}
|
||||
for (let i: any = 0; i < this.pendingPromises.length; ++i) {
|
||||
// @ts-ignore
|
||||
this.pendingPromises[i].cancel();
|
||||
}
|
||||
this.pendingPromises = [];
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/* typehints:start */
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end */
|
||||
import { ExplainedResult } from "./explained_result";
|
||||
import { ReadWriteProxy } from "./read_write_proxy";
|
||||
import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso";
|
||||
export class RestrictionManager extends ReadWriteProxy {
|
||||
public currentData = this.getDefaultData();
|
||||
|
||||
constructor(app) {
|
||||
super(app, "restriction-flags.bin");
|
||||
}
|
||||
// -- RW Proxy Impl
|
||||
verify(data: any): any {
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
|
||||
getDefaultData(): any {
|
||||
return {
|
||||
version: this.getCurrentVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
getCurrentVersion(): any {
|
||||
return 1;
|
||||
}
|
||||
migrate(data: any): any {
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
initialize(): any {
|
||||
return this.readAsync();
|
||||
}
|
||||
// -- End RW Proxy Impl
|
||||
/**
|
||||
* Returns if the app is currently running as the limited version
|
||||
* {}
|
||||
*/
|
||||
isLimitedVersion(): boolean {
|
||||
if (G_IS_STANDALONE) {
|
||||
// Standalone is never limited
|
||||
return false;
|
||||
}
|
||||
if (WEB_STEAM_SSO_AUTHENTICATED) {
|
||||
return false;
|
||||
}
|
||||
if (G_IS_DEV) {
|
||||
return typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Returns if the app markets the standalone version on steam
|
||||
* {}
|
||||
*/
|
||||
getIsStandaloneMarketingActive(): boolean {
|
||||
return this.isLimitedVersion();
|
||||
}
|
||||
/**
|
||||
* Returns if exporting the base as a screenshot is possible
|
||||
* {}
|
||||
*/
|
||||
getIsExportingScreenshotsPossible(): boolean {
|
||||
return !this.isLimitedVersion();
|
||||
}
|
||||
/**
|
||||
* Returns the maximum number of supported waypoints
|
||||
* {}
|
||||
*/
|
||||
getMaximumWaypoints(): number {
|
||||
return this.isLimitedVersion() ? 2 : 1e20;
|
||||
}
|
||||
/**
|
||||
* Returns if the user has unlimited savegames
|
||||
* {}
|
||||
*/
|
||||
getHasUnlimitedSavegames(): boolean {
|
||||
return !this.isLimitedVersion();
|
||||
}
|
||||
/**
|
||||
* Returns if the app has all settings available
|
||||
* {}
|
||||
*/
|
||||
getHasExtendedSettings(): boolean {
|
||||
return !this.isLimitedVersion();
|
||||
}
|
||||
/**
|
||||
* Returns if all upgrades are available
|
||||
* {}
|
||||
*/
|
||||
getHasExtendedUpgrades(): boolean {
|
||||
return !this.isLimitedVersion();
|
||||
}
|
||||
/**
|
||||
* Returns if all levels & freeplay is available
|
||||
* {}
|
||||
*/
|
||||
getHasExtendedLevelsAndFreeplay(): boolean {
|
||||
return !this.isLimitedVersion();
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
// ALEA RNG
|
||||
function Mash(): any {
|
||||
var n: any = 0xefc8249d;
|
||||
return function (data: any): any {
|
||||
data = data.toString();
|
||||
for (var i: any = 0; i < data.length; i++) {
|
||||
n += data.charCodeAt(i);
|
||||
var h: any = 0.02519603282416938 * n;
|
||||
n = h >>> 0;
|
||||
h -= n;
|
||||
h *= n;
|
||||
n = h >>> 0;
|
||||
h -= n;
|
||||
n += h * 0x100000000; // 2^32
|
||||
}
|
||||
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
|
||||
};
|
||||
}
|
||||
function makeNewRng(seed: number | string): any {
|
||||
// Johannes Baagøe <baagoe@baagoe.com>, 2010
|
||||
var c: any = 1;
|
||||
var mash: any = Mash();
|
||||
let s0: any = mash(" ");
|
||||
let s1: any = mash(" ");
|
||||
let s2: any = mash(" ");
|
||||
s0 -= mash(seed);
|
||||
if (s0 < 0) {
|
||||
s0 += 1;
|
||||
}
|
||||
s1 -= mash(seed);
|
||||
if (s1 < 0) {
|
||||
s1 += 1;
|
||||
}
|
||||
s2 -= mash(seed);
|
||||
if (s2 < 0) {
|
||||
s2 += 1;
|
||||
}
|
||||
mash = null;
|
||||
var random: any = function (): any {
|
||||
var t: any = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
|
||||
s0 = s1;
|
||||
s1 = s2;
|
||||
return (s2 = t - (c = t | 0));
|
||||
};
|
||||
random.exportState = function (): any {
|
||||
return [s0, s1, s2, c];
|
||||
};
|
||||
random.importState = function (i: any): any {
|
||||
s0 = +i[0] || 0;
|
||||
s1 = +i[1] || 0;
|
||||
s2 = +i[2] || 0;
|
||||
c = +i[3] || 0;
|
||||
};
|
||||
return random;
|
||||
}
|
||||
export class RandomNumberGenerator {
|
||||
public internalRng = makeNewRng(seed || Math.random());
|
||||
|
||||
constructor(seed) {
|
||||
}
|
||||
/**
|
||||
* Re-seeds the generator
|
||||
*/
|
||||
reseed(seed: number | string): any {
|
||||
this.internalRng = makeNewRng(seed || Math.random());
|
||||
}
|
||||
/**
|
||||
* {} between 0 and 1
|
||||
*/
|
||||
next(): number {
|
||||
return this.internalRng();
|
||||
}
|
||||
/**
|
||||
* Random choice of an array
|
||||
*/
|
||||
choice(array: array): any {
|
||||
const index: any = this.nextIntRange(0, array.length);
|
||||
return array[index];
|
||||
}
|
||||
/**
|
||||
* {} Integer in range [min, max[
|
||||
*/
|
||||
nextIntRange(min: number, max: number): number {
|
||||
assert(Number.isFinite(min), "Minimum is no integer");
|
||||
assert(Number.isFinite(max), "Maximum is no integer");
|
||||
assert(max > min, "rng: max <= min");
|
||||
return Math.floor(this.next() * (max - min) + min);
|
||||
}
|
||||
/**
|
||||
* {} Number in range [min, max[
|
||||
*/
|
||||
nextRange(min: number, max: number): number {
|
||||
assert(max > min, "rng: max <= min");
|
||||
return this.next() * (max - min) + min;
|
||||
}
|
||||
/**
|
||||
* Updates the seed
|
||||
*/
|
||||
setSeed(seed: number): any {
|
||||
this.internalRng = makeNewRng(seed);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { createHash } from "rusha";
|
||||
import crc32 from "crc/crc32";
|
||||
import { decompressX64 } from "./lzstring";
|
||||
export function sha1(str: any): any {
|
||||
return createHash().update(str).digest("hex");
|
||||
}
|
||||
// Window.location.host
|
||||
export function getNameOfProvider(): any {
|
||||
return window[decompressX64("DYewxghgLgliB2Q")][decompressX64("BYewzgLgdghgtgUyA")];
|
||||
}
|
||||
// Distinguish legacy crc prefixes
|
||||
export const CRC_PREFIX: any = "crc32".padEnd(32, "-");
|
||||
/**
|
||||
* Computes the crc for a given string
|
||||
*/
|
||||
export function computeCrc(str: string): any {
|
||||
return CRC_PREFIX + crc32(str).toString(16).padStart(8, "0");
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
export const STOP_PROPAGATION: any = "stop_propagation";
|
||||
export class Signal {
|
||||
public receivers = [];
|
||||
public modifyCount = 0;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
/**
|
||||
* Adds a new signal listener
|
||||
*/
|
||||
add(receiver: function, scope: object = null): any {
|
||||
assert(receiver, "receiver is null");
|
||||
this.receivers.push({ receiver, scope });
|
||||
++this.modifyCount;
|
||||
}
|
||||
/**
|
||||
* Adds a new signal listener
|
||||
*/
|
||||
addToTop(receiver: function, scope: object = null): any {
|
||||
assert(receiver, "receiver is null");
|
||||
this.receivers.unshift({ receiver, scope });
|
||||
++this.modifyCount;
|
||||
}
|
||||
/**
|
||||
* Dispatches the signal
|
||||
* @param {} payload
|
||||
*/
|
||||
dispatch(): any {
|
||||
const modifyState: any = this.modifyCount;
|
||||
const n: any = this.receivers.length;
|
||||
for (let i: any = 0; i < n; ++i) {
|
||||
const { receiver, scope }: any = this.receivers[i];
|
||||
if (receiver.apply(scope, arguments) === STOP_PROPAGATION) {
|
||||
return STOP_PROPAGATION;
|
||||
}
|
||||
if (modifyState !== this.modifyCount) {
|
||||
// Signal got modified during iteration
|
||||
return STOP_PROPAGATION;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Removes a receiver
|
||||
*/
|
||||
remove(receiver: function): any {
|
||||
let index: any = null;
|
||||
const n: any = this.receivers.length;
|
||||
for (let i: any = 0; i < n; ++i) {
|
||||
if (this.receivers[i].receiver === receiver) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(index !== null, "Receiver not found in list");
|
||||
this.receivers.splice(index, 1);
|
||||
++this.modifyCount;
|
||||
}
|
||||
/**
|
||||
* Removes all receivers
|
||||
*/
|
||||
removeAll(): any {
|
||||
this.receivers = [];
|
||||
++this.modifyCount;
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
import { createLogger } from "./logging";
|
||||
const logger: any = createLogger("singleton_factory");
|
||||
// simple factory pattern
|
||||
export class SingletonFactory {
|
||||
public id = id;
|
||||
public entries = [];
|
||||
public idToEntry = {};
|
||||
|
||||
constructor(id) {
|
||||
}
|
||||
getId(): any {
|
||||
return this.id;
|
||||
}
|
||||
register(classHandle: any): any {
|
||||
// First, construct instance
|
||||
const instance: any = new classHandle();
|
||||
// Extract id
|
||||
const id: any = instance.getId();
|
||||
assert(id, "Factory: Invalid id for class " + classHandle.name + ": " + id);
|
||||
// Check duplicates
|
||||
assert(!this.idToEntry[id], "Duplicate factory entry for " + id);
|
||||
// Insert
|
||||
this.entries.push(instance);
|
||||
this.idToEntry[id] = instance;
|
||||
}
|
||||
/**
|
||||
* Checks if a given id is registered
|
||||
* {}
|
||||
*/
|
||||
hasId(id: string): boolean {
|
||||
return !!this.idToEntry[id];
|
||||
}
|
||||
/**
|
||||
* Finds an instance by a given id
|
||||
* {}
|
||||
*/
|
||||
findById(id: string): object {
|
||||
const entry: any = this.idToEntry[id];
|
||||
if (!entry) {
|
||||
logger.error("Object with id", id, "is not registered!");
|
||||
assert(false, "Factory: Object with id '" + id + "' is not registered!");
|
||||
return null;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
/**
|
||||
|
||||
* Finds an instance by its constructor (The class handle)
|
||||
* {}
|
||||
*/
|
||||
findByClass(classHandle: object): object {
|
||||
for (let i: any = 0; i < this.entries.length; ++i) {
|
||||
if (this.entries[i] instanceof classHandle) {
|
||||
return this.entries[i];
|
||||
}
|
||||
}
|
||||
assert(false, "Factory: Object not found by classHandle (classid: " + classHandle.name + ")");
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Returns all entries
|
||||
* {}
|
||||
*/
|
||||
getEntries(): Array<object> {
|
||||
return this.entries;
|
||||
}
|
||||
/**
|
||||
* Returns all registered ids
|
||||
* {}
|
||||
*/
|
||||
getAllIds(): Array<string> {
|
||||
return Object.keys(this.idToEntry);
|
||||
}
|
||||
/**
|
||||
* Returns amount of stored entries
|
||||
* {}
|
||||
*/
|
||||
getNumEntries(): number {
|
||||
return this.entries.length;
|
||||
}
|
||||
}
|
@ -0,0 +1,273 @@
|
||||
import { DrawParameters } from "./draw_parameters";
|
||||
import { Rectangle } from "./rectangle";
|
||||
import { round3Digits } from "./utils";
|
||||
export const ORIGINAL_SPRITE_SCALE: any = "0.75";
|
||||
export const FULL_CLIP_RECT: any = new Rectangle(0, 0, 1, 1);
|
||||
const EXTRUDE: any = 0.1;
|
||||
export class BaseSprite {
|
||||
/**
|
||||
* Returns the raw handle
|
||||
* {}
|
||||
* @abstract
|
||||
*/
|
||||
getRawTexture(): HTMLImageElement | HTMLCanvasElement {
|
||||
abstract;
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Draws the sprite
|
||||
*/
|
||||
draw(context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number): any {
|
||||
// eslint-disable-line no-unused-vars
|
||||
abstract;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Position of a sprite within an atlas
|
||||
*/
|
||||
export class SpriteAtlasLink {
|
||||
public packedX = packedX;
|
||||
public packedY = packedY;
|
||||
public packedW = packedW;
|
||||
public packedH = packedH;
|
||||
public packOffsetX = packOffsetX;
|
||||
public packOffsetY = packOffsetY;
|
||||
public atlas = atlas;
|
||||
public w = w;
|
||||
public h = h;
|
||||
|
||||
constructor({ w, h, packedX, packedY, packOffsetX, packOffsetY, packedW, packedH, atlas }) {
|
||||
}
|
||||
}
|
||||
export class AtlasSprite extends BaseSprite {
|
||||
public linksByResolution: {
|
||||
[idx: string]: SpriteAtlasLink;
|
||||
} = {};
|
||||
public spriteName = spriteName;
|
||||
public frozen = false;
|
||||
|
||||
constructor(spriteName = "sprite") {
|
||||
super();
|
||||
}
|
||||
getRawTexture(): any {
|
||||
return this.linksByResolution[ORIGINAL_SPRITE_SCALE].atlas;
|
||||
}
|
||||
/**
|
||||
* Draws the sprite onto a regular context using no contexts
|
||||
* @see {BaseSprite.draw}
|
||||
*/
|
||||
draw(context: any, x: any, y: any, w: any, h: any): any {
|
||||
if (G_IS_DEV) {
|
||||
assert(context instanceof CanvasRenderingContext2D, "Not a valid context");
|
||||
}
|
||||
const link: any = this.linksByResolution[ORIGINAL_SPRITE_SCALE];
|
||||
if (!link) {
|
||||
throw new Error("draw: Link for " +
|
||||
this.spriteName +
|
||||
" not known: " +
|
||||
ORIGINAL_SPRITE_SCALE +
|
||||
" (having " +
|
||||
Object.keys(this.linksByResolution) +
|
||||
")");
|
||||
}
|
||||
const width: any = w || link.w;
|
||||
const height: any = h || link.h;
|
||||
const scaleW: any = width / link.w;
|
||||
const scaleH: any = 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);
|
||||
}
|
||||
drawCachedCentered(parameters: DrawParameters, x: number, y: number, size: number, clipping: boolean= = true): any {
|
||||
this.drawCached(parameters, x - size / 2, y - size / 2, size, size, clipping);
|
||||
}
|
||||
drawCentered(context: CanvasRenderingContext2D, x: number, y: number, size: number): any {
|
||||
this.draw(context, x - size / 2, y - size / 2, size, size);
|
||||
}
|
||||
/**
|
||||
* Draws the sprite
|
||||
*/
|
||||
drawCached(parameters: DrawParameters, x: number, y: number, w: number = null, h: number = null, clipping: boolean= = true): any {
|
||||
if (G_IS_DEV) {
|
||||
assert(parameters instanceof DrawParameters, "Not a valid context");
|
||||
assert(!!w && w > 0, "Not a valid width:" + w);
|
||||
assert(!!h && h > 0, "Not a valid height:" + h);
|
||||
}
|
||||
const visibleRect: any = parameters.visibleRect;
|
||||
const scale: any = parameters.desiredAtlasScale;
|
||||
const link: any = this.linksByResolution[scale];
|
||||
if (!link) {
|
||||
throw new Error("drawCached: Link for " +
|
||||
this.spriteName +
|
||||
" at scale " +
|
||||
scale +
|
||||
" not known (having " +
|
||||
Object.keys(this.linksByResolution) +
|
||||
")");
|
||||
}
|
||||
const scaleW: any = w / link.w;
|
||||
const scaleH: any = h / link.h;
|
||||
let destX: any = x + link.packOffsetX * scaleW;
|
||||
let destY: any = y + link.packOffsetY * scaleH;
|
||||
let destW: any = link.packedW * scaleW;
|
||||
let destH: any = link.packedH * scaleH;
|
||||
let srcX: any = link.packedX;
|
||||
let srcY: any = link.packedY;
|
||||
let srcW: any = link.packedW;
|
||||
let srcH: any = link.packedH;
|
||||
let intersection: any = null;
|
||||
if (clipping) {
|
||||
const rect: any = new Rectangle(destX, destY, destW, destH);
|
||||
intersection = rect.getIntersection(visibleRect);
|
||||
if (!intersection) {
|
||||
return;
|
||||
}
|
||||
srcX += (intersection.x - destX) / scaleW;
|
||||
srcY += (intersection.y - destY) / scaleH;
|
||||
srcW *= intersection.w / destW;
|
||||
srcH *= intersection.h / destH;
|
||||
destX = intersection.x;
|
||||
destY = intersection.y;
|
||||
destW = intersection.w;
|
||||
destH = intersection.h;
|
||||
}
|
||||
parameters.context.drawImage(link.atlas,
|
||||
// atlas src pos
|
||||
srcX, srcY,
|
||||
// atlas src size
|
||||
srcW, srcH,
|
||||
// dest pos and size
|
||||
destX - EXTRUDE, destY - EXTRUDE, destW + 2 * EXTRUDE, destH + 2 * EXTRUDE);
|
||||
}
|
||||
/**
|
||||
* 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): any {
|
||||
if (G_IS_DEV) {
|
||||
assert(parameters instanceof DrawParameters, "Not a valid context");
|
||||
assert(!!w && w > 0, "Not a valid width:" + w);
|
||||
assert(!!h && h > 0, "Not a valid height:" + h);
|
||||
assert(clipRect, "No clip rect given!");
|
||||
}
|
||||
const scale: any = parameters.desiredAtlasScale;
|
||||
const link: any = this.linksByResolution[scale];
|
||||
if (!link) {
|
||||
throw new Error("drawCachedWithClipRect: Link for " +
|
||||
this.spriteName +
|
||||
" at scale " +
|
||||
scale +
|
||||
" not known (having " +
|
||||
Object.keys(this.linksByResolution) +
|
||||
")");
|
||||
}
|
||||
const scaleW: any = w / link.w;
|
||||
const scaleH: any = h / link.h;
|
||||
let destX: any = x + link.packOffsetX * scaleW + clipRect.x * w;
|
||||
let destY: any = y + link.packOffsetY * scaleH + clipRect.y * h;
|
||||
let destW: any = link.packedW * scaleW * clipRect.w;
|
||||
let destH: any = link.packedH * scaleH * clipRect.h;
|
||||
let srcX: any = link.packedX + clipRect.x * link.packedW;
|
||||
let srcY: any = link.packedY + clipRect.y * link.packedH;
|
||||
let srcW: any = link.packedW * clipRect.w;
|
||||
let srcH: any = link.packedH * clipRect.h;
|
||||
parameters.context.drawImage(link.atlas,
|
||||
// atlas src pos
|
||||
srcX, srcY,
|
||||
// atlas src siize
|
||||
srcW, srcH,
|
||||
// dest pos and size
|
||||
destX - EXTRUDE, destY - EXTRUDE, destW + 2 * EXTRUDE, destH + 2 * EXTRUDE);
|
||||
}
|
||||
/**
|
||||
* Renders into an html element
|
||||
*/
|
||||
renderToHTMLElement(element: HTMLElement, w: number = 1, h: number = 1): any {
|
||||
element.style.position = "relative";
|
||||
element.innerHTML = this.getAsHTML(w, h);
|
||||
}
|
||||
/**
|
||||
* Returns the html to render as icon
|
||||
*/
|
||||
getAsHTML(w: number, h: number): any {
|
||||
const link: any = this.linksByResolution["0.5"];
|
||||
if (!link) {
|
||||
throw new Error("getAsHTML: Link for " +
|
||||
this.spriteName +
|
||||
" at scale 0.5" +
|
||||
" not known (having " +
|
||||
Object.keys(this.linksByResolution) +
|
||||
")");
|
||||
}
|
||||
// Find out how much we have to scale it so that it fits
|
||||
const scaleX: any = w / link.w;
|
||||
const scaleY: any = h / link.h;
|
||||
// Find out how big the scaled atlas is
|
||||
const atlasW: any = link.atlas.width * scaleX;
|
||||
const atlasH: any = link.atlas.height * scaleY;
|
||||
// @ts-ignore
|
||||
const srcSafe: any = link.atlas.src.replaceAll("\\", "/");
|
||||
// Find out how big we render the sprite
|
||||
const widthAbsolute: any = scaleX * link.packedW;
|
||||
const heightAbsolute: any = scaleY * link.packedH;
|
||||
// Compute the position in the relative container
|
||||
const leftRelative: any = (link.packOffsetX * scaleX) / w;
|
||||
const topRelative: any = (link.packOffsetY * scaleY) / h;
|
||||
const widthRelative: any = widthAbsolute / w;
|
||||
const heightRelative: any = heightAbsolute / h;
|
||||
// Scale the atlas relative to the width and height of the element
|
||||
const bgW: any = atlasW / widthAbsolute;
|
||||
const bgH: any = atlasH / heightAbsolute;
|
||||
// Figure out what the position of the atlas is
|
||||
const bgX: any = link.packedX * scaleX;
|
||||
const bgY: any = link.packedY * scaleY;
|
||||
// Fuck you, whoever thought its a good idea to make background-position work like it does now
|
||||
const bgXRelative: any = -bgX / (widthAbsolute - atlasW);
|
||||
const bgYRelative: any = -bgY / (heightAbsolute - atlasH);
|
||||
return `
|
||||
<span class="spritesheetImage" style="
|
||||
background-image: url('${srcSafe}');
|
||||
left: ${round3Digits(leftRelative * 100.0)}%;
|
||||
top: ${round3Digits(topRelative * 100.0)}%;
|
||||
width: ${round3Digits(widthRelative * 100.0)}%;
|
||||
height: ${round3Digits(heightRelative * 100.0)}%;
|
||||
background-repeat: repeat;
|
||||
background-position: ${round3Digits(bgXRelative * 100.0)}% ${round3Digits(bgYRelative * 100.0)}%;
|
||||
background-size: ${round3Digits(bgW * 100.0)}% ${round3Digits(bgH * 100.0)}%;
|
||||
"></span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
export class RegularSprite extends BaseSprite {
|
||||
public w = w;
|
||||
public h = h;
|
||||
public sprite = sprite;
|
||||
|
||||
constructor(sprite, w, h) {
|
||||
super();
|
||||
}
|
||||
getRawTexture(): any {
|
||||
return this.sprite;
|
||||
}
|
||||
/**
|
||||
* Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing
|
||||
* images into buffers
|
||||
*/
|
||||
draw(context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number): any {
|
||||
assert(context, "No context given");
|
||||
assert(x !== undefined, "No x given");
|
||||
assert(y !== undefined, "No y given");
|
||||
assert(w !== undefined, "No width given");
|
||||
assert(h !== undefined, "No height given");
|
||||
context.drawImage(this.sprite, x, y, w, h);
|
||||
}
|
||||
/**
|
||||
* Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing
|
||||
* images into buffers
|
||||
*/
|
||||
drawCentered(context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number): any {
|
||||
assert(context, "No context given");
|
||||
assert(x !== undefined, "No x given");
|
||||
assert(y !== undefined, "No y given");
|
||||
assert(w !== undefined, "No width given");
|
||||
assert(h !== undefined, "No height given");
|
||||
context.drawImage(this.sprite, x - w / 2, y - h / 2, w, h);
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import { Component } from "../game/component";
|
||||
import { Entity } from "../game/entity";
|
||||
import { globalConfig } from "./config";
|
||||
import { createLogger } from "./logging";
|
||||
import { Rectangle } from "./rectangle";
|
||||
const logger: any = createLogger("stale_areas");
|
||||
export class StaleAreaDetector {
|
||||
public root = root;
|
||||
public name = name;
|
||||
public recomputeMethod = recomputeMethod;
|
||||
public staleArea: Rectangle = null;
|
||||
|
||||
constructor({ root, name, recomputeMethod }) {
|
||||
}
|
||||
/**
|
||||
* Invalidates the given area
|
||||
*/
|
||||
invalidate(area: Rectangle): any {
|
||||
// logger.log(this.name, "invalidated", area.toString());
|
||||
if (this.staleArea) {
|
||||
this.staleArea = this.staleArea.getUnion(area);
|
||||
}
|
||||
else {
|
||||
this.staleArea = area.clone();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Makes this detector recompute the area of an entity whenever
|
||||
* it changes in any way
|
||||
*/
|
||||
recomputeOnComponentsChanged(components: Array<typeof Component>, tilesAround: number): any {
|
||||
const componentIds: any = components.map((component: any): any => component.getId());
|
||||
/**
|
||||
* Internal checker method
|
||||
*/
|
||||
const checker: any = (entity: Entity): any => {
|
||||
if (!this.root.gameInitialized) {
|
||||
return;
|
||||
}
|
||||
// Check for all components
|
||||
for (let i: any = 0; i < componentIds.length; ++i) {
|
||||
if (entity.components[componentIds[i]]) {
|
||||
// Entity is relevant, compute affected area
|
||||
const area: any = entity.components.StaticMapEntity.getTileSpaceBounds().expandedInAllDirections(tilesAround);
|
||||
this.invalidate(area);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
this.root.signals.entityAdded.add(checker);
|
||||
this.root.signals.entityChanged.add(checker);
|
||||
this.root.signals.entityComponentRemoved.add(checker);
|
||||
this.root.signals.entityGotNewComponent.add(checker);
|
||||
this.root.signals.entityDestroyed.add(checker);
|
||||
}
|
||||
/**
|
||||
* Updates the stale area
|
||||
*/
|
||||
update(): any {
|
||||
if (this.staleArea) {
|
||||
if (G_IS_DEV && globalConfig.debug.renderChanges) {
|
||||
logger.log(this.name, "is recomputing", this.staleArea.toString());
|
||||
this.root.hud.parts.changesDebugger.renderChange(this.name, this.staleArea, "#fd145b");
|
||||
}
|
||||
this.recomputeMethod(this.staleArea);
|
||||
this.staleArea = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
/* typehints:start*/
|
||||
import type { Application } from "../application";
|
||||
/* typehints:end*/
|
||||
import { GameState } from "./game_state";
|
||||
import { createLogger } from "./logging";
|
||||
import { waitNextFrame, removeAllChildren } from "./utils";
|
||||
import { MOD_SIGNALS } from "../mods/mod_signals";
|
||||
const logger: any = createLogger("state_manager");
|
||||
/**
|
||||
* This is the main state machine which drives the game states.
|
||||
*/
|
||||
export class StateManager {
|
||||
public app = app;
|
||||
public currentState: GameState = null;
|
||||
public stateClasses: {
|
||||
[idx: string]: new () => GameState;
|
||||
} = {};
|
||||
|
||||
constructor(app) {
|
||||
}
|
||||
/**
|
||||
* Registers a new state class, should be a GameState derived class
|
||||
*/
|
||||
register(stateClass: object): any {
|
||||
// Create a dummy to retrieve the key
|
||||
const dummy: any = new stateClass();
|
||||
assert(dummy instanceof GameState, "Not a state!");
|
||||
const key: any = dummy.getKey();
|
||||
assert(!this.stateClasses[key], `State '${key}' is already registered!`);
|
||||
this.stateClasses[key] = stateClass;
|
||||
}
|
||||
/**
|
||||
* Constructs a new state or returns the instance from the cache
|
||||
*/
|
||||
constructState(key: string): any {
|
||||
if (this.stateClasses[key]) {
|
||||
return new this.stateClasses[key]();
|
||||
}
|
||||
assert(false, `State '${key}' is not known!`);
|
||||
}
|
||||
/**
|
||||
* Moves to a given state
|
||||
*/
|
||||
moveToState(key: string, payload: any = {}): any {
|
||||
if (window.APP_ERROR_OCCURED) {
|
||||
console.warn("Skipping state transition because of application crash");
|
||||
return;
|
||||
}
|
||||
if (this.currentState) {
|
||||
if (key === this.currentState.getKey()) {
|
||||
logger.error(`State '${key}' is already active!`);
|
||||
return false;
|
||||
}
|
||||
this.currentState.internalLeaveCallback();
|
||||
// Remove all references
|
||||
for (const stateKey: any in this.currentState) {
|
||||
if (this.currentState.hasOwnProperty(stateKey)) {
|
||||
delete this.currentState[stateKey];
|
||||
}
|
||||
}
|
||||
this.currentState = null;
|
||||
}
|
||||
this.currentState = this.constructState(key);
|
||||
this.currentState.internalRegisterCallback(this, this.app);
|
||||
// Clean up old elements
|
||||
if (this.currentState.getRemovePreviousContent()) {
|
||||
removeAllChildren(document.body);
|
||||
}
|
||||
document.body.className = "gameState " + (this.currentState.getHasFadeIn() ? "" : "arrived");
|
||||
document.body.id = "state_" + key;
|
||||
if (this.currentState.getRemovePreviousContent()) {
|
||||
document.body.innerHTML = this.currentState.internalGetFullHtml();
|
||||
}
|
||||
const dialogParent: any = document.createElement("div");
|
||||
dialogParent.classList.add("modalDialogParent");
|
||||
document.body.appendChild(dialogParent);
|
||||
try {
|
||||
this.currentState.internalEnterCallback(payload);
|
||||
}
|
||||
catch (ex: any) {
|
||||
console.error(ex);
|
||||
throw ex;
|
||||
}
|
||||
this.app.sound.playThemeMusic(this.currentState.getThemeMusic());
|
||||
this.currentState.onResized(this.app.screenWidth, this.app.screenHeight);
|
||||
this.app.analytics.trackStateEnter(key);
|
||||
window.history.pushState({
|
||||
key,
|
||||
}, key);
|
||||
MOD_SIGNALS.stateEntered.dispatch(this.currentState);
|
||||
waitNextFrame().then((): any => {
|
||||
document.body.classList.add("arrived");
|
||||
});
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Returns the current state
|
||||
* {}
|
||||
*/
|
||||
getCurrentState(): GameState {
|
||||
return this.currentState;
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import { T } from "../translations";
|
||||
import { openStandaloneLink } from "./config";
|
||||
export let WEB_STEAM_SSO_AUTHENTICATED: any = false;
|
||||
export async function authorizeViaSSOToken(app: any, dialogs: any): any {
|
||||
if (G_IS_STANDALONE) {
|
||||
return;
|
||||
}
|
||||
if (window.location.search.includes("sso_logout_silent")) {
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
window.location.replace("/");
|
||||
return new Promise((): any => null);
|
||||
}
|
||||
if (window.location.search.includes("sso_logout")) {
|
||||
const { ok }: any = dialogs.showWarning(T.dialogs.steamSsoError.title, T.dialogs.steamSsoError.desc);
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
ok.add((): any => window.location.replace("/"));
|
||||
return new Promise((): any => null);
|
||||
}
|
||||
if (window.location.search.includes("steam_sso_no_ownership")) {
|
||||
const { ok, getStandalone }: any = dialogs.showWarning(T.dialogs.steamSsoNoOwnership.title, T.dialogs.steamSsoNoOwnership.desc, ["ok", "getStandalone:good"]);
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
getStandalone.add((): any => {
|
||||
openStandaloneLink(app, "sso_ownership");
|
||||
window.location.replace("/");
|
||||
});
|
||||
ok.add((): any => window.location.replace("/"));
|
||||
return new Promise((): any => null);
|
||||
}
|
||||
const token: any = window.localStorage.getItem("steam_sso_auth_token");
|
||||
if (!token) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const apiUrl: any = app.clientApi.getEndpoint();
|
||||
console.warn("Authorizing via token:", token);
|
||||
const verify: any = async (): any => {
|
||||
const token: any = window.localStorage.getItem("steam_sso_auth_token");
|
||||
if (!token) {
|
||||
window.location.replace("?sso_logout");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response: any = await Promise.race([
|
||||
fetch(apiUrl + "/v1/sso/refresh", {
|
||||
method: "POST",
|
||||
body: token,
|
||||
headers: {
|
||||
"x-api-key": "d5c54aaa491f200709afff082c153ef2",
|
||||
},
|
||||
}),
|
||||
new Promise((resolve: any, reject: any): any => {
|
||||
setTimeout((): any => reject("timeout exceeded"), 20000);
|
||||
}),
|
||||
]);
|
||||
const responseText: any = await response.json();
|
||||
if (!responseText.token) {
|
||||
console.warn("Failed to register");
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
window.location.replace("?sso_logout");
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem("steam_sso_auth_token", responseText.token);
|
||||
app.clientApi.token = responseText.token;
|
||||
WEB_STEAM_SSO_AUTHENTICATED = true;
|
||||
}
|
||||
catch (ex: any) {
|
||||
console.warn("Auth failure", ex);
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
window.location.replace("/");
|
||||
return new Promise((): any => null);
|
||||
}
|
||||
};
|
||||
await verify();
|
||||
setInterval(verify, 120000);
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
||||
import { GameState } from "./game_state";
|
||||
import { T } from "../translations";
|
||||
/**
|
||||
* Baseclass for all game states which are structured similary: A header with back button + some
|
||||
* scrollable content.
|
||||
*/
|
||||
export class TextualGameState extends GameState {
|
||||
///// INTERFACE ////
|
||||
/**
|
||||
* Should return the states inner html. If not overriden, will create a scrollable container
|
||||
* with the content of getMainContentHTML()
|
||||
* {}
|
||||
*/
|
||||
getInnerHTML(): string {
|
||||
return `
|
||||
<div class="content mainContent">
|
||||
${this.getMainContentHTML()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
/**
|
||||
* Should return the states HTML content.
|
||||
*/
|
||||
getMainContentHTML(): any {
|
||||
return "";
|
||||
}
|
||||
/**
|
||||
* Should return the title of the game state. If null, no title and back button will
|
||||
* get created
|
||||
* {}
|
||||
*/
|
||||
getStateHeaderTitle(): string | null {
|
||||
return null;
|
||||
}
|
||||
/////////////
|
||||
/**
|
||||
* Back button handler, can be overridden. Per default it goes back to the main menu,
|
||||
* or if coming from the game it moves back to the game again.
|
||||
*/
|
||||
onBackButton(): any {
|
||||
if (this.backToStateId) {
|
||||
this.moveToState(this.backToStateId, this.backToStatePayload);
|
||||
}
|
||||
else {
|
||||
this.moveToState(this.getDefaultPreviousState());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the default state to go back to
|
||||
*/
|
||||
getDefaultPreviousState(): any {
|
||||
return "MainMenuState";
|
||||
}
|
||||
/**
|
||||
* Goes to a new state, telling him to go back to this state later
|
||||
*/
|
||||
moveToStateAddGoBack(stateId: string): any {
|
||||
this.moveToState(stateId, {
|
||||
backToStateId: this.key,
|
||||
backToStatePayload: {
|
||||
backToStateId: this.backToStateId,
|
||||
backToStatePayload: this.backToStatePayload,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Removes all click detectors, except the one on the back button. Useful when regenerating
|
||||
* content.
|
||||
*/
|
||||
clearClickDetectorsExceptHeader(): any {
|
||||
for (let i: any = 0; i < this.clickDetectors.length; ++i) {
|
||||
const detector: any = this.clickDetectors[i];
|
||||
if (detector.element === this.headerElement) {
|
||||
continue;
|
||||
}
|
||||
detector.cleanup();
|
||||
this.clickDetectors.splice(i, 1);
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Overrides the GameState implementation to provide our own html
|
||||
*/
|
||||
internalGetFullHtml(): any {
|
||||
let headerHtml: any = "";
|
||||
if (this.getStateHeaderTitle()) {
|
||||
headerHtml = `
|
||||
<div class="headerBar">
|
||||
|
||||
<h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1>
|
||||
</div>`;
|
||||
}
|
||||
return `
|
||||
${headerHtml}
|
||||
<div class="container">
|
||||
${this.getInnerHTML()}
|
||||
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
//// INTERNALS /////
|
||||
/**
|
||||
* Overrides the GameState leave callback to cleanup stuff
|
||||
*/
|
||||
internalLeaveCallback(): any {
|
||||
super.internalLeaveCallback();
|
||||
this.dialogs.cleanup();
|
||||
}
|
||||
/**
|
||||
* Overrides the GameState enter callback to setup required stuff
|
||||
*/
|
||||
internalEnterCallback(payload: any): any {
|
||||
super.internalEnterCallback(payload, false);
|
||||
if (payload.backToStateId) {
|
||||
this.backToStateId = payload.backToStateId;
|
||||
this.backToStatePayload = payload.backToStatePayload;
|
||||
}
|
||||
this.htmlElement.classList.add("textualState");
|
||||
if (this.getStateHeaderTitle()) {
|
||||
this.htmlElement.classList.add("hasTitle");
|
||||
}
|
||||
this.containerElement = this.htmlElement.querySelector(".widthKeeper .container");
|
||||
this.headerElement = this.htmlElement.querySelector(".headerBar > h1");
|
||||
if (this.headerElement) {
|
||||
this.trackClicks(this.headerElement, this.onBackButton);
|
||||
}
|
||||
this.dialogs = new HUDModalDialogs(null, this.app);
|
||||
const dialogsElement: any = document.body.querySelector(".modalDialogParent");
|
||||
this.dialogs.initializeToElement(dialogsElement);
|
||||
this.onEnter(payload);
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
export class TrackedState {
|
||||
public lastSeenValue = null;
|
||||
|
||||
constructor(callbackMethod = null, callbackScope = null) {
|
||||
if (callbackMethod) {
|
||||
this.callback = callbackMethod;
|
||||
if (callbackScope) {
|
||||
this.callback = this.callback.bind(callbackScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
set(value: any, changeHandler: any = null, changeScope: any = null): any {
|
||||
if (value !== this.lastSeenValue) {
|
||||
// Copy value since the changeHandler call could actually modify our lastSeenValue
|
||||
const valueCopy: any = value;
|
||||
this.lastSeenValue = value;
|
||||
if (changeHandler) {
|
||||
if (changeScope) {
|
||||
changeHandler.call(changeScope, valueCopy);
|
||||
}
|
||||
else {
|
||||
changeHandler(valueCopy);
|
||||
}
|
||||
}
|
||||
else if (this.callback) {
|
||||
this.callback(value);
|
||||
}
|
||||
else {
|
||||
assert(false, "No callback specified");
|
||||
}
|
||||
}
|
||||
}
|
||||
setSilent(value: any): any {
|
||||
this.lastSeenValue = value;
|
||||
}
|
||||
get(): any {
|
||||
return this.lastSeenValue;
|
||||
}
|
||||
}
|
@ -0,0 +1,628 @@
|
||||
import { T } from "../translations";
|
||||
import { rando } from "@nastyox/rando.js";
|
||||
import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso";
|
||||
const bigNumberSuffixTranslationKeys: any = ["thousands", "millions", "billions", "trillions"];
|
||||
/**
|
||||
* Returns a platform name
|
||||
* {}
|
||||
*/
|
||||
export function getPlatformName(): "android" | "browser" | "ios" | "standalone" | "unknown" {
|
||||
if (G_IS_STANDALONE) {
|
||||
return "standalone";
|
||||
}
|
||||
else if (G_IS_BROWSER) {
|
||||
return "browser";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
/**
|
||||
* Makes a new 2D array with undefined contents
|
||||
* {}
|
||||
*/
|
||||
export function make2DUndefinedArray(w: number, h: number): Array<Array<any>> {
|
||||
const result: any = new Array(w);
|
||||
for (let x: any = 0; x < w; ++x) {
|
||||
result[x] = new Array(h);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Creates a new map (an empty object without any props)
|
||||
*/
|
||||
export function newEmptyMap(): any {
|
||||
return Object.create(null);
|
||||
}
|
||||
/**
|
||||
* Returns a random integer in the range [start,end]
|
||||
*/
|
||||
export function randomInt(start: number, end: number): any {
|
||||
return rando(start, end);
|
||||
}
|
||||
/**
|
||||
* Access an object in a very annoying way, used for obsfuscation.
|
||||
*/
|
||||
export function accessNestedPropertyReverse(obj: any, keys: Array<string>): any {
|
||||
let result: any = obj;
|
||||
for (let i: any = keys.length - 1; i >= 0; --i) {
|
||||
result = result[keys[i]];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Chooses a random entry of an array
|
||||
* @template T
|
||||
* {}
|
||||
*/
|
||||
export function randomChoice(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
/**
|
||||
* Deletes from an array by swapping with the last element
|
||||
*/
|
||||
export function fastArrayDelete(array: Array<any>, index: number): any {
|
||||
if (index < 0 || index >= array.length) {
|
||||
throw new Error("Out of bounds");
|
||||
}
|
||||
// When the element is not the last element
|
||||
if (index !== array.length - 1) {
|
||||
// Get the last element, and swap it with the one we want to delete
|
||||
const last: any = array[array.length - 1];
|
||||
array[index] = last;
|
||||
}
|
||||
// Finally remove the last element
|
||||
array.length -= 1;
|
||||
}
|
||||
/**
|
||||
* Deletes from an array by swapping with the last element. Searches
|
||||
* for the value in the array first
|
||||
*/
|
||||
export function fastArrayDeleteValue(array: Array<any>, value: any): any {
|
||||
if (array == null) {
|
||||
throw new Error("Tried to delete from non array!");
|
||||
}
|
||||
const index: any = array.indexOf(value);
|
||||
if (index < 0) {
|
||||
console.error("Value", value, "not contained in array:", array, "!");
|
||||
return value;
|
||||
}
|
||||
return fastArrayDelete(array, index);
|
||||
}
|
||||
/**
|
||||
* @see fastArrayDeleteValue
|
||||
*/
|
||||
export function fastArrayDeleteValueIfContained(array: Array<any>, value: any): any {
|
||||
if (array == null) {
|
||||
throw new Error("Tried to delete from non array!");
|
||||
}
|
||||
const index: any = array.indexOf(value);
|
||||
if (index < 0) {
|
||||
return value;
|
||||
}
|
||||
return fastArrayDelete(array, index);
|
||||
}
|
||||
/**
|
||||
* Deletes from an array at the given index
|
||||
*/
|
||||
export function arrayDelete(array: Array<any>, index: number): any {
|
||||
if (index < 0 || index >= array.length) {
|
||||
throw new Error("Out of bounds");
|
||||
}
|
||||
array.splice(index, 1);
|
||||
}
|
||||
/**
|
||||
* Deletes the given value from an array
|
||||
*/
|
||||
export function arrayDeleteValue(array: Array<any>, value: any): any {
|
||||
if (array == null) {
|
||||
throw new Error("Tried to delete from non array!");
|
||||
}
|
||||
const index: any = array.indexOf(value);
|
||||
if (index < 0) {
|
||||
console.error("Value", value, "not contained in array:", array, "!");
|
||||
return value;
|
||||
}
|
||||
return arrayDelete(array, index);
|
||||
}
|
||||
/**
|
||||
* Compare two floats for epsilon equality
|
||||
* {}
|
||||
*/
|
||||
export function epsilonCompare(a: number, b: number, epsilon: any = 1e-5): boolean {
|
||||
return Math.abs(a - b) < epsilon;
|
||||
}
|
||||
/**
|
||||
* Interpolates two numbers
|
||||
*/
|
||||
export function lerp(a: number, b: number, x: number): any {
|
||||
return a * (1 - x) + b * x;
|
||||
}
|
||||
/**
|
||||
* Finds a value which is nice to display, e.g. 15669 -> 15000. Also handles fractional stuff
|
||||
*/
|
||||
export function findNiceValue(num: number): any {
|
||||
if (num > 1e8) {
|
||||
return num;
|
||||
}
|
||||
if (num < 0.00001) {
|
||||
return 0;
|
||||
}
|
||||
let roundAmount: any = 1;
|
||||
if (num > 50000) {
|
||||
roundAmount = 10000;
|
||||
}
|
||||
else if (num > 20000) {
|
||||
roundAmount = 5000;
|
||||
}
|
||||
else if (num > 5000) {
|
||||
roundAmount = 1000;
|
||||
}
|
||||
else if (num > 2000) {
|
||||
roundAmount = 500;
|
||||
}
|
||||
else if (num > 1000) {
|
||||
roundAmount = 100;
|
||||
}
|
||||
else if (num > 100) {
|
||||
roundAmount = 20;
|
||||
}
|
||||
else if (num > 20) {
|
||||
roundAmount = 5;
|
||||
}
|
||||
const niceValue: any = Math.floor(num / roundAmount) * roundAmount;
|
||||
if (num >= 10) {
|
||||
return Math.round(niceValue);
|
||||
}
|
||||
if (num >= 1) {
|
||||
return Math.round(niceValue * 10) / 10;
|
||||
}
|
||||
return Math.round(niceValue * 100) / 100;
|
||||
}
|
||||
/**
|
||||
* Finds a nice integer value
|
||||
* @see findNiceValue
|
||||
*/
|
||||
export function findNiceIntegerValue(num: number): any {
|
||||
return Math.ceil(findNiceValue(num));
|
||||
}
|
||||
/**
|
||||
* Formats a big number
|
||||
* {}
|
||||
*/
|
||||
export function formatBigNumber(num: number, separator: string= = T.global.decimalSeparator): string {
|
||||
const sign: any = num < 0 ? "-" : "";
|
||||
num = Math.abs(num);
|
||||
if (num > 1e54) {
|
||||
return sign + T.global.infinite;
|
||||
}
|
||||
if (num < 10 && !Number.isInteger(num)) {
|
||||
return sign + num.toFixed(2);
|
||||
}
|
||||
if (num < 50 && !Number.isInteger(num)) {
|
||||
return sign + num.toFixed(1);
|
||||
}
|
||||
num = Math.floor(num);
|
||||
if (num < 1000) {
|
||||
return sign + "" + num;
|
||||
}
|
||||
else {
|
||||
let leadingDigits: any = num;
|
||||
let suffix: any = "";
|
||||
for (let suffixIndex: any = 0; suffixIndex < bigNumberSuffixTranslationKeys.length; ++suffixIndex) {
|
||||
leadingDigits = leadingDigits / 1000;
|
||||
suffix = T.global.suffix[bigNumberSuffixTranslationKeys[suffixIndex]];
|
||||
if (leadingDigits < 1000) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const leadingDigitsRounded: any = round1Digit(leadingDigits);
|
||||
const leadingDigitsNoTrailingDecimal: any = leadingDigitsRounded
|
||||
.toString()
|
||||
.replace(".0", "")
|
||||
.replace(".", separator);
|
||||
return sign + leadingDigitsNoTrailingDecimal + suffix;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Formats a big number, but does not add any suffix and instead uses its full representation
|
||||
* {}
|
||||
*/
|
||||
export function formatBigNumberFull(num: number, divider: string= = T.global.thousandsDivider): string {
|
||||
if (num < 1000) {
|
||||
return num + "";
|
||||
}
|
||||
if (num > 1e54) {
|
||||
return T.global.infinite;
|
||||
}
|
||||
let rest: any = num;
|
||||
let out: any = "";
|
||||
while (rest >= 1000) {
|
||||
out = (rest % 1000).toString().padStart(3, "0") + divider + out;
|
||||
rest = Math.floor(rest / 1000);
|
||||
}
|
||||
out = rest + divider + out;
|
||||
return out.substring(0, out.length - 1);
|
||||
}
|
||||
/**
|
||||
* Waits two frames so the ui is updated
|
||||
* {}
|
||||
*/
|
||||
export function waitNextFrame(): Promise<void> {
|
||||
return new Promise(function (resolve: any): any {
|
||||
window.requestAnimationFrame(function (): any {
|
||||
window.requestAnimationFrame(function (): any {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Rounds 1 digit
|
||||
* {}
|
||||
*/
|
||||
export function round1Digit(n: number): number {
|
||||
return Math.floor(n * 10.0) / 10.0;
|
||||
}
|
||||
/**
|
||||
* Rounds 2 digits
|
||||
* {}
|
||||
*/
|
||||
export function round2Digits(n: number): number {
|
||||
return Math.floor(n * 100.0) / 100.0;
|
||||
}
|
||||
/**
|
||||
* Rounds 3 digits
|
||||
* {}
|
||||
*/
|
||||
export function round3Digits(n: number): number {
|
||||
return Math.floor(n * 1000.0) / 1000.0;
|
||||
}
|
||||
/**
|
||||
* Rounds 4 digits
|
||||
* {}
|
||||
*/
|
||||
export function round4Digits(n: number): number {
|
||||
return Math.floor(n * 10000.0) / 10000.0;
|
||||
}
|
||||
/**
|
||||
* Clamps a value between [min, max]
|
||||
*/
|
||||
export function clamp(v: number, minimum: number= = 0, maximum: number= = 1): any {
|
||||
return Math.max(minimum, Math.min(maximum, v));
|
||||
}
|
||||
/**
|
||||
* Helper method to create a new div element
|
||||
*/
|
||||
export function makeDivElement(id: string= = null, classes: Array<string>= = [], innerHTML: string= = ""): any {
|
||||
const div: any = document.createElement("div");
|
||||
if (id) {
|
||||
div.id = id;
|
||||
}
|
||||
for (let i: any = 0; i < classes.length; ++i) {
|
||||
div.classList.add(classes[i]);
|
||||
}
|
||||
div.innerHTML = innerHTML;
|
||||
return div;
|
||||
}
|
||||
/**
|
||||
* Helper method to create a new div
|
||||
*/
|
||||
export function makeDiv(parent: Element, id: string= = null, classes: Array<string>= = [], innerHTML: string= = ""): any {
|
||||
const div: any = makeDivElement(id, classes, innerHTML);
|
||||
parent.appendChild(div);
|
||||
return div;
|
||||
}
|
||||
/**
|
||||
* Helper method to create a new button element
|
||||
*/
|
||||
export function makeButtonElement(classes: Array<string>= = [], innerHTML: string= = ""): any {
|
||||
const element: any = document.createElement("button");
|
||||
for (let i: any = 0; i < classes.length; ++i) {
|
||||
element.classList.add(classes[i]);
|
||||
}
|
||||
element.classList.add("styledButton");
|
||||
element.innerHTML = innerHTML;
|
||||
return element;
|
||||
}
|
||||
/**
|
||||
* Helper method to create a new button
|
||||
*/
|
||||
export function makeButton(parent: Element, classes: Array<string>= = [], innerHTML: string= = ""): any {
|
||||
const element: any = makeButtonElement(classes, innerHTML);
|
||||
parent.appendChild(element);
|
||||
return element;
|
||||
}
|
||||
/**
|
||||
* Removes all children of the given element
|
||||
*/
|
||||
export function removeAllChildren(elem: Element): any {
|
||||
if (elem) {
|
||||
var range: any = document.createRange();
|
||||
range.selectNodeContents(elem);
|
||||
range.deleteContents();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns if the game supports this browser
|
||||
*/
|
||||
export function isSupportedBrowser(): any {
|
||||
// please note,
|
||||
// that IE11 now returns undefined again for window.chrome
|
||||
// and new Opera 30 outputs true for window.chrome
|
||||
// but needs to check if window.opr is not undefined
|
||||
// and new IE Edge outputs to true now for window.chrome
|
||||
// and if not iOS Chrome check
|
||||
// so use the below updated condition
|
||||
if (G_IS_STANDALONE) {
|
||||
return true;
|
||||
}
|
||||
// @ts-ignore
|
||||
var isChromium: any = window.chrome;
|
||||
var winNav: any = window.navigator;
|
||||
var vendorName: any = winNav.vendor;
|
||||
// @ts-ignore
|
||||
var isIEedge: any = winNav.userAgent.indexOf("Edge") > -1;
|
||||
var isIOSChrome: any = winNav.userAgent.match("CriOS");
|
||||
if (isIOSChrome) {
|
||||
// is Google Chrome on IOS
|
||||
return false;
|
||||
}
|
||||
else if (isChromium !== null &&
|
||||
typeof isChromium !== "undefined" &&
|
||||
vendorName === "Google Inc." &&
|
||||
isIEedge === false) {
|
||||
// is Google Chrome
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
// not Google Chrome
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Formats an amount of seconds into something like "5s ago"
|
||||
* {}
|
||||
*/
|
||||
export function formatSecondsToTimeAgo(secs: number): string {
|
||||
const seconds: any = Math.floor(secs);
|
||||
const minutes: any = Math.floor(seconds / 60);
|
||||
const hours: any = Math.floor(minutes / 60);
|
||||
const days: any = Math.floor(hours / 24);
|
||||
if (seconds < 60) {
|
||||
if (seconds === 1) {
|
||||
return T.global.time.oneSecondAgo;
|
||||
}
|
||||
return T.global.time.xSecondsAgo.replace("<x>", "" + seconds);
|
||||
}
|
||||
else if (minutes < 60) {
|
||||
if (minutes === 1) {
|
||||
return T.global.time.oneMinuteAgo;
|
||||
}
|
||||
return T.global.time.xMinutesAgo.replace("<x>", "" + minutes);
|
||||
}
|
||||
else if (hours < 24) {
|
||||
if (hours === 1) {
|
||||
return T.global.time.oneHourAgo;
|
||||
}
|
||||
return T.global.time.xHoursAgo.replace("<x>", "" + hours);
|
||||
}
|
||||
else {
|
||||
if (days === 1) {
|
||||
return T.global.time.oneDayAgo;
|
||||
}
|
||||
return T.global.time.xDaysAgo.replace("<x>", "" + days);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Formats seconds into a readable string like "5h 23m"
|
||||
* {}
|
||||
*/
|
||||
export function formatSeconds(secs: number): string {
|
||||
const trans: any = T.global.time;
|
||||
secs = Math.ceil(secs);
|
||||
if (secs < 60) {
|
||||
return trans.secondsShort.replace("<seconds>", "" + secs);
|
||||
}
|
||||
else if (secs < 60 * 60) {
|
||||
const minutes: any = Math.floor(secs / 60);
|
||||
const seconds: any = secs % 60;
|
||||
return trans.minutesAndSecondsShort
|
||||
.replace("<seconds>", "" + seconds)
|
||||
.replace("<minutes>", "" + minutes);
|
||||
}
|
||||
else {
|
||||
const hours: any = Math.floor(secs / 3600);
|
||||
const minutes: any = Math.floor(secs / 60) % 60;
|
||||
return trans.hoursAndMinutesShort.replace("<minutes>", "" + minutes).replace("<hours>", "" + hours);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Formats a number like 2.51 to "2.5"
|
||||
*/
|
||||
export function round1DigitLocalized(speed: number, separator: string= = T.global.decimalSeparator): any {
|
||||
return round1Digit(speed).toString().replace(".", separator);
|
||||
}
|
||||
/**
|
||||
* Formats a number like 2.51 to "2.51 items / s"
|
||||
*/
|
||||
export function formatItemsPerSecond(speed: number, double: boolean= = false, separator: string= = T.global.decimalSeparator): any {
|
||||
return ((speed === 1.0
|
||||
? T.ingame.buildingPlacement.infoTexts.oneItemPerSecond
|
||||
: T.ingame.buildingPlacement.infoTexts.itemsPerSecond.replace("<x>", round2Digits(speed).toString().replace(".", separator))) + (double ? " " + T.ingame.buildingPlacement.infoTexts.itemsPerSecondDouble : ""));
|
||||
}
|
||||
/**
|
||||
* Rotates a flat 3x3 matrix clockwise
|
||||
* Entries:
|
||||
* 0 lo
|
||||
* 1 mo
|
||||
* 2 ro
|
||||
* 3 lm
|
||||
* 4 mm
|
||||
* 5 rm
|
||||
* 6 lu
|
||||
* 7 mu
|
||||
* 8 ru
|
||||
*/
|
||||
export function rotateFlatMatrix3x3(flatMatrix: Array<number>): any {
|
||||
return [
|
||||
flatMatrix[6],
|
||||
flatMatrix[3],
|
||||
flatMatrix[0],
|
||||
flatMatrix[7],
|
||||
flatMatrix[4],
|
||||
flatMatrix[1],
|
||||
flatMatrix[8],
|
||||
flatMatrix[5],
|
||||
flatMatrix[2],
|
||||
];
|
||||
}
|
||||
/**
|
||||
* Generates rotated variants of the matrix
|
||||
* {}
|
||||
*/
|
||||
export function generateMatrixRotations(originalMatrix: Array<number>): Object<number, Array<number>> {
|
||||
const result: any = {
|
||||
0: originalMatrix,
|
||||
};
|
||||
originalMatrix = rotateFlatMatrix3x3(originalMatrix);
|
||||
result[90] = originalMatrix;
|
||||
originalMatrix = rotateFlatMatrix3x3(originalMatrix);
|
||||
result[180] = originalMatrix;
|
||||
originalMatrix = rotateFlatMatrix3x3(originalMatrix);
|
||||
result[270] = originalMatrix;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates a directional object
|
||||
* {}
|
||||
*/
|
||||
export function rotateDirectionalObject(obj: DirectionalObject, rotation: any): DirectionalObject {
|
||||
const queue: any = [obj.top, obj.right, obj.bottom, obj.left];
|
||||
while (rotation !== 0) {
|
||||
rotation -= 90;
|
||||
queue.push(queue.shift());
|
||||
}
|
||||
return {
|
||||
top: queue[0],
|
||||
right: queue[1],
|
||||
bottom: queue[2],
|
||||
left: queue[3],
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Modulo which works for negative numbers
|
||||
*/
|
||||
export function safeModulo(n: number, m: number): any {
|
||||
return ((n % m) + m) % m;
|
||||
}
|
||||
/**
|
||||
* Returns a smooth pulse between 0 and 1
|
||||
* {}
|
||||
*/
|
||||
export function smoothPulse(time: number): number {
|
||||
return Math.sin(time * 4) * 0.5 + 0.5;
|
||||
}
|
||||
/**
|
||||
* Fills in a <link> tag
|
||||
*/
|
||||
export function fillInLinkIntoTranslation(translation: string, link: string): any {
|
||||
return translation
|
||||
.replace("<link>", "<a href='" + link + "' target='_blank'>")
|
||||
.replace("</link>", "</a>");
|
||||
}
|
||||
/**
|
||||
* Generates a file download
|
||||
*/
|
||||
export function generateFileDownload(filename: string, text: string): any {
|
||||
var element: any = document.createElement("a");
|
||||
element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
|
||||
element.setAttribute("download", filename);
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
/**
|
||||
* Starts a file chooser
|
||||
*/
|
||||
export function startFileChoose(acceptedType: string = ".bin"): any {
|
||||
var input: any = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = acceptedType;
|
||||
return new Promise((resolve: any): any => {
|
||||
input.onchange = (_: any): any => resolve(input.files[0]);
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
const MAX_ROMAN_NUMBER: any = 49;
|
||||
const romanLiteralsCache: any = ["0"];
|
||||
/**
|
||||
*
|
||||
* {}
|
||||
*/
|
||||
export function getRomanNumber(number: number): string {
|
||||
number = Math.max(0, Math.round(number));
|
||||
if (romanLiteralsCache[number]) {
|
||||
return romanLiteralsCache[number];
|
||||
}
|
||||
if (number > MAX_ROMAN_NUMBER) {
|
||||
return String(number);
|
||||
}
|
||||
function formatDigit(digit: any, unit: any, quintuple: any, decuple: any): any {
|
||||
switch (digit) {
|
||||
case 0:
|
||||
return "";
|
||||
case 1: // I
|
||||
return unit;
|
||||
case 2: // II
|
||||
return unit + unit;
|
||||
case 3: // III
|
||||
return unit + unit + unit;
|
||||
case 4: // IV
|
||||
return unit + quintuple;
|
||||
case 9: // IX
|
||||
return unit + decuple;
|
||||
default:
|
||||
// V, VI, VII, VIII
|
||||
return quintuple + formatDigit(digit - 5, unit, quintuple, decuple);
|
||||
}
|
||||
}
|
||||
let thousands: any = Math.floor(number / 1000);
|
||||
let thousandsPart: any = "";
|
||||
while (thousands > 0) {
|
||||
thousandsPart += "M";
|
||||
thousands -= 1;
|
||||
}
|
||||
const hundreds: any = Math.floor((number % 1000) / 100);
|
||||
const hundredsPart: any = formatDigit(hundreds, "C", "D", "M");
|
||||
const tens: any = Math.floor((number % 100) / 10);
|
||||
const tensPart: any = formatDigit(tens, "X", "L", "C");
|
||||
const units: any = number % 10;
|
||||
const unitsPart: any = formatDigit(units, "I", "V", "X");
|
||||
const formatted: any = thousandsPart + hundredsPart + tensPart + unitsPart;
|
||||
romanLiteralsCache[number] = formatted;
|
||||
return formatted;
|
||||
}
|
||||
/**
|
||||
* Returns the appropriate logo sprite path
|
||||
*/
|
||||
export function getLogoSprite(): any {
|
||||
if (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) {
|
||||
return "logo.png";
|
||||
}
|
||||
if (G_IS_BROWSER) {
|
||||
return "logo_demo.png";
|
||||
}
|
||||
return "logo.png";
|
||||
}
|
||||
/**
|
||||
* Rejects a promise after X ms
|
||||
*/
|
||||
export function timeoutPromise(promise: Promise, timeout: any = 30000): any {
|
||||
return Promise.race([
|
||||
new Promise((resolve: any, reject: any): any => {
|
||||
setTimeout((): any => reject("timeout of " + timeout + " ms exceeded"), timeout);
|
||||
}),
|
||||
promise,
|
||||
]);
|
||||
}
|
@ -0,0 +1,577 @@
|
||||
import { globalConfig } from "./config";
|
||||
import { safeModulo } from "./utils";
|
||||
const tileSize: any = globalConfig.tileSize;
|
||||
const halfTileSize: any = globalConfig.halfTileSize;
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const enumDirection: any = {
|
||||
top: "top",
|
||||
right: "right",
|
||||
bottom: "bottom",
|
||||
left: "left",
|
||||
};
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const enumInvertedDirections: any = {
|
||||
[enumDirection.top]: enumDirection.bottom,
|
||||
[enumDirection.right]: enumDirection.left,
|
||||
[enumDirection.bottom]: enumDirection.top,
|
||||
[enumDirection.left]: enumDirection.right,
|
||||
};
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const enumDirectionToAngle: any = {
|
||||
[enumDirection.top]: 0,
|
||||
[enumDirection.right]: 90,
|
||||
[enumDirection.bottom]: 180,
|
||||
[enumDirection.left]: 270,
|
||||
};
|
||||
/**
|
||||
* @enum {enumDirection}
|
||||
*/
|
||||
export const enumAngleToDirection: any = {
|
||||
0: enumDirection.top,
|
||||
90: enumDirection.right,
|
||||
180: enumDirection.bottom,
|
||||
270: enumDirection.left,
|
||||
};
|
||||
export const arrayAllDirections: Array<enumDirection> = [
|
||||
enumDirection.top,
|
||||
enumDirection.right,
|
||||
enumDirection.bottom,
|
||||
enumDirection.left,
|
||||
];
|
||||
export class Vector {
|
||||
public x = x || 0;
|
||||
public y = y || 0;
|
||||
|
||||
constructor(x, y) {
|
||||
}
|
||||
/**
|
||||
* return a copy of the vector
|
||||
* {}
|
||||
*/
|
||||
copy(): Vector {
|
||||
return new Vector(this.x, this.y);
|
||||
}
|
||||
/**
|
||||
* Adds a vector and return a new vector
|
||||
* {}
|
||||
*/
|
||||
add(other: Vector): Vector {
|
||||
return new Vector(this.x + other.x, this.y + other.y);
|
||||
}
|
||||
/**
|
||||
* Adds a vector
|
||||
* {}
|
||||
*/
|
||||
addInplace(other: Vector): Vector {
|
||||
this.x += other.x;
|
||||
this.y += other.y;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Substracts a vector and return a new vector
|
||||
* {}
|
||||
*/
|
||||
sub(other: Vector): Vector {
|
||||
return new Vector(this.x - other.x, this.y - other.y);
|
||||
}
|
||||
/**
|
||||
* Subs a vector
|
||||
* {}
|
||||
*/
|
||||
subInplace(other: Vector): Vector {
|
||||
this.x -= other.x;
|
||||
this.y -= other.y;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Multiplies with a vector and return a new vector
|
||||
* {}
|
||||
*/
|
||||
mul(other: Vector): Vector {
|
||||
return new Vector(this.x * other.x, this.y * other.y);
|
||||
}
|
||||
/**
|
||||
* Adds two scalars and return a new vector
|
||||
* {}
|
||||
*/
|
||||
addScalars(x: number, y: number): Vector {
|
||||
return new Vector(this.x + x, this.y + y);
|
||||
}
|
||||
/**
|
||||
* Substracts a scalar and return a new vector
|
||||
* {}
|
||||
*/
|
||||
subScalar(f: number): Vector {
|
||||
return new Vector(this.x - f, this.y - f);
|
||||
}
|
||||
/**
|
||||
* Substracts two scalars and return a new vector
|
||||
* {}
|
||||
*/
|
||||
subScalars(x: number, y: number): Vector {
|
||||
return new Vector(this.x - x, this.y - y);
|
||||
}
|
||||
/**
|
||||
* Returns the euclidian length
|
||||
* {}
|
||||
*/
|
||||
length(): number {
|
||||
return Math.hypot(this.x, this.y);
|
||||
}
|
||||
/**
|
||||
* Returns the square length
|
||||
* {}
|
||||
*/
|
||||
lengthSquare(): number {
|
||||
return this.x * this.x + this.y * this.y;
|
||||
}
|
||||
/**
|
||||
* Divides both components by a scalar and return a new vector
|
||||
* {}
|
||||
*/
|
||||
divideScalar(f: number): Vector {
|
||||
return new Vector(this.x / f, this.y / f);
|
||||
}
|
||||
/**
|
||||
* Divides both components by the given scalars and return a new vector
|
||||
* {}
|
||||
*/
|
||||
divideScalars(a: number, b: number): Vector {
|
||||
return new Vector(this.x / a, this.y / b);
|
||||
}
|
||||
/**
|
||||
* Divides both components by a scalar
|
||||
* {}
|
||||
*/
|
||||
divideScalarInplace(f: number): Vector {
|
||||
this.x /= f;
|
||||
this.y /= f;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Multiplies both components with a scalar and return a new vector
|
||||
* {}
|
||||
*/
|
||||
multiplyScalar(f: number): Vector {
|
||||
return new Vector(this.x * f, this.y * f);
|
||||
}
|
||||
/**
|
||||
* Multiplies both components with two scalars and returns a new vector
|
||||
* {}
|
||||
*/
|
||||
multiplyScalars(a: number, b: number): Vector {
|
||||
return new Vector(this.x * a, this.y * b);
|
||||
}
|
||||
/**
|
||||
* For both components, compute the maximum of each component and the given scalar, and return a new vector.
|
||||
* For example:
|
||||
* - new Vector(-1, 5).maxScalar(0) -> Vector(0, 5)
|
||||
* {}
|
||||
*/
|
||||
maxScalar(f: number): Vector {
|
||||
return new Vector(Math.max(f, this.x), Math.max(f, this.y));
|
||||
}
|
||||
/**
|
||||
* Adds a scalar to both components and return a new vector
|
||||
* {}
|
||||
*/
|
||||
addScalar(f: number): Vector {
|
||||
return new Vector(this.x + f, this.y + f);
|
||||
}
|
||||
/**
|
||||
* Computes the component wise minimum and return a new vector
|
||||
* {}
|
||||
*/
|
||||
min(v: Vector): Vector {
|
||||
return new Vector(Math.min(v.x, this.x), Math.min(v.y, this.y));
|
||||
}
|
||||
/**
|
||||
* Computes the component wise maximum and return a new vector
|
||||
* {}
|
||||
*/
|
||||
max(v: Vector): Vector {
|
||||
return new Vector(Math.max(v.x, this.x), Math.max(v.y, this.y));
|
||||
}
|
||||
/**
|
||||
* Computes the component wise absolute
|
||||
* {}
|
||||
*/
|
||||
abs(): Vector {
|
||||
return new Vector(Math.abs(this.x), Math.abs(this.y));
|
||||
}
|
||||
/**
|
||||
* Computes the scalar product
|
||||
* {}
|
||||
*/
|
||||
dot(v: Vector): number {
|
||||
return this.x * v.x + this.y * v.y;
|
||||
}
|
||||
/**
|
||||
* Computes the distance to a given vector
|
||||
* {}
|
||||
*/
|
||||
distance(v: Vector): number {
|
||||
return Math.hypot(this.x - v.x, this.y - v.y);
|
||||
}
|
||||
/**
|
||||
* Computes the square distance to a given vectort
|
||||
* {}
|
||||
*/
|
||||
distanceSquare(v: Vector): number {
|
||||
const dx: any = this.x - v.x;
|
||||
const dy: any = this.y - v.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
/**
|
||||
* Returns x % f, y % f
|
||||
* {} new vector
|
||||
*/
|
||||
modScalar(f: number): Vector {
|
||||
return new Vector(safeModulo(this.x, f), safeModulo(this.y, f));
|
||||
}
|
||||
/**
|
||||
* Computes and returns the center between both points
|
||||
* {}
|
||||
*/
|
||||
centerPoint(v: Vector): Vector {
|
||||
const cx: any = this.x + v.x;
|
||||
const cy: any = this.y + v.y;
|
||||
return new Vector(cx / 2, cy / 2);
|
||||
}
|
||||
/**
|
||||
* Computes componentwise floor and returns a new vector
|
||||
* {}
|
||||
*/
|
||||
floor(): Vector {
|
||||
return new Vector(Math.floor(this.x), Math.floor(this.y));
|
||||
}
|
||||
/**
|
||||
* Computes componentwise ceil and returns a new vector
|
||||
* {}
|
||||
*/
|
||||
ceil(): Vector {
|
||||
return new Vector(Math.ceil(this.x), Math.ceil(this.y));
|
||||
}
|
||||
/**
|
||||
* Computes componentwise round and return a new vector
|
||||
* {}
|
||||
*/
|
||||
round(): Vector {
|
||||
return new Vector(Math.round(this.x), Math.round(this.y));
|
||||
}
|
||||
/**
|
||||
* Converts this vector from world to tile space and return a new vector
|
||||
* {}
|
||||
*/
|
||||
toTileSpace(): Vector {
|
||||
return new Vector(Math.floor(this.x / tileSize), Math.floor(this.y / tileSize));
|
||||
}
|
||||
/**
|
||||
* Converts this vector from world to street space and return a new vector
|
||||
* {}
|
||||
*/
|
||||
toStreetSpace(): Vector {
|
||||
return new Vector(Math.floor(this.x / halfTileSize + 0.25), Math.floor(this.y / halfTileSize + 0.25));
|
||||
}
|
||||
/**
|
||||
* Converts this vector to world space and return a new vector
|
||||
* {}
|
||||
*/
|
||||
toWorldSpace(): Vector {
|
||||
return new Vector(this.x * tileSize, this.y * tileSize);
|
||||
}
|
||||
/**
|
||||
* Converts this vector to world space and return a new vector
|
||||
* {}
|
||||
*/
|
||||
toWorldSpaceCenterOfTile(): Vector {
|
||||
return new Vector(this.x * tileSize + halfTileSize, this.y * tileSize + halfTileSize);
|
||||
}
|
||||
/**
|
||||
* Converts the top left tile position of this vector
|
||||
* {}
|
||||
*/
|
||||
snapWorldToTile(): Vector {
|
||||
return new Vector(Math.floor(this.x / tileSize) * tileSize, Math.floor(this.y / tileSize) * tileSize);
|
||||
}
|
||||
/**
|
||||
* Normalizes the vector, dividing by the length(), and return a new vector
|
||||
* {}
|
||||
*/
|
||||
normalize(): Vector {
|
||||
const len: any = Math.max(1e-5, Math.hypot(this.x, this.y));
|
||||
return new Vector(this.x / len, this.y / len);
|
||||
}
|
||||
/**
|
||||
* Normalizes the vector, dividing by the length(), and return a new vector
|
||||
* {}
|
||||
*/
|
||||
normalizeIfGreaterOne(): Vector {
|
||||
const len: any = Math.max(1, Math.hypot(this.x, this.y));
|
||||
return new Vector(this.x / len, this.y / len);
|
||||
}
|
||||
/**
|
||||
* Returns the normalized vector to the other point
|
||||
* {}
|
||||
*/
|
||||
normalizedDirection(v: Vector): Vector {
|
||||
const dx: any = v.x - this.x;
|
||||
const dy: any = v.y - this.y;
|
||||
const len: any = Math.max(1e-5, Math.hypot(dx, dy));
|
||||
return new Vector(dx / len, dy / len);
|
||||
}
|
||||
/**
|
||||
* Returns a perpendicular vector
|
||||
* {}
|
||||
*/
|
||||
findPerpendicular(): Vector {
|
||||
return new Vector(-this.y, this.x);
|
||||
}
|
||||
/**
|
||||
* Returns the unnormalized direction to the other point
|
||||
* {}
|
||||
*/
|
||||
direction(v: Vector): Vector {
|
||||
return new Vector(v.x - this.x, v.y - this.y);
|
||||
}
|
||||
/**
|
||||
* Returns a string representation of the vector
|
||||
* {}
|
||||
*/
|
||||
toString(): string {
|
||||
return this.x + "," + this.y;
|
||||
}
|
||||
/**
|
||||
* Compares both vectors for exact equality. Does not do an epsilon compare
|
||||
* {}
|
||||
*/
|
||||
equals(v: Vector): Boolean {
|
||||
return this.x === v.x && this.y === v.y;
|
||||
}
|
||||
/**
|
||||
* Rotates this vector
|
||||
* {} new vector
|
||||
*/
|
||||
rotated(angle: number): Vector {
|
||||
const sin: any = Math.sin(angle);
|
||||
const cos: any = Math.cos(angle);
|
||||
return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
|
||||
}
|
||||
/**
|
||||
* Rotates this vector
|
||||
* {} this vector
|
||||
*/
|
||||
rotateInplaceFastMultipleOf90(angle: number): Vector {
|
||||
// const sin = Math.sin(angle);
|
||||
// const cos = Math.cos(angle);
|
||||
// let sin = 0, cos = 1;
|
||||
assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle);
|
||||
switch (angle) {
|
||||
case 0:
|
||||
case 360: {
|
||||
return this;
|
||||
}
|
||||
case 90: {
|
||||
// sin = 1;
|
||||
// cos = 0;
|
||||
const x: any = this.x;
|
||||
this.x = -this.y;
|
||||
this.y = x;
|
||||
return this;
|
||||
}
|
||||
case 180: {
|
||||
// sin = 0
|
||||
// cos = -1
|
||||
this.x = -this.x;
|
||||
this.y = -this.y;
|
||||
return this;
|
||||
}
|
||||
case 270: {
|
||||
// sin = -1
|
||||
// cos = 0
|
||||
const x: any = this.x;
|
||||
this.x = this.y;
|
||||
this.y = -x;
|
||||
return this;
|
||||
}
|
||||
default: {
|
||||
assertAlways(false, "Invalid fast inplace rotation: " + angle);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
// return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
|
||||
}
|
||||
/**
|
||||
* Rotates this vector
|
||||
* {} new vector
|
||||
*/
|
||||
rotateFastMultipleOf90(angle: number): Vector {
|
||||
assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle);
|
||||
switch (angle) {
|
||||
case 360:
|
||||
case 0: {
|
||||
return new Vector(this.x, this.y);
|
||||
}
|
||||
case 90: {
|
||||
return new Vector(-this.y, this.x);
|
||||
}
|
||||
case 180: {
|
||||
return new Vector(-this.x, -this.y);
|
||||
}
|
||||
case 270: {
|
||||
return new Vector(this.y, -this.x);
|
||||
}
|
||||
default: {
|
||||
assertAlways(false, "Invalid fast inplace rotation: " + angle);
|
||||
return new Vector();
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Helper method to rotate a direction
|
||||
* {}
|
||||
*/
|
||||
static transformDirectionFromMultipleOf90(direction: enumDirection, angle: number): enumDirection {
|
||||
if (angle === 0 || angle === 360) {
|
||||
return direction;
|
||||
}
|
||||
assert(angle >= 0 && angle <= 360, "Invalid angle: " + angle);
|
||||
switch (direction) {
|
||||
case enumDirection.top: {
|
||||
switch (angle) {
|
||||
case 90:
|
||||
return enumDirection.right;
|
||||
case 180:
|
||||
return enumDirection.bottom;
|
||||
case 270:
|
||||
return enumDirection.left;
|
||||
default:
|
||||
assertAlways(false, "Invalid angle: " + angle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
case enumDirection.right: {
|
||||
switch (angle) {
|
||||
case 90:
|
||||
return enumDirection.bottom;
|
||||
case 180:
|
||||
return enumDirection.left;
|
||||
case 270:
|
||||
return enumDirection.top;
|
||||
default:
|
||||
assertAlways(false, "Invalid angle: " + angle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
case enumDirection.bottom: {
|
||||
switch (angle) {
|
||||
case 90:
|
||||
return enumDirection.left;
|
||||
case 180:
|
||||
return enumDirection.top;
|
||||
case 270:
|
||||
return enumDirection.right;
|
||||
default:
|
||||
assertAlways(false, "Invalid angle: " + angle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
case enumDirection.left: {
|
||||
switch (angle) {
|
||||
case 90:
|
||||
return enumDirection.top;
|
||||
case 180:
|
||||
return enumDirection.right;
|
||||
case 270:
|
||||
return enumDirection.bottom;
|
||||
default:
|
||||
assertAlways(false, "Invalid angle: " + angle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
default:
|
||||
assertAlways(false, "Invalid angle: " + angle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Compares both vectors for epsilon equality
|
||||
* {}
|
||||
*/
|
||||
equalsEpsilon(v: Vector, epsilon: any = 1e-5): Boolean {
|
||||
return Math.abs(this.x - v.x) < 1e-5 && Math.abs(this.y - v.y) < epsilon;
|
||||
}
|
||||
/**
|
||||
* Returns the angle
|
||||
* {} 0 .. 2 PI
|
||||
*/
|
||||
angle(): number {
|
||||
return Math.atan2(this.y, this.x) + Math.PI / 2;
|
||||
}
|
||||
/**
|
||||
* Serializes the vector to a string
|
||||
* {}
|
||||
*/
|
||||
serializeTile(): string {
|
||||
return String.fromCharCode(33 + this.x) + String.fromCharCode(33 + this.y);
|
||||
}
|
||||
/**
|
||||
* Creates a simple representation of the vector
|
||||
*/
|
||||
serializeSimple(): any {
|
||||
return { x: this.x, y: this.y };
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
serializeTileToInt(): number {
|
||||
return this.x + this.y * 256;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* {}
|
||||
*/
|
||||
static deserializeTileFromInt(i: number): Vector {
|
||||
const x: any = i % 256;
|
||||
const y: any = Math.floor(i / 256);
|
||||
return new Vector(x, y);
|
||||
}
|
||||
/**
|
||||
* Deserializes a vector from a string
|
||||
* {}
|
||||
*/
|
||||
static deserializeTile(s: string): Vector {
|
||||
return new Vector(s.charCodeAt(0) - 33, s.charCodeAt(1) - 33);
|
||||
}
|
||||
/**
|
||||
* Deserializes a vector from a serialized json object
|
||||
* {}
|
||||
*/
|
||||
static fromSerializedObject(obj: object): Vector {
|
||||
if (obj) {
|
||||
return new Vector(obj.x || 0, obj.y || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Interpolates two vectors, for a = 0, returns v1 and for a = 1 return v2, otherwise interpolate
|
||||
*/
|
||||
export function mixVector(v1: Vector, v2: Vector, a: number): any {
|
||||
return new Vector(v1.x * (1 - a) + v2.x * a, v1.y * (1 - a) + v2.y * a);
|
||||
}
|
||||
/**
|
||||
* Mapping from string direction to actual vector
|
||||
* @enum {Vector}
|
||||
*/
|
||||
export const enumDirectionToVector: any = {
|
||||
top: new Vector(0, -1),
|
||||
right: new Vector(1, 0),
|
||||
bottom: new Vector(0, 1),
|
||||
left: new Vector(-1, 0),
|
||||
};
|
@ -0,0 +1,104 @@
|
||||
/* typehints:start */
|
||||
import type { Entity } from "./entity";
|
||||
import type { GameRoot } from "./root";
|
||||
/* typehints:end */
|
||||
import { globalConfig } from "../core/config";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { ACHIEVEMENTS } from "../platform/achievement_provider";
|
||||
import { getBuildingDataFromCode } from "./building_codes";
|
||||
const logger: any = createLogger("achievement_proxy");
|
||||
const ROTATER: any = "rotater";
|
||||
const DEFAULT: any = "default";
|
||||
export class AchievementProxy {
|
||||
public root = root;
|
||||
public provider = this.root.app.achievementProvider;
|
||||
public disabled = true;
|
||||
public sliceTime = 0;
|
||||
|
||||
constructor(root) {
|
||||
if (G_IS_DEV && globalConfig.debug.testAchievements) {
|
||||
// still enable the proxy
|
||||
}
|
||||
else if (!this.provider.hasAchievements()) {
|
||||
return;
|
||||
}
|
||||
this.root.signals.postLoadHook.add(this.onLoad, this);
|
||||
}
|
||||
onLoad(): any {
|
||||
if (!this.root.gameMode.hasAchievements()) {
|
||||
logger.log("Disabling achievements because game mode does not have achievements");
|
||||
this.disabled = true;
|
||||
return;
|
||||
}
|
||||
this.provider
|
||||
.onLoad(this.root)
|
||||
.then((): any => {
|
||||
this.disabled = false;
|
||||
logger.log("Recieving achievement signals");
|
||||
this.initialize();
|
||||
})
|
||||
.catch((err: any): any => {
|
||||
this.disabled = true;
|
||||
logger.error("Ignoring achievement signals", err);
|
||||
});
|
||||
}
|
||||
initialize(): any {
|
||||
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.darkMode, null);
|
||||
if (this.has(ACHIEVEMENTS.mam)) {
|
||||
this.root.signals.entityAdded.add(this.onMamFailure, this);
|
||||
this.root.signals.entityDestroyed.add(this.onMamFailure, this);
|
||||
this.root.signals.storyGoalCompleted.add(this.onStoryGoalCompleted, this);
|
||||
}
|
||||
if (this.has(ACHIEVEMENTS.noInverseRotater)) {
|
||||
this.root.signals.entityAdded.add(this.onEntityAdded, this);
|
||||
}
|
||||
this.startSlice();
|
||||
}
|
||||
startSlice(): any {
|
||||
this.sliceTime = this.root.time.now();
|
||||
this.root.signals.bulkAchievementCheck.dispatch(ACHIEVEMENTS.storeShape, this.sliceTime, ACHIEVEMENTS.throughputBp25, this.sliceTime, ACHIEVEMENTS.throughputBp50, this.sliceTime, ACHIEVEMENTS.throughputLogo25, this.sliceTime, ACHIEVEMENTS.throughputLogo50, this.sliceTime, ACHIEVEMENTS.throughputRocket10, this.sliceTime, ACHIEVEMENTS.throughputRocket20, this.sliceTime, ACHIEVEMENTS.play1h, this.sliceTime, ACHIEVEMENTS.play10h, this.sliceTime, ACHIEVEMENTS.play20h, this.sliceTime);
|
||||
}
|
||||
update(): any {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
if (this.root.time.now() - this.sliceTime > globalConfig.achievementSliceDuration) {
|
||||
this.startSlice();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
has(key: string): boolean {
|
||||
if (!this.provider.collection) {
|
||||
return false;
|
||||
}
|
||||
return this.provider.collection.map.has(key);
|
||||
}
|
||||
onEntityAdded(entity: Entity): any {
|
||||
if (!entity.components.StaticMapEntity) {
|
||||
return;
|
||||
}
|
||||
const building: any = getBuildingDataFromCode(entity.components.StaticMapEntity.code);
|
||||
if (building.metaInstance.id !== ROTATER) {
|
||||
return;
|
||||
}
|
||||
if (building.variant === DEFAULT) {
|
||||
return;
|
||||
}
|
||||
this.root.savegame.currentData.stats.usedInverseRotater = true;
|
||||
this.root.signals.entityAdded.remove(this.onEntityAdded);
|
||||
}
|
||||
onStoryGoalCompleted(level: number): any {
|
||||
if (level > 26) {
|
||||
this.root.signals.entityAdded.add(this.onMamFailure, this);
|
||||
this.root.signals.entityDestroyed.add(this.onMamFailure, this);
|
||||
}
|
||||
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.mam, null);
|
||||
// reset on every level
|
||||
this.root.savegame.currentData.stats.failedMam = false;
|
||||
}
|
||||
onMamFailure(): any {
|
||||
this.root.savegame.currentData.stats.failedMam = true;
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import { globalConfig } from "../core/config";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { GameRoot } from "./root";
|
||||
// How important it is that a savegame is created
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const enumSavePriority: any = {
|
||||
regular: 2,
|
||||
asap: 100,
|
||||
};
|
||||
const logger: any = createLogger("autosave");
|
||||
export class AutomaticSave {
|
||||
public root: GameRoot = root;
|
||||
public saveImportance = enumSavePriority.regular;
|
||||
public lastSaveAttempt = -1000;
|
||||
|
||||
constructor(root) {
|
||||
}
|
||||
setSaveImportance(importance: any): any {
|
||||
this.saveImportance = Math.max(this.saveImportance, importance);
|
||||
}
|
||||
doSave(): any {
|
||||
if (G_IS_DEV && globalConfig.debug.disableSavegameWrite) {
|
||||
return;
|
||||
}
|
||||
this.root.gameState.doSave();
|
||||
this.saveImportance = enumSavePriority.regular;
|
||||
}
|
||||
update(): any {
|
||||
if (!this.root.gameInitialized) {
|
||||
// Bad idea
|
||||
return;
|
||||
}
|
||||
const saveInterval: any = this.root.app.settings.getAutosaveIntervalSeconds();
|
||||
if (!saveInterval) {
|
||||
// Disabled
|
||||
return;
|
||||
}
|
||||
// Check when the last save was, but make sure that if it fails, we don't spam
|
||||
const lastSaveTime: any = Math.max(this.lastSaveAttempt, this.root.savegame.getRealLastUpdate());
|
||||
const secondsSinceLastSave: any = (Date.now() - lastSaveTime) / 1000.0;
|
||||
let shouldSave: any = false;
|
||||
switch (this.saveImportance) {
|
||||
case enumSavePriority.asap:
|
||||
// High always should save
|
||||
shouldSave = true;
|
||||
break;
|
||||
case enumSavePriority.regular:
|
||||
// Could determine if there is a good / bad point here
|
||||
shouldSave = secondsSinceLastSave > saveInterval;
|
||||
break;
|
||||
default:
|
||||
assert(false, "Unknown save prio: " + this.saveImportance);
|
||||
break;
|
||||
}
|
||||
if (shouldSave) {
|
||||
logger.log("Saving automatically");
|
||||
this.lastSaveAttempt = Date.now();
|
||||
this.doSave();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import { globalConfig } from "../core/config";
|
||||
import { DrawParameters } from "../core/draw_parameters";
|
||||
import { BasicSerializableObject } from "../savegame/serialization";
|
||||
/**
|
||||
* Class for items on belts etc. Not an entity for performance reasons
|
||||
*/
|
||||
export class BaseItem extends BasicSerializableObject {
|
||||
public _type = this.getItemType();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
static getId(): any {
|
||||
return "base_item";
|
||||
}
|
||||
/** {} */
|
||||
static getSchema(): import("../savegame/serialization").Schema {
|
||||
return {};
|
||||
}
|
||||
/** {} **/
|
||||
getItemType(): ItemType {
|
||||
abstract;
|
||||
return "shape";
|
||||
}
|
||||
/**
|
||||
* Returns a string id of the item
|
||||
* {}
|
||||
* @abstract
|
||||
*/
|
||||
getAsCopyableKey(): string {
|
||||
abstract;
|
||||
return "";
|
||||
}
|
||||
/**
|
||||
* Returns if the item equals the other itme
|
||||
* {}
|
||||
*/
|
||||
equals(other: BaseItem): boolean {
|
||||
if (this.getItemType() !== other.getItemType()) {
|
||||
return false;
|
||||
}
|
||||
return this.equalsImpl(other);
|
||||
}
|
||||
/**
|
||||
* Override for custom comparison
|
||||
* {}
|
||||
* @abstract
|
||||
*/
|
||||
equalsImpl(other: BaseItem): boolean {
|
||||
abstract;
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Draws the item to a canvas
|
||||
* @abstract
|
||||
*/
|
||||
drawFullSizeOnCanvas(context: CanvasRenderingContext2D, size: number): any {
|
||||
abstract;
|
||||
}
|
||||
/**
|
||||
* Draws the item at the given position
|
||||
*/
|
||||
drawItemCenteredClipped(x: number, y: number, parameters: DrawParameters, diameter: number= = globalConfig.defaultItemDiameter): any {
|
||||
if (parameters.visibleRect.containsCircle(x, y, diameter / 2)) {
|
||||
this.drawItemCenteredImpl(x, y, parameters, diameter);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* INTERNAL
|
||||
* @abstract
|
||||
*/
|
||||
drawItemCenteredImpl(x: number, y: number, parameters: DrawParameters, diameter: number= = globalConfig.defaultItemDiameter): any {
|
||||
abstract;
|
||||
}
|
||||
getBackgroundColorAsResource(): any {
|
||||
abstract;
|
||||
return "";
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,143 @@
|
||||
import { globalConfig } from "../core/config";
|
||||
import { DrawParameters } from "../core/draw_parameters";
|
||||
import { findNiceIntegerValue } from "../core/utils";
|
||||
import { Vector } from "../core/vector";
|
||||
import { Entity } from "./entity";
|
||||
import { ACHIEVEMENTS } from "../platform/achievement_provider";
|
||||
import { GameRoot } from "./root";
|
||||
export class Blueprint {
|
||||
public entities = entities;
|
||||
|
||||
constructor(entities) {
|
||||
}
|
||||
/**
|
||||
* Returns the layer of this blueprint
|
||||
* {}
|
||||
*/
|
||||
get layer() {
|
||||
if (this.entities.length === 0) {
|
||||
return "regular";
|
||||
}
|
||||
return this.entities[0].layer;
|
||||
}
|
||||
/**
|
||||
* Creates a new blueprint from the given entity uids
|
||||
*/
|
||||
static fromUids(root: GameRoot, uids: Array<number>): any {
|
||||
const newEntities: any = [];
|
||||
let averagePosition: any = new Vector();
|
||||
// First, create a copy
|
||||
for (let i: any = 0; i < uids.length; ++i) {
|
||||
const entity: any = root.entityMgr.findByUid(uids[i]);
|
||||
assert(entity, "Entity for blueprint not found:" + uids[i]);
|
||||
const clone: any = entity.clone();
|
||||
newEntities.push(clone);
|
||||
const pos: any = entity.components.StaticMapEntity.getTileSpaceBounds().getCenter();
|
||||
averagePosition.addInplace(pos);
|
||||
}
|
||||
averagePosition.divideScalarInplace(uids.length);
|
||||
const blueprintOrigin: any = averagePosition.subScalars(0.5, 0.5).floor();
|
||||
for (let i: any = 0; i < uids.length; ++i) {
|
||||
newEntities[i].components.StaticMapEntity.origin.subInplace(blueprintOrigin);
|
||||
}
|
||||
// Now, make sure the origin is 0,0
|
||||
return new Blueprint(newEntities);
|
||||
}
|
||||
/**
|
||||
* Returns the cost of this blueprint in shapes
|
||||
*/
|
||||
getCost(): any {
|
||||
if (G_IS_DEV && globalConfig.debug.blueprintsNoCost) {
|
||||
return 0;
|
||||
}
|
||||
return findNiceIntegerValue(4 * Math.pow(this.entities.length, 1.1));
|
||||
}
|
||||
/**
|
||||
* Draws the blueprint at the given origin
|
||||
*/
|
||||
draw(parameters: DrawParameters, tile: any): any {
|
||||
parameters.context.globalAlpha = 0.8;
|
||||
for (let i: any = 0; i < this.entities.length; ++i) {
|
||||
const entity: any = this.entities[i];
|
||||
const staticComp: any = entity.components.StaticMapEntity;
|
||||
const newPos: any = staticComp.origin.add(tile);
|
||||
const rect: any = staticComp.getTileSpaceBounds();
|
||||
rect.moveBy(tile.x, tile.y);
|
||||
if (!parameters.root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
|
||||
parameters.context.globalAlpha = 0.3;
|
||||
}
|
||||
else {
|
||||
parameters.context.globalAlpha = 1;
|
||||
}
|
||||
staticComp.drawSpriteOnBoundsClipped(parameters, staticComp.getBlueprintSprite(), 0, newPos);
|
||||
}
|
||||
parameters.context.globalAlpha = 1;
|
||||
}
|
||||
/**
|
||||
* Rotates the blueprint clockwise
|
||||
*/
|
||||
rotateCw(): any {
|
||||
for (let i: any = 0; i < this.entities.length; ++i) {
|
||||
const entity: any = this.entities[i];
|
||||
const staticComp: any = entity.components.StaticMapEntity;
|
||||
// Actually keeping this in as an easter egg to rotate the trash can
|
||||
// if (staticComp.getMetaBuilding().getIsRotateable()) {
|
||||
staticComp.rotation = (staticComp.rotation + 90) % 360;
|
||||
staticComp.originalRotation = (staticComp.originalRotation + 90) % 360;
|
||||
// }
|
||||
staticComp.origin = staticComp.origin.rotateFastMultipleOf90(90);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Rotates the blueprint counter clock wise
|
||||
*/
|
||||
rotateCcw(): any {
|
||||
// Well ...
|
||||
for (let i: any = 0; i < 3; ++i) {
|
||||
this.rotateCw();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks if the blueprint can be placed at the given tile
|
||||
*/
|
||||
canPlace(root: GameRoot, tile: Vector): any {
|
||||
let anyPlaceable: any = false;
|
||||
for (let i: any = 0; i < this.entities.length; ++i) {
|
||||
const entity: any = this.entities[i];
|
||||
if (root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
|
||||
anyPlaceable = true;
|
||||
}
|
||||
}
|
||||
return anyPlaceable;
|
||||
}
|
||||
canAfford(root: GameRoot): any {
|
||||
if (root.gameMode.getHasFreeCopyPaste()) {
|
||||
return true;
|
||||
}
|
||||
return root.hubGoals.getShapesStoredByKey(root.gameMode.getBlueprintShapeKey()) >= this.getCost();
|
||||
}
|
||||
/**
|
||||
* Attempts to place the blueprint at the given tile
|
||||
*/
|
||||
tryPlace(root: GameRoot, tile: Vector): any {
|
||||
return root.logic.performBulkOperation((): any => {
|
||||
return root.logic.performImmutableOperation((): any => {
|
||||
let count: any = 0;
|
||||
for (let i: any = 0; i < this.entities.length; ++i) {
|
||||
const entity: any = this.entities[i];
|
||||
if (!root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
|
||||
continue;
|
||||
}
|
||||
const clone: any = entity.clone();
|
||||
clone.components.StaticMapEntity.origin.addInplace(tile);
|
||||
root.logic.freeEntityAreaBeforeBuild(clone);
|
||||
root.map.placeStaticEntity(clone);
|
||||
root.entityMgr.registerEntity(clone);
|
||||
count++;
|
||||
}
|
||||
root.signals.bulkAchievementCheck.dispatch(ACHIEVEMENTS.placeBlueprint, count, ACHIEVEMENTS.placeBp1000, count);
|
||||
return count !== 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
/* typehints:start */
|
||||
import type { MetaBuilding } from "./meta_building";
|
||||
import type { AtlasSprite } from "../core/sprites";
|
||||
import type { Vector } from "../core/vector";
|
||||
/* typehints:end */
|
||||
import { gMetaBuildingRegistry } from "../core/global_registries";
|
||||
export type BuildingVariantIdentifier = {
|
||||
metaClass: typeof MetaBuilding;
|
||||
metaInstance?: MetaBuilding;
|
||||
variant?: string;
|
||||
rotationVariant?: number;
|
||||
tileSize?: Vector;
|
||||
sprite?: AtlasSprite;
|
||||
blueprintSprite?: AtlasSprite;
|
||||
silhouetteColor?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stores a lookup table for all building variants (for better performance)
|
||||
*/
|
||||
export const gBuildingVariants: {
|
||||
[idx: number|string]: BuildingVariantIdentifier;
|
||||
} = {
|
||||
// Set later
|
||||
};
|
||||
/**
|
||||
* Mapping from 'metaBuildingId/variant/rotationVariant' to building code
|
||||
*/
|
||||
const variantsCache: Map<string, number | string> = new Map();
|
||||
/**
|
||||
* Registers a new variant
|
||||
*/
|
||||
export function registerBuildingVariant(code: number | string, meta: typeof MetaBuilding, variant: string = "default" /* @TODO: Circular dependency, actually its defaultBuildingVariant */, rotationVariant: number = 0): any {
|
||||
assert(!gBuildingVariants[code], "Duplicate id: " + code);
|
||||
gBuildingVariants[code] = {
|
||||
metaClass: meta,
|
||||
metaInstance: gMetaBuildingRegistry.findByClass(meta),
|
||||
variant,
|
||||
rotationVariant,
|
||||
// @ts-ignore
|
||||
tileSize: new meta().getDimensions(variant),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
* @param {} buildingId
|
||||
* @param {} variant
|
||||
* @param {} rotat * @
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
* @param {string} buildingId
|
||||
* @param {string} variant
|
||||
* @param {number} rotat * @returns
|
||||
*/
|
||||
functioildingHash(build striniant: strin
|
||||
/**
|
||||
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
* @param {} buildingId
|
||||
* @param {} variant
|
||||
* @param {} rotationVar * @
|
||||
/**
|
||||
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
* @param {string} buildingId
|
||||
* @param {string} variant
|
||||
* @param {number} rotationVar * @returns
|
||||
*/
|
||||
functiorateBuildingHash(build string, variant: strin
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
* @param {} buildingId
|
||||
* @param {} variant
|
||||
* @param {} rotationVar * @
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
* @param {string} buildingId
|
||||
* @param {string} variant
|
||||
* @param {number} rotationVar * @returns
|
||||
*/
|
||||
functiorateBuildingHash(build string, variant: strin
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
* @param {} buildingId
|
||||
* @param {} variant
|
||||
* @param {} rotationVariant
|
||||
* @
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
*
|
||||
/**
|
||||
* Hashes the combination of buildng, variant and rotation variant
|
||||
* @param {string} buildingId
|
||||
* @param {string} variant
|
||||
* @param {number} rotationVariant
|
||||
* @returns
|
||||
*/
|
||||
function generateBuildingHash(buildingId: string, variant: string, rotationVariant: number): any {
|
||||
return buildingId + "/" + variant + "/" + rotationVariant;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* {}
|
||||
*/
|
||||
export function getBuildingDataFromCode(code: string | number): BuildingVariantIdentifier {
|
||||
assert(gBuildingVariants[code], "Invalid building code: " + code);
|
||||
return gBuildingVariants[code];
|
||||
}
|
||||
/**
|
||||
* Builds the cache for the codes
|
||||
*/
|
||||
export function buildBuildingCodeCache(): any {
|
||||
for (const code: any in gBuildingVariants) {
|
||||
const data: any = gBuildingVariants[code];
|
||||
const hash: any = generateBuildingHash(data.metaInstance.getId(), data.variant, data.rotationVariant);
|
||||
variantsCache.set(hash, isNaN(+code) ? code : +code);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Finds the code for a given variant
|
||||
* {}
|
||||
*/
|
||||
export function getCodeFromBuildingData(metaBuilding: MetaBuilding, variant: string, rotationVariant: number): number | string {
|
||||
const hash: any = generateBuildingHash(metaBuilding.getId(), variant, rotationVariant);
|
||||
const result: any = variantsCache.get(hash);
|
||||
if (G_IS_DEV) {
|
||||
if (!result) {
|
||||
console.warn("Known hashes:", Array.from(variantsCache.keys()));
|
||||
assertAlways(false, "Building not found by data: " + hash);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import { generateMatrixRotations } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
const overlayMatrix: any = generateMatrixRotations([1, 1, 0, 1, 1, 1, 0, 1, 0]);
|
||||
export class MetaAnalyzerBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("analyzer");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 43,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#3a52bc";
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_virtual_processing);
|
||||
}
|
||||
/** {} **/
|
||||
getLayer(): "wires" {
|
||||
return "wires";
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
getRenderPins(): any {
|
||||
// We already have it included
|
||||
return false;
|
||||
}
|
||||
getSpecialOverlayRenderMatrix(rotation: any, rotationVariant: any, variant: any): any {
|
||||
return overlayMatrix[rotation];
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.right,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new LogicGateComponent({
|
||||
type: enumLogicGateType.analyzer,
|
||||
}));
|
||||
}
|
||||
}
|
@ -0,0 +1,207 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||
import { Entity } from "../entity";
|
||||
import { MetaBuilding, defaultBuildingVariant } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
import { T } from "../../translations";
|
||||
import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils";
|
||||
import { BeltUnderlaysComponent } from "../components/belt_underlays";
|
||||
/** @enum {string} */
|
||||
export const enumBalancerVariants: any = {
|
||||
merger: "merger",
|
||||
mergerInverse: "merger-inverse",
|
||||
splitter: "splitter",
|
||||
splitterInverse: "splitter-inverse",
|
||||
};
|
||||
const overlayMatrices: any = {
|
||||
[defaultBuildingVariant]: null,
|
||||
[enumBalancerVariants.merger]: generateMatrixRotations([0, 1, 0, 0, 1, 1, 0, 1, 0]),
|
||||
[enumBalancerVariants.mergerInverse]: generateMatrixRotations([0, 1, 0, 1, 1, 0, 0, 1, 0]),
|
||||
[enumBalancerVariants.splitter]: generateMatrixRotations([0, 1, 0, 0, 1, 1, 0, 1, 0]),
|
||||
[enumBalancerVariants.splitterInverse]: generateMatrixRotations([0, 1, 0, 1, 1, 0, 0, 1, 0]),
|
||||
};
|
||||
export class MetaBalancerBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("balancer");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 4,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
{
|
||||
internalId: 5,
|
||||
variant: enumBalancerVariants.merger,
|
||||
},
|
||||
{
|
||||
internalId: 6,
|
||||
variant: enumBalancerVariants.mergerInverse,
|
||||
},
|
||||
{
|
||||
internalId: 47,
|
||||
variant: enumBalancerVariants.splitter,
|
||||
},
|
||||
{
|
||||
internalId: 48,
|
||||
variant: enumBalancerVariants.splitterInverse,
|
||||
},
|
||||
];
|
||||
}
|
||||
getDimensions(variant: any): any {
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant:
|
||||
return new Vector(2, 1);
|
||||
case enumBalancerVariants.merger:
|
||||
case enumBalancerVariants.mergerInverse:
|
||||
case enumBalancerVariants.splitter:
|
||||
case enumBalancerVariants.splitterInverse:
|
||||
return new Vector(1, 1);
|
||||
default:
|
||||
assertAlways(false, "Unknown balancer variant: " + variant);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): Array<number> | null {
|
||||
const matrix: any = overlayMatrices[variant];
|
||||
if (matrix) {
|
||||
return matrix[rotation];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
|
||||
string,
|
||||
string
|
||||
]> {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
let speedMultiplier: any = 2;
|
||||
switch (variant) {
|
||||
case enumBalancerVariants.merger:
|
||||
case enumBalancerVariants.mergerInverse:
|
||||
case enumBalancerVariants.splitter:
|
||||
case enumBalancerVariants.splitterInverse:
|
||||
speedMultiplier = 1;
|
||||
}
|
||||
const speed: any = (root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.balancer) / 2) * speedMultiplier;
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#555759";
|
||||
}
|
||||
getAvailableVariants(root: GameRoot): any {
|
||||
const deterministic: any = root.gameMode.getIsDeterministic();
|
||||
let available: any = deterministic ? [] : [defaultBuildingVariant];
|
||||
if (!deterministic && root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_merger)) {
|
||||
available.push(enumBalancerVariants.merger, enumBalancerVariants.mergerInverse);
|
||||
}
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_splitter)) {
|
||||
available.push(enumBalancerVariants.splitter, enumBalancerVariants.splitterInverse);
|
||||
}
|
||||
return available;
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_balancer);
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots: [], // set later
|
||||
}));
|
||||
entity.addComponent(new ItemProcessorComponent({
|
||||
inputsPerCharge: 1,
|
||||
processorType: enumItemProcessorTypes.balancer,
|
||||
}));
|
||||
entity.addComponent(new ItemEjectorComponent({
|
||||
slots: [],
|
||||
renderFloatingItems: false,
|
||||
}));
|
||||
entity.addComponent(new BeltUnderlaysComponent({ underlays: [] }));
|
||||
}
|
||||
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant: {
|
||||
entity.components.ItemAcceptor.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
},
|
||||
{
|
||||
pos: new Vector(1, 0),
|
||||
direction: enumDirection.bottom,
|
||||
},
|
||||
]);
|
||||
entity.components.ItemEjector.setSlots([
|
||||
{ pos: new Vector(0, 0), direction: enumDirection.top },
|
||||
{ pos: new Vector(1, 0), direction: enumDirection.top },
|
||||
]);
|
||||
entity.components.BeltUnderlays.underlays = [
|
||||
{ pos: new Vector(0, 0), direction: enumDirection.top },
|
||||
{ pos: new Vector(1, 0), direction: enumDirection.top },
|
||||
];
|
||||
break;
|
||||
}
|
||||
case enumBalancerVariants.merger:
|
||||
case enumBalancerVariants.mergerInverse: {
|
||||
entity.components.ItemAcceptor.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: variant === enumBalancerVariants.mergerInverse
|
||||
? enumDirection.left
|
||||
: enumDirection.right,
|
||||
},
|
||||
]);
|
||||
entity.components.ItemEjector.setSlots([
|
||||
{ pos: new Vector(0, 0), direction: enumDirection.top },
|
||||
]);
|
||||
entity.components.BeltUnderlays.underlays = [
|
||||
{ pos: new Vector(0, 0), direction: enumDirection.top },
|
||||
];
|
||||
break;
|
||||
}
|
||||
case enumBalancerVariants.splitter:
|
||||
case enumBalancerVariants.splitterInverse: {
|
||||
entity.components.ItemAcceptor.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
},
|
||||
]);
|
||||
entity.components.ItemEjector.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: variant === enumBalancerVariants.splitterInverse
|
||||
? enumDirection.left
|
||||
: enumDirection.right,
|
||||
},
|
||||
]);
|
||||
entity.components.BeltUnderlays.underlays = [
|
||||
{ pos: new Vector(0, 0), direction: enumDirection.top },
|
||||
];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertAlways(false, "Unknown balancer variant: " + variant);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,218 @@
|
||||
import { Loader } from "../../core/loader";
|
||||
import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils";
|
||||
import { enumAngleToDirection, enumDirection, Vector } from "../../core/vector";
|
||||
import { SOUNDS } from "../../platform/sound";
|
||||
import { T } from "../../translations";
|
||||
import { BeltComponent } from "../components/belt";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { THEME } from "../theme";
|
||||
export const arrayBeltVariantToRotation: any = [enumDirection.top, enumDirection.left, enumDirection.right];
|
||||
export const beltOverlayMatrices: any = {
|
||||
[enumDirection.top]: generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]),
|
||||
[enumDirection.left]: generateMatrixRotations([0, 0, 0, 1, 1, 0, 0, 1, 0]),
|
||||
[enumDirection.right]: generateMatrixRotations([0, 0, 0, 0, 1, 1, 0, 1, 0]),
|
||||
};
|
||||
export class MetaBeltBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("belt");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 1,
|
||||
variant: defaultBuildingVariant,
|
||||
rotationVariant: 0,
|
||||
},
|
||||
{
|
||||
internalId: 2,
|
||||
variant: defaultBuildingVariant,
|
||||
rotationVariant: 1,
|
||||
},
|
||||
{
|
||||
internalId: 3,
|
||||
variant: defaultBuildingVariant,
|
||||
rotationVariant: 2,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return THEME.map.chunkOverview.beltColor;
|
||||
}
|
||||
getPlacementSound(): any {
|
||||
return SOUNDS.placeBelt;
|
||||
}
|
||||
getHasDirectionLockAvailable(): any {
|
||||
return true;
|
||||
}
|
||||
getStayInPlacementMode(): any {
|
||||
return true;
|
||||
}
|
||||
getRotateAutomaticallyWhilePlacing(): any {
|
||||
return true;
|
||||
}
|
||||
getSprite(): any {
|
||||
return null;
|
||||
}
|
||||
getIsReplaceable(): any {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
|
||||
string,
|
||||
string
|
||||
]> {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const beltSpeed: any = root.hubGoals.getBeltBaseSpeed();
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]];
|
||||
}
|
||||
getPreviewSprite(rotationVariant: any): any {
|
||||
switch (arrayBeltVariantToRotation[rotationVariant]) {
|
||||
case enumDirection.top: {
|
||||
return Loader.getSprite("sprites/buildings/belt_top.png");
|
||||
}
|
||||
case enumDirection.left: {
|
||||
return Loader.getSprite("sprites/buildings/belt_left.png");
|
||||
}
|
||||
case enumDirection.right: {
|
||||
return Loader.getSprite("sprites/buildings/belt_right.png");
|
||||
}
|
||||
default: {
|
||||
assertAlways(false, "Invalid belt rotation variant");
|
||||
}
|
||||
}
|
||||
}
|
||||
getBlueprintSprite(rotationVariant: any): any {
|
||||
switch (arrayBeltVariantToRotation[rotationVariant]) {
|
||||
case enumDirection.top: {
|
||||
return Loader.getSprite("sprites/blueprints/belt_top.png");
|
||||
}
|
||||
case enumDirection.left: {
|
||||
return Loader.getSprite("sprites/blueprints/belt_left.png");
|
||||
}
|
||||
case enumDirection.right: {
|
||||
return Loader.getSprite("sprites/blueprints/belt_right.png");
|
||||
}
|
||||
default: {
|
||||
assertAlways(false, "Invalid belt rotation variant");
|
||||
}
|
||||
}
|
||||
}
|
||||
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): any {
|
||||
return beltOverlayMatrices[entity.components.Belt.direction][rotation];
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new BeltComponent({
|
||||
direction: enumDirection.top, // updated later
|
||||
}));
|
||||
}
|
||||
updateVariants(entity: Entity, rotationVariant: number): any {
|
||||
entity.components.Belt.direction = arrayBeltVariantToRotation[rotationVariant];
|
||||
}
|
||||
/**
|
||||
* Should compute the optimal rotation variant on the given tile
|
||||
* @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array<Entity> }}
|
||||
*/
|
||||
computeOptimalDirectionAndRotationVariantAtTile({ root, tile, rotation, variant, layer }: {
|
||||
root: GameRoot;
|
||||
tile: Vector;
|
||||
rotation: number;
|
||||
variant: string;
|
||||
layer: Layer;
|
||||
}): {
|
||||
rotation: number;
|
||||
rotationVariant: number;
|
||||
connectedEntities?: Array<Entity>;
|
||||
} {
|
||||
const topDirection: any = enumAngleToDirection[rotation];
|
||||
const rightDirection: any = enumAngleToDirection[(rotation + 90) % 360];
|
||||
const bottomDirection: any = enumAngleToDirection[(rotation + 180) % 360];
|
||||
const leftDirection: any = enumAngleToDirection[(rotation + 270) % 360];
|
||||
const { ejectors, acceptors }: any = root.logic.getEjectorsAndAcceptorsAtTile(tile);
|
||||
let hasBottomEjector: any = false;
|
||||
let hasRightEjector: any = false;
|
||||
let hasLeftEjector: any = false;
|
||||
let hasTopAcceptor: any = false;
|
||||
let hasLeftAcceptor: any = false;
|
||||
let hasRightAcceptor: any = false;
|
||||
// Check all ejectors
|
||||
for (let i: any = 0; i < ejectors.length; ++i) {
|
||||
const ejector: any = ejectors[i];
|
||||
if (ejector.toDirection === topDirection) {
|
||||
hasBottomEjector = true;
|
||||
}
|
||||
else if (ejector.toDirection === leftDirection) {
|
||||
hasRightEjector = true;
|
||||
}
|
||||
else if (ejector.toDirection === rightDirection) {
|
||||
hasLeftEjector = true;
|
||||
}
|
||||
}
|
||||
// Check all acceptors
|
||||
for (let i: any = 0; i < acceptors.length; ++i) {
|
||||
const acceptor: any = acceptors[i];
|
||||
if (acceptor.fromDirection === bottomDirection) {
|
||||
hasTopAcceptor = true;
|
||||
}
|
||||
else if (acceptor.fromDirection === rightDirection) {
|
||||
hasLeftAcceptor = true;
|
||||
}
|
||||
else if (acceptor.fromDirection === leftDirection) {
|
||||
hasRightAcceptor = true;
|
||||
}
|
||||
}
|
||||
// Soo .. if there is any ejector below us we always prioritize
|
||||
// this ejector
|
||||
if (!hasBottomEjector) {
|
||||
// When something ejects to us from the left and nothing from the right,
|
||||
// do a curve from the left to the top
|
||||
if (hasRightEjector && !hasLeftEjector) {
|
||||
return {
|
||||
rotation: (rotation + 270) % 360,
|
||||
rotationVariant: 2,
|
||||
};
|
||||
}
|
||||
// When something ejects to us from the right and nothing from the left,
|
||||
// do a curve from the right to the top
|
||||
if (hasLeftEjector && !hasRightEjector) {
|
||||
return {
|
||||
rotation: (rotation + 90) % 360,
|
||||
rotationVariant: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
// When there is a top acceptor, ignore sides
|
||||
// NOTICE: This makes the belt prefer side turns *way* too much!
|
||||
if (!hasTopAcceptor) {
|
||||
// When there is an acceptor to the right but no acceptor to the left,
|
||||
// do a turn to the right
|
||||
if (hasRightAcceptor && !hasLeftAcceptor) {
|
||||
return {
|
||||
rotation,
|
||||
rotationVariant: 2,
|
||||
};
|
||||
}
|
||||
// When there is an acceptor to the left but no acceptor to the right,
|
||||
// do a turn to the left
|
||||
if (hasLeftAcceptor && !hasRightAcceptor) {
|
||||
return {
|
||||
rotation,
|
||||
rotationVariant: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
rotation,
|
||||
rotationVariant: 0,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
/* typehints:start */
|
||||
import type { Entity } from "../entity";
|
||||
/* typehints:end */
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
export class MetaBlockBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("block");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 64,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#333";
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param {import("../../savegame/
|
||||
*/
|
||||
g /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param {import("../../savegame/
|
||||
*/
|
||||
g /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param {} root
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param {import("../../savegame/savegame_serializer").GameRoot} root
|
||||
* @returns
|
||||
*/
|
||||
getIsRemovable(root: import("../../savegame/savegame_serializer").GameRoot): any {
|
||||
return root.gameMode.getIsEditor();
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any { }
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
export class MetaComparatorBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("comparator");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 46,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#823cab";
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_virtual_processing);
|
||||
}
|
||||
/** {} **/
|
||||
getLayer(): "wires" {
|
||||
return "wires";
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
getRenderPins(): any {
|
||||
// We already have it included
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.right,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new LogicGateComponent({
|
||||
type: enumLogicGateType.compare,
|
||||
}));
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
/* typehints:start */
|
||||
import type { Entity } from "../entity";
|
||||
/* typehints:end */
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { ConstantSignalComponent } from "../components/constant_signal";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { ItemProducerComponent } from "../components/item_producer";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
export class MetaConstantProducerBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("constant_producer");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 62,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#bfd630";
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param {import("../../savegame/
|
||||
*/
|
||||
g /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param {import("../../savegame/
|
||||
*/
|
||||
g /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param {} root
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param {import("../../savegame/savegame_serializer").GameRoot} root
|
||||
* @returns
|
||||
*/
|
||||
getIsRemovable(root: import("../../savegame/savegame_serializer").GameRoot): any {
|
||||
return root.gameMode.getIsEditor();
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new ItemEjectorComponent({
|
||||
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
|
||||
}));
|
||||
entity.addComponent(new ItemProducerComponent({}));
|
||||
entity.addComponent(new ConstantSignalComponent({}));
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { ConstantSignalComponent } from "../components/constant_signal";
|
||||
import { generateMatrixRotations } from "../../core/utils";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
const overlayMatrix: any = generateMatrixRotations([0, 1, 0, 1, 1, 1, 1, 1, 1]);
|
||||
export class MetaConstantSignalBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("constant_signal");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 31,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#2b84fd";
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_constant_signal);
|
||||
}
|
||||
/** {} **/
|
||||
getLayer(): "wires" {
|
||||
return "wires";
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
getRenderPins(): any {
|
||||
return false;
|
||||
}
|
||||
getSpecialOverlayRenderMatrix(rotation: any): any {
|
||||
return overlayMatrix[rotation];
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new ConstantSignalComponent({}));
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
import { formatItemsPerSecond } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { T } from "../../translations";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
/** @enum {string} */
|
||||
export const enumCutterVariants: any = { quad: "quad" };
|
||||
export class MetaCutterBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("cutter");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 9,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
{
|
||||
internalId: 10,
|
||||
variant: enumCutterVariants.quad,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#7dcda2";
|
||||
}
|
||||
getDimensions(variant: any): any {
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant:
|
||||
return new Vector(2, 1);
|
||||
case enumCutterVariants.quad:
|
||||
return new Vector(4, 1);
|
||||
default:
|
||||
assertAlways(false, "Unknown cutter variant: " + variant);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
|
||||
string,
|
||||
string
|
||||
]> {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const speed: any = root.hubGoals.getProcessorBaseSpeed(variant === enumCutterVariants.quad
|
||||
? enumItemProcessorTypes.cutterQuad
|
||||
: enumItemProcessorTypes.cutter);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
getAvailableVariants(root: GameRoot): any {
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_quad)) {
|
||||
return [defaultBuildingVariant, enumCutterVariants.quad];
|
||||
}
|
||||
return super.getAvailableVariants(root);
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_and_trash);
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new ItemProcessorComponent({
|
||||
inputsPerCharge: 1,
|
||||
processorType: enumItemProcessorTypes.cutter,
|
||||
}));
|
||||
entity.addComponent(new ItemEjectorComponent({}));
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
filter: "shape",
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant: {
|
||||
entity.components.ItemEjector.setSlots([
|
||||
{ pos: new Vector(0, 0), direction: enumDirection.top },
|
||||
{ pos: new Vector(1, 0), direction: enumDirection.top },
|
||||
]);
|
||||
entity.components.ItemProcessor.type = enumItemProcessorTypes.cutter;
|
||||
break;
|
||||
}
|
||||
case enumCutterVariants.quad: {
|
||||
entity.components.ItemEjector.setSlots([
|
||||
{ pos: new Vector(0, 0), direction: enumDirection.top },
|
||||
{ pos: new Vector(1, 0), direction: enumDirection.top },
|
||||
{ pos: new Vector(2, 0), direction: enumDirection.top },
|
||||
{ pos: new Vector(3, 0), direction: enumDirection.top },
|
||||
]);
|
||||
entity.components.ItemProcessor.type = enumItemProcessorTypes.cutterQuad;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertAlways(false, "Unknown painter variant: " + variant);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { DisplayComponent } from "../components/display";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
export class MetaDisplayBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("display");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 40,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#aaaaaa";
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_display);
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
getShowWiresLayerPreview(): any {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new DisplayComponent());
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import { formatItemsPerSecond } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { T } from "../../translations";
|
||||
import { FilterComponent } from "../components/filter";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
export class MetaFilterBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("filter");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 37,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#c45c2e";
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_filter);
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(2, 1);
|
||||
}
|
||||
getShowWiresLayerPreview(): any {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
|
||||
string,
|
||||
string
|
||||
]> {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const beltSpeed: any = root.hubGoals.getBeltBaseSpeed();
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]];
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new ItemEjectorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
},
|
||||
{
|
||||
pos: new Vector(1, 0),
|
||||
direction: enumDirection.right,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new FilterComponent());
|
||||
}
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
/* typehints:start */
|
||||
import type { Entity } from "../entity";
|
||||
/* typehints:end */
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { GoalAcceptorComponent } from "../components/goal_acceptor";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
export class MetaGoalAcceptorBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("goal_acceptor");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 63,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#ce418a";
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param {import("../../savegame/
|
||||
*/
|
||||
g /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param { /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param {import("../../savegame/
|
||||
*/
|
||||
g /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param {} root
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @ /**
|
||||
*
|
||||
* @param {import("../../savegame/savegame_serializer").GameRoot} root
|
||||
* @returns
|
||||
*/
|
||||
getIsRemovable(root: import("../../savegame/savegame_serializer").GameRoot): any {
|
||||
return root.gameMode.getIsEditor();
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
filter: "shape",
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new ItemProcessorComponent({
|
||||
processorType: enumItemProcessorTypes.goal,
|
||||
}));
|
||||
entity.addComponent(new GoalAcceptorComponent({}));
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { HubComponent } from "../components/hub";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins";
|
||||
export class MetaHubBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("hub");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 26,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(4, 4);
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#eb5555";
|
||||
}
|
||||
getIsRotateable(): any {
|
||||
return false;
|
||||
}
|
||||
getBlueprintSprite(): any {
|
||||
return null;
|
||||
}
|
||||
getSprite(): any {
|
||||
// We render it ourself
|
||||
return null;
|
||||
}
|
||||
getIsRemovable(): any {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new HubComponent());
|
||||
entity.addComponent(new ItemProcessorComponent({
|
||||
inputsPerCharge: 1,
|
||||
processorType: enumItemProcessorTypes.hub,
|
||||
}));
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 2),
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
direction: enumDirection.left,
|
||||
},
|
||||
],
|
||||
}));
|
||||
const slots: Array<import("../components/item_acceptor").ItemAcceptorSlotConfig> = [];
|
||||
for (let i: any = 0; i < 4; ++i) {
|
||||
slots.push({ pos: new Vector(i, 0), direction: enumDirection.top, filter: "shape" }, { pos: new Vector(i, 3), direction: enumDirection.bottom, filter: "shape" }, { pos: new Vector(0, i), direction: enumDirection.left, filter: "shape" }, { pos: new Vector(3, i), direction: enumDirection.right, filter: "shape" });
|
||||
}
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots,
|
||||
}));
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { ItemProducerComponent } from "../components/item_producer";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
export class MetaItemProducerBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("item_producer");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 61,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#b37dcd";
|
||||
}
|
||||
getShowWiresLayerPreview(): any {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new ItemEjectorComponent({
|
||||
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
|
||||
}));
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
direction: enumDirection.bottom,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new ItemProducerComponent({}));
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { LeverComponent } from "../components/lever";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
export class MetaLeverBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("lever");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 33,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
// @todo: Render differently based on if its activated or not
|
||||
return "#1a678b";
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers);
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
getSprite(): any {
|
||||
return null;
|
||||
}
|
||||
getShowWiresLayerPreview(): any {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new LeverComponent({}));
|
||||
}
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { MetaBuilding, defaultBuildingVariant } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
|
||||
import { generateMatrixRotations } from "../../core/utils";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
/** @enum {string} */
|
||||
export const enumLogicGateVariants: any = {
|
||||
not: "not",
|
||||
xor: "xor",
|
||||
or: "or",
|
||||
};
|
||||
/** @enum {string} */
|
||||
const enumVariantToGate: any = {
|
||||
[defaultBuildingVariant]: enumLogicGateType.and,
|
||||
[enumLogicGateVariants.not]: enumLogicGateType.not,
|
||||
[enumLogicGateVariants.xor]: enumLogicGateType.xor,
|
||||
[enumLogicGateVariants.or]: enumLogicGateType.or,
|
||||
};
|
||||
const overlayMatrices: any = {
|
||||
[defaultBuildingVariant]: generateMatrixRotations([0, 1, 0, 1, 1, 1, 0, 1, 1]),
|
||||
[enumLogicGateVariants.xor]: generateMatrixRotations([0, 1, 0, 1, 1, 1, 0, 1, 1]),
|
||||
[enumLogicGateVariants.or]: generateMatrixRotations([0, 1, 0, 1, 1, 1, 0, 1, 1]),
|
||||
[enumLogicGateVariants.not]: generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]),
|
||||
};
|
||||
const colors: any = {
|
||||
[defaultBuildingVariant]: "#f48d41",
|
||||
[enumLogicGateVariants.xor]: "#f4a241",
|
||||
[enumLogicGateVariants.or]: "#f4d041",
|
||||
[enumLogicGateVariants.not]: "#f44184",
|
||||
};
|
||||
export class MetaLogicGateBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("logic_gate");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 32,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
{
|
||||
internalId: 34,
|
||||
variant: enumLogicGateVariants.not,
|
||||
},
|
||||
{
|
||||
internalId: 35,
|
||||
variant: enumLogicGateVariants.xor,
|
||||
},
|
||||
{
|
||||
internalId: 36,
|
||||
variant: enumLogicGateVariants.or,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(variant: any): any {
|
||||
return colors[variant];
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_logic_gates);
|
||||
}
|
||||
/** {} **/
|
||||
getLayer(): "wires" {
|
||||
return "wires";
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
getSpecialOverlayRenderMatrix(rotation: any, rotationVariant: any, variant: any): any {
|
||||
return overlayMatrices[variant][rotation];
|
||||
}
|
||||
getAvailableVariants(): any {
|
||||
return [
|
||||
defaultBuildingVariant,
|
||||
enumLogicGateVariants.or,
|
||||
enumLogicGateVariants.not,
|
||||
enumLogicGateVariants.xor,
|
||||
];
|
||||
}
|
||||
getRenderPins(): any {
|
||||
// We already have it included
|
||||
return false;
|
||||
}
|
||||
updateVariants(entity: Entity, rotationVariant: number, variant: any): any {
|
||||
const gateType: any = enumVariantToGate[variant];
|
||||
entity.components.LogicGate.type = gateType;
|
||||
const pinComp: any = entity.components.WiredPins;
|
||||
switch (gateType) {
|
||||
case enumLogicGateType.and:
|
||||
case enumLogicGateType.xor:
|
||||
case enumLogicGateType.or: {
|
||||
pinComp.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.right,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case enumLogicGateType.not: {
|
||||
pinComp.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
]);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertAlways("unknown logic gate type: " + gateType);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [],
|
||||
}));
|
||||
entity.addComponent(new LogicGateComponent({}));
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { MinerComponent } from "../components/miner";
|
||||
import { Entity } from "../entity";
|
||||
import { MetaBuilding, defaultBuildingVariant } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
import { T } from "../../translations";
|
||||
import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils";
|
||||
/** @enum {string} */
|
||||
export const enumMinerVariants: any = { chainable: "chainable" };
|
||||
const overlayMatrix: any = {
|
||||
[defaultBuildingVariant]: generateMatrixRotations([1, 1, 1, 1, 0, 1, 1, 1, 1]),
|
||||
[enumMinerVariants.chainable]: generateMatrixRotations([0, 1, 0, 1, 1, 1, 1, 1, 1]),
|
||||
};
|
||||
export class MetaMinerBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("miner");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 7,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
{
|
||||
internalId: 8,
|
||||
variant: enumMinerVariants.chainable,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#b37dcd";
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
|
||||
string,
|
||||
string
|
||||
]> {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const speed: any = root.hubGoals.getMinerBaseSpeed();
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
getAvailableVariants(root: GameRoot): any {
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_miner_chainable)) {
|
||||
return [enumMinerVariants.chainable];
|
||||
}
|
||||
return super.getAvailableVariants(root);
|
||||
}
|
||||
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): any {
|
||||
return overlayMatrix[variant][rotation];
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new MinerComponent({}));
|
||||
entity.addComponent(new ItemEjectorComponent({
|
||||
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
|
||||
}));
|
||||
}
|
||||
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
|
||||
entity.components.Miner.chainable = variant === enumMinerVariants.chainable;
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import { formatItemsPerSecond } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { T } from "../../translations";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
export class MetaMixerBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("mixer");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 15,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(2, 1);
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#cdbb7d";
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_mixer);
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
|
||||
string,
|
||||
string
|
||||
]> {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.mixer);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new ItemProcessorComponent({
|
||||
inputsPerCharge: 2,
|
||||
processorType: enumItemProcessorTypes.mixer,
|
||||
}));
|
||||
entity.addComponent(new ItemEjectorComponent({
|
||||
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
|
||||
}));
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
filter: "color",
|
||||
},
|
||||
{
|
||||
pos: new Vector(1, 0),
|
||||
direction: enumDirection.bottom,
|
||||
filter: "color",
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
@ -0,0 +1,243 @@
|
||||
import { formatItemsPerSecond } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { T } from "../../translations";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent, enumItemProcessorRequirements, } from "../components/item_processor";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins";
|
||||
/** @enum {string} */
|
||||
export const enumPainterVariants: any = { mirrored: "mirrored", double: "double", quad: "quad" };
|
||||
export class MetaPainterBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("painter");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 16,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
{
|
||||
internalId: 17,
|
||||
variant: enumPainterVariants.mirrored,
|
||||
},
|
||||
{
|
||||
internalId: 18,
|
||||
variant: enumPainterVariants.double,
|
||||
},
|
||||
{
|
||||
internalId: 19,
|
||||
variant: enumPainterVariants.quad,
|
||||
},
|
||||
];
|
||||
}
|
||||
getDimensions(variant: any): any {
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant:
|
||||
case enumPainterVariants.mirrored:
|
||||
return new Vector(2, 1);
|
||||
case enumPainterVariants.double:
|
||||
return new Vector(2, 2);
|
||||
case enumPainterVariants.quad:
|
||||
return new Vector(4, 1);
|
||||
default:
|
||||
assertAlways(false, "Unknown painter variant: " + variant);
|
||||
}
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#cd9b7d";
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
|
||||
string,
|
||||
string
|
||||
]> {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant:
|
||||
case enumPainterVariants.mirrored: {
|
||||
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.painter);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
case enumPainterVariants.double: {
|
||||
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.painterDouble);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed, true)]];
|
||||
}
|
||||
case enumPainterVariants.quad: {
|
||||
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.painterQuad);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
}
|
||||
}
|
||||
getAvailableVariants(root: GameRoot): any {
|
||||
let variants: any = [defaultBuildingVariant, enumPainterVariants.mirrored];
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_double)) {
|
||||
variants.push(enumPainterVariants.double);
|
||||
}
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers) &&
|
||||
root.gameMode.getSupportsWires()) {
|
||||
variants.push(enumPainterVariants.quad);
|
||||
}
|
||||
return variants;
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter);
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new ItemProcessorComponent({}));
|
||||
entity.addComponent(new ItemEjectorComponent({
|
||||
slots: [{ pos: new Vector(1, 0), direction: enumDirection.right }],
|
||||
}));
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
filter: "shape",
|
||||
},
|
||||
{
|
||||
pos: new Vector(1, 0),
|
||||
direction: enumDirection.top,
|
||||
filter: "color",
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant:
|
||||
case enumPainterVariants.mirrored: {
|
||||
// REGULAR PAINTER
|
||||
if (entity.components.WiredPins) {
|
||||
entity.removeComponent(WiredPinsComponent);
|
||||
}
|
||||
entity.components.ItemAcceptor.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
filter: "shape",
|
||||
},
|
||||
{
|
||||
pos: new Vector(1, 0),
|
||||
direction: variant === defaultBuildingVariant ? enumDirection.top : enumDirection.bottom,
|
||||
filter: "color",
|
||||
},
|
||||
]);
|
||||
entity.components.ItemEjector.setSlots([
|
||||
{ pos: new Vector(1, 0), direction: enumDirection.right },
|
||||
]);
|
||||
entity.components.ItemProcessor.type = enumItemProcessorTypes.painter;
|
||||
entity.components.ItemProcessor.processingRequirement = null;
|
||||
entity.components.ItemProcessor.inputsPerCharge = 2;
|
||||
break;
|
||||
}
|
||||
case enumPainterVariants.double: {
|
||||
// DOUBLE PAINTER
|
||||
if (entity.components.WiredPins) {
|
||||
entity.removeComponent(WiredPinsComponent);
|
||||
}
|
||||
entity.components.ItemAcceptor.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
filter: "shape",
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 1),
|
||||
direction: enumDirection.left,
|
||||
filter: "shape",
|
||||
},
|
||||
{
|
||||
pos: new Vector(1, 0),
|
||||
direction: enumDirection.top,
|
||||
filter: "color",
|
||||
},
|
||||
]);
|
||||
entity.components.ItemEjector.setSlots([
|
||||
{ pos: new Vector(1, 0), direction: enumDirection.right },
|
||||
]);
|
||||
entity.components.ItemProcessor.type = enumItemProcessorTypes.painterDouble;
|
||||
entity.components.ItemProcessor.processingRequirement = null;
|
||||
entity.components.ItemProcessor.inputsPerCharge = 3;
|
||||
break;
|
||||
}
|
||||
case enumPainterVariants.quad: {
|
||||
// QUAD PAINTER
|
||||
if (!entity.components.WiredPins) {
|
||||
entity.addComponent(new WiredPinsComponent({ slots: [] }));
|
||||
}
|
||||
entity.components.WiredPins.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
{
|
||||
pos: new Vector(1, 0),
|
||||
direction: enumDirection.bottom,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
{
|
||||
pos: new Vector(2, 0),
|
||||
direction: enumDirection.bottom,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
{
|
||||
pos: new Vector(3, 0),
|
||||
direction: enumDirection.bottom,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
]);
|
||||
entity.components.ItemAcceptor.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
filter: "shape",
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
filter: "color",
|
||||
},
|
||||
{
|
||||
pos: new Vector(1, 0),
|
||||
direction: enumDirection.bottom,
|
||||
filter: "color",
|
||||
},
|
||||
{
|
||||
pos: new Vector(2, 0),
|
||||
direction: enumDirection.bottom,
|
||||
filter: "color",
|
||||
},
|
||||
{
|
||||
pos: new Vector(3, 0),
|
||||
direction: enumDirection.bottom,
|
||||
filter: "color",
|
||||
},
|
||||
]);
|
||||
entity.components.ItemEjector.setSlots([
|
||||
{ pos: new Vector(0, 0), direction: enumDirection.top },
|
||||
]);
|
||||
entity.components.ItemProcessor.type = enumItemProcessorTypes.painterQuad;
|
||||
entity.components.ItemProcessor.processingRequirement =
|
||||
enumItemProcessorRequirements.painterQuad;
|
||||
entity.components.ItemProcessor.inputsPerCharge = 5;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertAlways(false, "Unknown painter variant: " + variant);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { BeltUnderlaysComponent } from "../components/belt_underlays";
|
||||
import { BeltReaderComponent } from "../components/belt_reader";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
import { generateMatrixRotations } from "../../core/utils";
|
||||
const overlayMatrix: any = generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]);
|
||||
export class MetaReaderBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("reader");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 49,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#25fff2";
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_belt_reader);
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
getShowWiresLayerPreview(): any {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): Array<number> | null {
|
||||
return overlayMatrix[rotation];
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.right,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new ItemEjectorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new ItemProcessorComponent({
|
||||
processorType: enumItemProcessorTypes.reader,
|
||||
inputsPerCharge: 1,
|
||||
}));
|
||||
entity.addComponent(new BeltUnderlaysComponent({
|
||||
underlays: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new BeltReaderComponent());
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { T } from "../../translations";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
/** @enum {string} */
|
||||
export const enumRotaterVariants: any = { ccw: "ccw", rotate180: "rotate180" };
|
||||
const overlayMatrices: any = {
|
||||
[defaultBuildingVariant]: generateMatrixRotations([0, 1, 1, 1, 1, 0, 0, 1, 1]),
|
||||
[enumRotaterVariants.ccw]: generateMatrixRotations([1, 1, 0, 0, 1, 1, 1, 1, 0]),
|
||||
[enumRotaterVariants.rotate180]: generateMatrixRotations([1, 1, 0, 1, 1, 1, 0, 1, 1]),
|
||||
};
|
||||
export class MetaRotaterBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("rotater");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 11,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
{
|
||||
internalId: 12,
|
||||
variant: enumRotaterVariants.ccw,
|
||||
},
|
||||
{
|
||||
internalId: 13,
|
||||
variant: enumRotaterVariants.rotate180,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#7dc6cd";
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): Array<number> | null {
|
||||
const matrix: any = overlayMatrices[variant];
|
||||
if (matrix) {
|
||||
return matrix[rotation];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
|
||||
string,
|
||||
string
|
||||
]> {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant: {
|
||||
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotater);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
case enumRotaterVariants.ccw: {
|
||||
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotaterCCW);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
case enumRotaterVariants.rotate180: {
|
||||
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotater180);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
}
|
||||
}
|
||||
getAvailableVariants(root: GameRoot): any {
|
||||
let variants: any = [defaultBuildingVariant];
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater_ccw)) {
|
||||
variants.push(enumRotaterVariants.ccw);
|
||||
}
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater_180)) {
|
||||
variants.push(enumRotaterVariants.rotate180);
|
||||
}
|
||||
return variants;
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater);
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new ItemProcessorComponent({
|
||||
inputsPerCharge: 1,
|
||||
processorType: enumItemProcessorTypes.rotater,
|
||||
}));
|
||||
entity.addComponent(new ItemEjectorComponent({
|
||||
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
|
||||
}));
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
filter: "shape",
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant: {
|
||||
entity.components.ItemProcessor.type = enumItemProcessorTypes.rotater;
|
||||
break;
|
||||
}
|
||||
case enumRotaterVariants.ccw: {
|
||||
entity.components.ItemProcessor.type = enumItemProcessorTypes.rotaterCCW;
|
||||
break;
|
||||
}
|
||||
case enumRotaterVariants.rotate180: {
|
||||
entity.components.ItemProcessor.type = enumItemProcessorTypes.rotater180;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertAlways(false, "Unknown rotater variant: " + variant);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import { formatItemsPerSecond } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { T } from "../../translations";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
export class MetaStackerBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("stacker");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 14,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#9fcd7d";
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(2, 1);
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
|
||||
string,
|
||||
string
|
||||
]> {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const speed: any = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.stacker);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_stacker);
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new ItemProcessorComponent({
|
||||
inputsPerCharge: 2,
|
||||
processorType: enumItemProcessorTypes.stacker,
|
||||
}));
|
||||
entity.addComponent(new ItemEjectorComponent({
|
||||
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
|
||||
}));
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
filter: "shape",
|
||||
},
|
||||
{
|
||||
pos: new Vector(1, 0),
|
||||
direction: enumDirection.bottom,
|
||||
filter: "shape",
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import { formatBigNumber } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { T } from "../../translations";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { StorageComponent } from "../components/storage";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
const storageSize: any = 5000;
|
||||
export class MetaStorageBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("storage");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 21,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#bbdf6d";
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getAdditionalStatistics(root: any, variant: any): Array<[
|
||||
string,
|
||||
string
|
||||
]> {
|
||||
return [[T.ingame.buildingPlacement.infoTexts.storage, formatBigNumber(storageSize)]];
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(2, 2);
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_storage);
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
// Required, since the item processor needs this.
|
||||
entity.addComponent(new ItemEjectorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
},
|
||||
{
|
||||
pos: new Vector(1, 0),
|
||||
direction: enumDirection.top,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 1),
|
||||
direction: enumDirection.bottom,
|
||||
},
|
||||
{
|
||||
pos: new Vector(1, 1),
|
||||
direction: enumDirection.bottom,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new StorageComponent({
|
||||
maximumStorage: storageSize,
|
||||
}));
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(1, 1),
|
||||
direction: enumDirection.right,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 1),
|
||||
direction: enumDirection.left,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
import { generateMatrixRotations } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
/** @enum {string} */
|
||||
export const enumTransistorVariants: any = {
|
||||
mirrored: "mirrored",
|
||||
};
|
||||
const overlayMatrices: any = {
|
||||
[defaultBuildingVariant]: generateMatrixRotations([0, 1, 0, 1, 1, 0, 0, 1, 0]),
|
||||
[enumTransistorVariants.mirrored]: generateMatrixRotations([0, 1, 0, 0, 1, 1, 0, 1, 0]),
|
||||
};
|
||||
export class MetaTransistorBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("transistor");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 38,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
{
|
||||
internalId: 60,
|
||||
variant: enumTransistorVariants.mirrored,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#bc3a61";
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_logic_gates);
|
||||
}
|
||||
/** {} **/
|
||||
getLayer(): "wires" {
|
||||
return "wires";
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
getAvailableVariants(): any {
|
||||
return [defaultBuildingVariant, enumTransistorVariants.mirrored];
|
||||
}
|
||||
getSpecialOverlayRenderMatrix(rotation: any, rotationVariant: any, variant: any): any {
|
||||
return overlayMatrices[variant][rotation];
|
||||
}
|
||||
getRenderPins(): any {
|
||||
// We already have it included
|
||||
return false;
|
||||
}
|
||||
updateVariants(entity: Entity, rotationVariant: number, variant: any): any {
|
||||
entity.components.WiredPins.slots[1].direction =
|
||||
variant === enumTransistorVariants.mirrored ? enumDirection.right : enumDirection.left;
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new LogicGateComponent({
|
||||
type: enumLogicGateType.transistor,
|
||||
}));
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
import { generateMatrixRotations } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
const overlayMatrix: any = generateMatrixRotations([1, 1, 0, 1, 1, 1, 0, 1, 1]);
|
||||
export class MetaTrashBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("trash");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 20,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getIsRotateable(): any {
|
||||
return false;
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#ed1d5d";
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
getSpecialOverlayRenderMatrix(rotation: any): any {
|
||||
return overlayMatrix[rotation];
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_and_trash);
|
||||
}
|
||||
addAchievementReceiver(entity: any): any {
|
||||
if (!entity.root) {
|
||||
return;
|
||||
}
|
||||
const itemProcessor: any = entity.components.ItemProcessor;
|
||||
const tryTakeItem: any = itemProcessor.tryTakeItem.bind(itemProcessor);
|
||||
itemProcessor.tryTakeItem = (): any => {
|
||||
const taken: any = tryTakeItem(...arguments);
|
||||
if (taken) {
|
||||
entity.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.trash1000, 1);
|
||||
}
|
||||
return taken;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.right,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
},
|
||||
],
|
||||
}));
|
||||
entity.addComponent(new ItemProcessorComponent({
|
||||
inputsPerCharge: 1,
|
||||
processorType: enumItemProcessorTypes.trash,
|
||||
}));
|
||||
this.addAchievementReceiver(entity);
|
||||
}
|
||||
}
|
@ -0,0 +1,237 @@
|
||||
import { Loader } from "../../core/loader";
|
||||
import { enumDirection, Vector, enumAngleToDirection, enumDirectionToVector } from "../../core/vector";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { enumUndergroundBeltMode, UndergroundBeltComponent } from "../components/underground_belt";
|
||||
import { Entity } from "../entity";
|
||||
import { MetaBuilding, defaultBuildingVariant } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils";
|
||||
import { T } from "../../translations";
|
||||
/** @enum {string} */
|
||||
export const arrayUndergroundRotationVariantToMode: any = [
|
||||
enumUndergroundBeltMode.sender,
|
||||
enumUndergroundBeltMode.receiver,
|
||||
];
|
||||
/** @enum {string} */
|
||||
export const enumUndergroundBeltVariants: any = { tier2: "tier2" };
|
||||
export const enumUndergroundBeltVariantToTier: any = {
|
||||
[defaultBuildingVariant]: 0,
|
||||
[enumUndergroundBeltVariants.tier2]: 1,
|
||||
};
|
||||
const colorsByRotationVariant: any = ["#6d9dff", "#71ff9c"];
|
||||
const overlayMatrices: any = [
|
||||
// Sender
|
||||
generateMatrixRotations([1, 1, 1, 0, 1, 0, 0, 1, 0]),
|
||||
// Receiver
|
||||
generateMatrixRotations([0, 1, 0, 0, 1, 0, 1, 1, 1]),
|
||||
];
|
||||
export class MetaUndergroundBeltBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("underground_belt");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 22,
|
||||
variant: defaultBuildingVariant,
|
||||
rotationVariant: 0,
|
||||
},
|
||||
{
|
||||
internalId: 23,
|
||||
variant: defaultBuildingVariant,
|
||||
rotationVariant: 1,
|
||||
},
|
||||
{
|
||||
internalId: 24,
|
||||
variant: enumUndergroundBeltVariants.tier2,
|
||||
rotationVariant: 0,
|
||||
},
|
||||
{
|
||||
internalId: 25,
|
||||
variant: enumUndergroundBeltVariants.tier2,
|
||||
rotationVariant: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(variant: any, rotationVariant: any): any {
|
||||
return colorsByRotationVariant[rotationVariant];
|
||||
}
|
||||
getFlipOrientationAfterPlacement(): any {
|
||||
return true;
|
||||
}
|
||||
getStayInPlacementMode(): any {
|
||||
return true;
|
||||
}
|
||||
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): any {
|
||||
return overlayMatrices[rotationVariant][rotation];
|
||||
}
|
||||
/**
|
||||
* {}
|
||||
*/
|
||||
getAdditionalStatistics(root: GameRoot, variant: string): Array<[
|
||||
string,
|
||||
string
|
||||
]> {
|
||||
const rangeTiles: any = globalConfig.undergroundBeltMaxTilesByTier[enumUndergroundBeltVariantToTier[variant]];
|
||||
const beltSpeed: any = root.hubGoals.getUndergroundBeltBaseSpeed();
|
||||
const stats: Array<[
|
||||
string,
|
||||
string
|
||||
]> = [
|
||||
[
|
||||
T.ingame.buildingPlacement.infoTexts.range,
|
||||
T.ingame.buildingPlacement.infoTexts.tiles.replace("<x>", "" + rangeTiles),
|
||||
],
|
||||
];
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return stats;
|
||||
}
|
||||
stats.push([T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]);
|
||||
return stats;
|
||||
}
|
||||
getAvailableVariants(root: GameRoot): any {
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_underground_belt_tier_2)) {
|
||||
return [defaultBuildingVariant, enumUndergroundBeltVariants.tier2];
|
||||
}
|
||||
return super.getAvailableVariants(root);
|
||||
}
|
||||
getPreviewSprite(rotationVariant: number, variant: string): any {
|
||||
let suffix: any = "";
|
||||
if (variant !== defaultBuildingVariant) {
|
||||
suffix = "-" + variant;
|
||||
}
|
||||
switch (arrayUndergroundRotationVariantToMode[rotationVariant]) {
|
||||
case enumUndergroundBeltMode.sender:
|
||||
return Loader.getSprite("sprites/buildings/underground_belt_entry" + suffix + ".png");
|
||||
case enumUndergroundBeltMode.receiver:
|
||||
return Loader.getSprite("sprites/buildings/underground_belt_exit" + suffix + ".png");
|
||||
default:
|
||||
assertAlways(false, "Invalid rotation variant");
|
||||
}
|
||||
}
|
||||
getBlueprintSprite(rotationVariant: number, variant: string): any {
|
||||
let suffix: any = "";
|
||||
if (variant !== defaultBuildingVariant) {
|
||||
suffix = "-" + variant;
|
||||
}
|
||||
switch (arrayUndergroundRotationVariantToMode[rotationVariant]) {
|
||||
case enumUndergroundBeltMode.sender:
|
||||
return Loader.getSprite("sprites/blueprints/underground_belt_entry" + suffix + ".png");
|
||||
case enumUndergroundBeltMode.receiver:
|
||||
return Loader.getSprite("sprites/blueprints/underground_belt_exit" + suffix + ".png");
|
||||
default:
|
||||
assertAlways(false, "Invalid rotation variant");
|
||||
}
|
||||
}
|
||||
getSprite(rotationVariant: number, variant: string): any {
|
||||
return this.getPreviewSprite(rotationVariant, variant);
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_tunnel);
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
// Required, since the item processor needs this.
|
||||
entity.addComponent(new ItemEjectorComponent({
|
||||
slots: [],
|
||||
}));
|
||||
entity.addComponent(new UndergroundBeltComponent({}));
|
||||
entity.addComponent(new ItemAcceptorComponent({
|
||||
slots: [],
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Should compute the optimal rotation variant on the given tile
|
||||
* @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array<Entity> }}
|
||||
*/
|
||||
computeOptimalDirectionAndRotationVariantAtTile({ root, tile, rotation, variant, layer }: {
|
||||
root: GameRoot;
|
||||
tile: Vector;
|
||||
rotation: number;
|
||||
variant: string;
|
||||
layer: Layer;
|
||||
}): {
|
||||
rotation: number;
|
||||
rotationVariant: number;
|
||||
connectedEntities?: Array<Entity>;
|
||||
} {
|
||||
const searchDirection: any = enumAngleToDirection[rotation];
|
||||
const searchVector: any = enumDirectionToVector[searchDirection];
|
||||
const tier: any = enumUndergroundBeltVariantToTier[variant];
|
||||
const targetRotation: any = (rotation + 180) % 360;
|
||||
const targetSenderRotation: any = rotation;
|
||||
for (let searchOffset: any = 1; searchOffset <= globalConfig.undergroundBeltMaxTilesByTier[tier]; ++searchOffset) {
|
||||
tile = tile.addScalars(searchVector.x, searchVector.y);
|
||||
const contents: any = root.map.getTileContent(tile, "regular");
|
||||
if (contents) {
|
||||
const undergroundComp: any = contents.components.UndergroundBelt;
|
||||
if (undergroundComp && undergroundComp.tier === tier) {
|
||||
const staticComp: any = contents.components.StaticMapEntity;
|
||||
if (staticComp.rotation === targetRotation) {
|
||||
if (undergroundComp.mode !== enumUndergroundBeltMode.sender) {
|
||||
// If we encounter an underground receiver on our way which is also faced in our direction, we don't accept that
|
||||
break;
|
||||
}
|
||||
return {
|
||||
rotation: targetRotation,
|
||||
rotationVariant: 1,
|
||||
connectedEntities: [contents],
|
||||
};
|
||||
}
|
||||
else if (staticComp.rotation === targetSenderRotation) {
|
||||
// Draw connections to receivers
|
||||
if (undergroundComp.mode === enumUndergroundBeltMode.receiver) {
|
||||
return {
|
||||
rotation: rotation,
|
||||
rotationVariant: 0,
|
||||
connectedEntities: [contents],
|
||||
};
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
rotation,
|
||||
rotationVariant: 0,
|
||||
};
|
||||
}
|
||||
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
|
||||
entity.components.UndergroundBelt.tier = enumUndergroundBeltVariantToTier[variant];
|
||||
switch (arrayUndergroundRotationVariantToMode[rotationVariant]) {
|
||||
case enumUndergroundBeltMode.sender: {
|
||||
entity.components.UndergroundBelt.mode = enumUndergroundBeltMode.sender;
|
||||
entity.components.ItemEjector.setSlots([]);
|
||||
entity.components.ItemAcceptor.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
case enumUndergroundBeltMode.receiver: {
|
||||
entity.components.UndergroundBelt.mode = enumUndergroundBeltMode.receiver;
|
||||
entity.components.ItemAcceptor.setSlots([]);
|
||||
entity.components.ItemEjector.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
assertAlways(false, "Invalid rotation variant");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
import { Vector, enumDirection } from "../../core/vector";
|
||||
import { LogicGateComponent, enumLogicGateType } from "../components/logic_gate";
|
||||
import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
import { MetaCutterBuilding } from "./cutter";
|
||||
import { MetaPainterBuilding } from "./painter";
|
||||
import { MetaRotaterBuilding } from "./rotater";
|
||||
import { MetaStackerBuilding } from "./stacker";
|
||||
/** @enum {string} */
|
||||
export const enumVirtualProcessorVariants: any = {
|
||||
rotater: "rotater",
|
||||
unstacker: "unstacker",
|
||||
stacker: "stacker",
|
||||
painter: "painter",
|
||||
};
|
||||
/** @enum {string} */
|
||||
const enumVariantToGate: any = {
|
||||
[defaultBuildingVariant]: enumLogicGateType.cutter,
|
||||
[enumVirtualProcessorVariants.rotater]: enumLogicGateType.rotater,
|
||||
[enumVirtualProcessorVariants.unstacker]: enumLogicGateType.unstacker,
|
||||
[enumVirtualProcessorVariants.stacker]: enumLogicGateType.stacker,
|
||||
[enumVirtualProcessorVariants.painter]: enumLogicGateType.painter,
|
||||
};
|
||||
const colors: any = {
|
||||
[defaultBuildingVariant]: new MetaCutterBuilding().getSilhouetteColor(),
|
||||
[enumVirtualProcessorVariants.rotater]: new MetaRotaterBuilding().getSilhouetteColor(),
|
||||
[enumVirtualProcessorVariants.unstacker]: new MetaStackerBuilding().getSilhouetteColor(),
|
||||
[enumVirtualProcessorVariants.stacker]: new MetaStackerBuilding().getSilhouetteColor(),
|
||||
[enumVirtualProcessorVariants.painter]: new MetaPainterBuilding().getSilhouetteColor(),
|
||||
};
|
||||
export class MetaVirtualProcessorBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("virtual_processor");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 42,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
{
|
||||
internalId: 44,
|
||||
variant: enumVirtualProcessorVariants.rotater,
|
||||
},
|
||||
{
|
||||
internalId: 45,
|
||||
variant: enumVirtualProcessorVariants.unstacker,
|
||||
},
|
||||
{
|
||||
internalId: 50,
|
||||
variant: enumVirtualProcessorVariants.stacker,
|
||||
},
|
||||
{
|
||||
internalId: 51,
|
||||
variant: enumVirtualProcessorVariants.painter,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(variant: any): any {
|
||||
return colors[variant];
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_virtual_processing);
|
||||
}
|
||||
/** {} **/
|
||||
getLayer(): "wires" {
|
||||
return "wires";
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
getAvailableVariants(): any {
|
||||
return [
|
||||
defaultBuildingVariant,
|
||||
enumVirtualProcessorVariants.rotater,
|
||||
enumVirtualProcessorVariants.stacker,
|
||||
enumVirtualProcessorVariants.painter,
|
||||
enumVirtualProcessorVariants.unstacker,
|
||||
];
|
||||
}
|
||||
getRenderPins(): any {
|
||||
// We already have it included
|
||||
return false;
|
||||
}
|
||||
updateVariants(entity: Entity, rotationVariant: number, variant: any): any {
|
||||
const gateType: any = enumVariantToGate[variant];
|
||||
entity.components.LogicGate.type = gateType;
|
||||
const pinComp: any = entity.components.WiredPins;
|
||||
switch (gateType) {
|
||||
case enumLogicGateType.cutter:
|
||||
case enumLogicGateType.unstacker: {
|
||||
pinComp.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.right,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case enumLogicGateType.rotater: {
|
||||
pinComp.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case enumLogicGateType.stacker:
|
||||
case enumLogicGateType.painter: {
|
||||
pinComp.setSlots([
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.right,
|
||||
type: enumPinSlotType.logicalAcceptor,
|
||||
},
|
||||
]);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertAlways("unknown logic gate type: " + gateType);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new WiredPinsComponent({
|
||||
slots: [],
|
||||
}));
|
||||
entity.addComponent(new LogicGateComponent({}));
|
||||
}
|
||||
}
|
@ -0,0 +1,258 @@
|
||||
import { Loader } from "../../core/loader";
|
||||
import { generateMatrixRotations } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { SOUNDS } from "../../platform/sound";
|
||||
import { enumWireType, enumWireVariant, WireComponent } from "../components/wire";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
export const arrayWireRotationVariantToType: any = [
|
||||
enumWireType.forward,
|
||||
enumWireType.turn,
|
||||
enumWireType.split,
|
||||
enumWireType.cross,
|
||||
];
|
||||
export const wireOverlayMatrices: any = {
|
||||
[enumWireType.forward]: generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]),
|
||||
[enumWireType.split]: generateMatrixRotations([0, 0, 0, 1, 1, 1, 0, 1, 0]),
|
||||
[enumWireType.turn]: generateMatrixRotations([0, 0, 0, 0, 1, 1, 0, 1, 0]),
|
||||
[enumWireType.cross]: generateMatrixRotations([0, 1, 0, 1, 1, 1, 0, 1, 0]),
|
||||
};
|
||||
/** @enum {string} */
|
||||
export const wireVariants: any = {
|
||||
second: "second",
|
||||
};
|
||||
const enumWireVariantToVariant: any = {
|
||||
[defaultBuildingVariant]: enumWireVariant.first,
|
||||
[wireVariants.second]: enumWireVariant.second,
|
||||
};
|
||||
export class MetaWireBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("wire");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 27,
|
||||
variant: defaultBuildingVariant,
|
||||
rotationVariant: 0,
|
||||
},
|
||||
{
|
||||
internalId: 28,
|
||||
variant: defaultBuildingVariant,
|
||||
rotationVariant: 1,
|
||||
},
|
||||
{
|
||||
internalId: 29,
|
||||
variant: defaultBuildingVariant,
|
||||
rotationVariant: 2,
|
||||
},
|
||||
{
|
||||
internalId: 30,
|
||||
variant: defaultBuildingVariant,
|
||||
rotationVariant: 3,
|
||||
},
|
||||
{
|
||||
internalId: 52,
|
||||
variant: enumWireVariant.second,
|
||||
rotationVariant: 0,
|
||||
},
|
||||
{
|
||||
internalId: 53,
|
||||
variant: enumWireVariant.second,
|
||||
rotationVariant: 1,
|
||||
},
|
||||
{
|
||||
internalId: 54,
|
||||
variant: enumWireVariant.second,
|
||||
rotationVariant: 2,
|
||||
},
|
||||
{
|
||||
internalId: 55,
|
||||
variant: enumWireVariant.second,
|
||||
rotationVariant: 3,
|
||||
},
|
||||
];
|
||||
}
|
||||
getHasDirectionLockAvailable(): any {
|
||||
return true;
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#61ef6f";
|
||||
}
|
||||
getAvailableVariants(): any {
|
||||
return [defaultBuildingVariant, wireVariants.second];
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
getStayInPlacementMode(): any {
|
||||
return true;
|
||||
}
|
||||
getPlacementSound(): any {
|
||||
return SOUNDS.placeBelt;
|
||||
}
|
||||
getRotateAutomaticallyWhilePlacing(): any {
|
||||
return true;
|
||||
}
|
||||
/** {} **/
|
||||
getLayer(): "wires" {
|
||||
return "wires";
|
||||
}
|
||||
getSprite(): any {
|
||||
return null;
|
||||
}
|
||||
getIsReplaceable(): any {
|
||||
return true;
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers);
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new WireComponent({}));
|
||||
}
|
||||
updateVariants(entity: Entity, rotationVariant: number, variant: string): any {
|
||||
entity.components.Wire.type = arrayWireRotationVariantToType[rotationVariant];
|
||||
entity.components.Wire.variant = enumWireVariantToVariant[variant];
|
||||
}
|
||||
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): any {
|
||||
return wireOverlayMatrices[entity.components.Wire.type][rotation];
|
||||
}
|
||||
/**
|
||||
*
|
||||
* {}
|
||||
*/
|
||||
getPreviewSprite(rotationVariant: number, variant: string): import("../../core/draw_utils").AtlasSprite {
|
||||
const wireVariant: any = enumWireVariantToVariant[variant];
|
||||
switch (arrayWireRotationVariantToType[rotationVariant]) {
|
||||
case enumWireType.forward: {
|
||||
return Loader.getSprite("sprites/wires/sets/" + wireVariant + "_forward.png");
|
||||
}
|
||||
case enumWireType.turn: {
|
||||
return Loader.getSprite("sprites/wires/sets/" + wireVariant + "_turn.png");
|
||||
}
|
||||
case enumWireType.split: {
|
||||
return Loader.getSprite("sprites/wires/sets/" + wireVariant + "_split.png");
|
||||
}
|
||||
case enumWireType.cross: {
|
||||
return Loader.getSprite("sprites/wires/sets/" + wireVariant + "_cross.png");
|
||||
}
|
||||
default: {
|
||||
assertAlways(false, "Invalid wire rotation variant");
|
||||
}
|
||||
}
|
||||
}
|
||||
getBlueprintSprite(rotationVariant: any, variant: any): any {
|
||||
return this.getPreviewSprite(rotationVariant, variant);
|
||||
}
|
||||
/**
|
||||
* Should compute the optimal rotation variant on the given tile
|
||||
* @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array<Entity> }}
|
||||
*/
|
||||
computeOptimalDirectionAndRotationVariantAtTile({ root, tile, rotation, variant, layer }: {
|
||||
root: GameRoot;
|
||||
tile: Vector;
|
||||
rotation: number;
|
||||
variant: string;
|
||||
layer: string;
|
||||
}): {
|
||||
rotation: number;
|
||||
rotationVariant: number;
|
||||
connectedEntities?: Array<Entity>;
|
||||
} {
|
||||
const wireVariant: any = enumWireVariantToVariant[variant];
|
||||
const connections: any = {
|
||||
top: root.logic.computeWireEdgeStatus({ tile, wireVariant, edge: enumDirection.top }),
|
||||
right: root.logic.computeWireEdgeStatus({ tile, wireVariant, edge: enumDirection.right }),
|
||||
bottom: root.logic.computeWireEdgeStatus({ tile, wireVariant, edge: enumDirection.bottom }),
|
||||
left: root.logic.computeWireEdgeStatus({ tile, wireVariant, edge: enumDirection.left }),
|
||||
};
|
||||
let flag: any = 0;
|
||||
flag |= connections.top ? 0x1000 : 0;
|
||||
flag |= connections.right ? 0x100 : 0;
|
||||
flag |= connections.bottom ? 0x10 : 0;
|
||||
flag |= connections.left ? 0x1 : 0;
|
||||
let targetType: any = enumWireType.forward;
|
||||
// First, reset rotation
|
||||
rotation = 0;
|
||||
switch (flag) {
|
||||
case 0x0000:
|
||||
// Nothing
|
||||
break;
|
||||
case 0x0001:
|
||||
// Left
|
||||
rotation += 90;
|
||||
break;
|
||||
case 0x0010:
|
||||
// Bottom
|
||||
// END
|
||||
break;
|
||||
case 0x0011:
|
||||
// Bottom | Left
|
||||
targetType = enumWireType.turn;
|
||||
rotation += 90;
|
||||
break;
|
||||
case 0x0100:
|
||||
// Right
|
||||
rotation += 90;
|
||||
break;
|
||||
case 0x0101:
|
||||
// Right | Left
|
||||
rotation += 90;
|
||||
break;
|
||||
case 0x0110:
|
||||
// Right | Bottom
|
||||
targetType = enumWireType.turn;
|
||||
break;
|
||||
case 0x0111:
|
||||
// Right | Bottom | Left
|
||||
targetType = enumWireType.split;
|
||||
break;
|
||||
case 0x1000:
|
||||
// Top
|
||||
break;
|
||||
case 0x1001:
|
||||
// Top | Left
|
||||
targetType = enumWireType.turn;
|
||||
rotation += 180;
|
||||
break;
|
||||
case 0x1010:
|
||||
// Top | Bottom
|
||||
break;
|
||||
case 0x1011:
|
||||
// Top | Bottom | Left
|
||||
targetType = enumWireType.split;
|
||||
rotation += 90;
|
||||
break;
|
||||
case 0x1100:
|
||||
// Top | Right
|
||||
targetType = enumWireType.turn;
|
||||
rotation -= 90;
|
||||
break;
|
||||
case 0x1101:
|
||||
// Top | Right | Left
|
||||
targetType = enumWireType.split;
|
||||
rotation += 180;
|
||||
break;
|
||||
case 0x1110:
|
||||
// Top | Right | Bottom
|
||||
targetType = enumWireType.split;
|
||||
rotation -= 90;
|
||||
break;
|
||||
case 0x1111:
|
||||
// Top | Right | Bottom | Left
|
||||
targetType = enumWireType.cross;
|
||||
break;
|
||||
}
|
||||
return {
|
||||
// Clamp rotation
|
||||
rotation: (rotation + 360 * 10) % 360,
|
||||
rotationVariant: arrayWireRotationVariantToType.indexOf(targetType),
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import { generateMatrixRotations } from "../../core/utils";
|
||||
import { Vector } from "../../core/vector";
|
||||
import { WireTunnelComponent } from "../components/wire_tunnel";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
const wireTunnelOverlayMatrix: any = generateMatrixRotations([0, 1, 0, 1, 1, 1, 0, 1, 0]);
|
||||
export class MetaWireTunnelBuilding extends MetaBuilding {
|
||||
|
||||
constructor() {
|
||||
super("wire_tunnel");
|
||||
}
|
||||
static getAllVariantCombinations(): any {
|
||||
return [
|
||||
{
|
||||
internalId: 39,
|
||||
variant: defaultBuildingVariant,
|
||||
},
|
||||
];
|
||||
}
|
||||
getSilhouetteColor(): any {
|
||||
return "#777a86";
|
||||
}
|
||||
getIsUnlocked(root: GameRoot): any {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers);
|
||||
}
|
||||
getSpecialOverlayRenderMatrix(rotation: number, rotationVariant: number, variant: string, entity: Entity): any {
|
||||
return wireTunnelOverlayMatrix[rotation];
|
||||
}
|
||||
getIsRotateable(): any {
|
||||
return false;
|
||||
}
|
||||
getDimensions(): any {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
/** {} **/
|
||||
getLayer(): "wires" {
|
||||
return "wires";
|
||||
}
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
*/
|
||||
setupEntityComponents(entity: Entity): any {
|
||||
entity.addComponent(new WireTunnelComponent());
|
||||
}
|
||||
}
|
@ -0,0 +1,825 @@
|
||||
import { clickDetectorGlobals } from "../core/click_detector";
|
||||
import { globalConfig, SUPPORT_TOUCH } from "../core/config";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { Rectangle } from "../core/rectangle";
|
||||
import { Signal, STOP_PROPAGATION } from "../core/signal";
|
||||
import { clamp } from "../core/utils";
|
||||
import { mixVector, Vector } from "../core/vector";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { KEYMAPPINGS } from "./key_action_mapper";
|
||||
import { GameRoot } from "./root";
|
||||
const logger: any = createLogger("camera");
|
||||
export const USER_INTERACT_MOVE: any = "move";
|
||||
export const USER_INTERACT_ZOOM: any = "zoom";
|
||||
export const USER_INTERACT_TOUCHEND: any = "touchend";
|
||||
const velocitySmoothing: any = 0.5;
|
||||
const velocityFade: any = 0.98;
|
||||
const velocityStrength: any = 0.4;
|
||||
const velocityMax: any = 20;
|
||||
const ticksBeforeErasingVelocity: any = 10;
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const enumMouseButton: any = {
|
||||
left: "left",
|
||||
middle: "middle",
|
||||
right: "right",
|
||||
};
|
||||
export class Camera extends BasicSerializableObject {
|
||||
public root: GameRoot = root;
|
||||
public zoomLevel = this.findInitialZoom();
|
||||
public center: Vector = new Vector(0, 0);
|
||||
public currentlyMoving = false;
|
||||
public lastMovingPosition = null;
|
||||
public lastMovingPositionLastTick = null;
|
||||
public numTicksStandingStill = null;
|
||||
public cameraUpdateTimeBucket = 0.0;
|
||||
public didMoveSinceTouchStart = false;
|
||||
public currentlyPinching = false;
|
||||
public lastPinchPositions = null;
|
||||
public keyboardForce = new Vector();
|
||||
public userInteraction = new Signal();
|
||||
public currentShake: Vector = new Vector(0, 0);
|
||||
public currentPan: Vector = new Vector(0, 0);
|
||||
public desiredPan: Vector = new Vector(0, 0);
|
||||
public desiredCenter: Vector = null;
|
||||
public desiredZoom: number = null;
|
||||
public touchPostMoveVelocity: Vector = new Vector(0, 0);
|
||||
public downPreHandler = (new Signal() as TypedSignal<[
|
||||
Vector,
|
||||
enumMouseButton
|
||||
]>);
|
||||
public movePreHandler = (new Signal() as TypedSignal<[
|
||||
Vector
|
||||
]>);
|
||||
public upPostHandler = (new Signal() as TypedSignal<[
|
||||
Vector
|
||||
]>);
|
||||
|
||||
constructor(root) {
|
||||
super();
|
||||
this.clampZoomLevel();
|
||||
this.internalInitEvents();
|
||||
this.clampZoomLevel();
|
||||
this.bindKeys();
|
||||
if (G_IS_DEV) {
|
||||
window.addEventListener("keydown", (ev: any): any => {
|
||||
if (ev.key === "i") {
|
||||
this.zoomLevel = 3;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Serialization
|
||||
static getId(): any {
|
||||
return "Camera";
|
||||
}
|
||||
static getSchema(): any {
|
||||
return {
|
||||
zoomLevel: types.float,
|
||||
center: types.vector,
|
||||
};
|
||||
}
|
||||
deserialize(data: any): any {
|
||||
const errorCode: any = super.deserialize(data);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
// Safety
|
||||
this.clampZoomLevel();
|
||||
}
|
||||
// Simple getters & setters
|
||||
addScreenShake(amount: any): any {
|
||||
const currentShakeAmount: any = this.currentShake.length();
|
||||
const scale: any = 1 / (1 + 3 * currentShakeAmount);
|
||||
this.currentShake.x = this.currentShake.x + 2 * (Math.random() - 0.5) * scale * amount;
|
||||
this.currentShake.y = this.currentShake.y + 2 * (Math.random() - 0.5) * scale * amount;
|
||||
}
|
||||
/**
|
||||
* Sets a point in world space to focus on
|
||||
*/
|
||||
setDesiredCenter(center: Vector): any {
|
||||
this.desiredCenter = center.copy();
|
||||
this.currentlyMoving = false;
|
||||
}
|
||||
/**
|
||||
* Sets a desired zoom level
|
||||
*/
|
||||
setDesiredZoom(zoom: number): any {
|
||||
this.desiredZoom = zoom;
|
||||
}
|
||||
/**
|
||||
* Returns if this camera is currently moving by a non-user interaction
|
||||
*/
|
||||
isCurrentlyMovingToDesiredCenter(): any {
|
||||
return this.desiredCenter !== null;
|
||||
}
|
||||
/**
|
||||
* Sets the camera pan, every frame the camera will move by this amount
|
||||
*/
|
||||
setPan(pan: Vector): any {
|
||||
this.desiredPan = pan.copy();
|
||||
}
|
||||
/**
|
||||
* Finds a good initial zoom level
|
||||
*/
|
||||
findInitialZoom(): any {
|
||||
let desiredWorldSpaceWidth: any = 18 * globalConfig.tileSize;
|
||||
if (window.innerWidth < 1000) {
|
||||
desiredWorldSpaceWidth = 12 * globalConfig.tileSize;
|
||||
}
|
||||
const zoomLevelX: any = this.root.gameWidth / desiredWorldSpaceWidth;
|
||||
const zoomLevelY: any = this.root.gameHeight / desiredWorldSpaceWidth;
|
||||
const finalLevel: any = Math.min(zoomLevelX, zoomLevelY);
|
||||
assert(Number.isFinite(finalLevel) && finalLevel > 0, "Invalid zoom level computed for initial zoom: " + finalLevel);
|
||||
return finalLevel;
|
||||
}
|
||||
/**
|
||||
* Clears all animations
|
||||
*/
|
||||
clearAnimations(): any {
|
||||
this.touchPostMoveVelocity.x = 0;
|
||||
this.touchPostMoveVelocity.y = 0;
|
||||
this.desiredCenter = null;
|
||||
this.desiredPan.x = 0;
|
||||
this.desiredPan.y = 0;
|
||||
this.currentPan.x = 0;
|
||||
this.currentPan.y = 0;
|
||||
this.currentlyPinching = false;
|
||||
this.currentlyMoving = false;
|
||||
this.lastMovingPosition = null;
|
||||
this.didMoveSinceTouchStart = false;
|
||||
this.desiredZoom = null;
|
||||
}
|
||||
/**
|
||||
* Returns if the user is currently interacting with the camera
|
||||
* {} true if the user interacts
|
||||
*/
|
||||
isCurrentlyInteracting(): boolean {
|
||||
if (this.currentlyPinching) {
|
||||
return true;
|
||||
}
|
||||
if (this.currentlyMoving) {
|
||||
// Only interacting if moved at least once
|
||||
return this.didMoveSinceTouchStart;
|
||||
}
|
||||
if (this.touchPostMoveVelocity.lengthSquare() > 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Returns if in the next frame the viewport will change
|
||||
* {} true if it willchange
|
||||
*/
|
||||
viewportWillChange(): boolean {
|
||||
return this.desiredCenter !== null || this.desiredZoom !== null || this.isCurrentlyInteracting();
|
||||
}
|
||||
/**
|
||||
* Cancels all interactions, that is user interaction and non user interaction
|
||||
*/
|
||||
cancelAllInteractions(): any {
|
||||
this.touchPostMoveVelocity = new Vector(0, 0);
|
||||
this.desiredCenter = null;
|
||||
this.currentlyMoving = false;
|
||||
this.currentlyPinching = false;
|
||||
this.desiredZoom = null;
|
||||
}
|
||||
/**
|
||||
* Returns effective viewport width
|
||||
*/
|
||||
getViewportWidth(): any {
|
||||
return this.root.gameWidth / this.zoomLevel;
|
||||
}
|
||||
/**
|
||||
* Returns effective viewport height
|
||||
*/
|
||||
getViewportHeight(): any {
|
||||
return this.root.gameHeight / this.zoomLevel;
|
||||
}
|
||||
/**
|
||||
* Returns effective world space viewport left
|
||||
*/
|
||||
getViewportLeft(): any {
|
||||
return this.center.x - this.getViewportWidth() / 2 + (this.currentShake.x * 10) / this.zoomLevel;
|
||||
}
|
||||
/**
|
||||
* Returns effective world space viewport right
|
||||
*/
|
||||
getViewportRight(): any {
|
||||
return this.center.x + this.getViewportWidth() / 2 + (this.currentShake.x * 10) / this.zoomLevel;
|
||||
}
|
||||
/**
|
||||
* Returns effective world space viewport top
|
||||
*/
|
||||
getViewportTop(): any {
|
||||
return this.center.y - this.getViewportHeight() / 2 + (this.currentShake.x * 10) / this.zoomLevel;
|
||||
}
|
||||
/**
|
||||
* Returns effective world space viewport bottom
|
||||
*/
|
||||
getViewportBottom(): any {
|
||||
return this.center.y + this.getViewportHeight() / 2 + (this.currentShake.x * 10) / this.zoomLevel;
|
||||
}
|
||||
/**
|
||||
* Returns the visible world space rect
|
||||
* {}
|
||||
*/
|
||||
getVisibleRect(): Rectangle {
|
||||
return Rectangle.fromTRBL(Math.floor(this.getViewportTop()), Math.ceil(this.getViewportRight()), Math.ceil(this.getViewportBottom()), Math.floor(this.getViewportLeft()));
|
||||
}
|
||||
getIsMapOverlayActive(): any {
|
||||
return this.zoomLevel < globalConfig.mapChunkOverviewMinZoom;
|
||||
}
|
||||
/**
|
||||
* Attaches all event listeners
|
||||
*/
|
||||
internalInitEvents(): any {
|
||||
this.eventListenerTouchStart = this.onTouchStart.bind(this);
|
||||
this.eventListenerTouchEnd = this.onTouchEnd.bind(this);
|
||||
this.eventListenerTouchMove = this.onTouchMove.bind(this);
|
||||
this.eventListenerMousewheel = this.onMouseWheel.bind(this);
|
||||
this.eventListenerMouseDown = this.onMouseDown.bind(this);
|
||||
this.eventListenerMouseMove = this.onMouseMove.bind(this);
|
||||
this.eventListenerMouseUp = this.onMouseUp.bind(this);
|
||||
if (SUPPORT_TOUCH) {
|
||||
this.root.canvas.addEventListener("touchstart", this.eventListenerTouchStart);
|
||||
this.root.canvas.addEventListener("touchend", this.eventListenerTouchEnd);
|
||||
this.root.canvas.addEventListener("touchcancel", this.eventListenerTouchEnd);
|
||||
this.root.canvas.addEventListener("touchmove", this.eventListenerTouchMove);
|
||||
}
|
||||
this.root.canvas.addEventListener("wheel", this.eventListenerMousewheel);
|
||||
this.root.canvas.addEventListener("mousedown", this.eventListenerMouseDown);
|
||||
this.root.canvas.addEventListener("mousemove", this.eventListenerMouseMove);
|
||||
window.addEventListener("mouseup", this.eventListenerMouseUp);
|
||||
// this.root.canvas.addEventListener("mouseout", this.eventListenerMouseUp);
|
||||
}
|
||||
/**
|
||||
* Cleans up all event listeners
|
||||
*/
|
||||
cleanup(): any {
|
||||
if (SUPPORT_TOUCH) {
|
||||
this.root.canvas.removeEventListener("touchstart", this.eventListenerTouchStart);
|
||||
this.root.canvas.removeEventListener("touchend", this.eventListenerTouchEnd);
|
||||
this.root.canvas.removeEventListener("touchcancel", this.eventListenerTouchEnd);
|
||||
this.root.canvas.removeEventListener("touchmove", this.eventListenerTouchMove);
|
||||
}
|
||||
this.root.canvas.removeEventListener("wheel", this.eventListenerMousewheel);
|
||||
this.root.canvas.removeEventListener("mousedown", this.eventListenerMouseDown);
|
||||
this.root.canvas.removeEventListener("mousemove", this.eventListenerMouseMove);
|
||||
window.removeEventListener("mouseup", this.eventListenerMouseUp);
|
||||
// this.root.canvas.removeEventListener("mouseout", this.eventListenerMouseUp);
|
||||
}
|
||||
/**
|
||||
* Binds the arrow keys
|
||||
*/
|
||||
bindKeys(): any {
|
||||
const mapper: any = this.root.keyMapper;
|
||||
mapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).add((): any => (this.keyboardForce.y = -1));
|
||||
mapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).add((): any => (this.keyboardForce.y = 1));
|
||||
mapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).add((): any => (this.keyboardForce.x = 1));
|
||||
mapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).add((): any => (this.keyboardForce.x = -1));
|
||||
mapper
|
||||
.getBinding(KEYMAPPINGS.navigation.mapZoomIn)
|
||||
.add((): any => (this.desiredZoom = this.zoomLevel * 1.2));
|
||||
mapper
|
||||
.getBinding(KEYMAPPINGS.navigation.mapZoomOut)
|
||||
.add((): any => (this.desiredZoom = this.zoomLevel / 1.2));
|
||||
mapper.getBinding(KEYMAPPINGS.navigation.centerMap).add((): any => this.centerOnMap());
|
||||
}
|
||||
centerOnMap(): any {
|
||||
this.desiredCenter = new Vector(0, 0);
|
||||
}
|
||||
/**
|
||||
* Converts from screen to world space
|
||||
* {} world space
|
||||
*/
|
||||
screenToWorld(screen: Vector): Vector {
|
||||
const centerSpace: any = screen.subScalars(this.root.gameWidth / 2, this.root.gameHeight / 2);
|
||||
return centerSpace.divideScalar(this.zoomLevel).add(this.center);
|
||||
}
|
||||
/**
|
||||
* Converts from world to screen space
|
||||
* {} screen space
|
||||
*/
|
||||
worldToScreen(world: Vector): Vector {
|
||||
const screenSpace: any = world.sub(this.center).multiplyScalar(this.zoomLevel);
|
||||
return screenSpace.addScalars(this.root.gameWidth / 2, this.root.gameHeight / 2);
|
||||
}
|
||||
/**
|
||||
* Returns if a point is on screen
|
||||
* {} true if its on screen
|
||||
*/
|
||||
isWorldPointOnScreen(point: Vector): boolean {
|
||||
const rect: any = this.getVisibleRect();
|
||||
return rect.containsPoint(point.x, point.y);
|
||||
}
|
||||
getMaximumZoom(): any {
|
||||
return this.root.gameMode.getMaximumZoom();
|
||||
}
|
||||
getMinimumZoom(): any {
|
||||
return this.root.gameMode.getMinimumZoom();
|
||||
}
|
||||
/**
|
||||
* Returns if we can further zoom in
|
||||
* {}
|
||||
*/
|
||||
canZoomIn(): boolean {
|
||||
return this.zoomLevel <= this.getMaximumZoom() - 0.01;
|
||||
}
|
||||
/**
|
||||
* Returns if we can further zoom out
|
||||
* {}
|
||||
*/
|
||||
canZoomOut(): boolean {
|
||||
return this.zoomLevel >= this.getMinimumZoom() + 0.01;
|
||||
}
|
||||
// EVENTS
|
||||
/**
|
||||
* Checks if the mouse event is too close after a touch event and thus
|
||||
* should get ignored
|
||||
*/
|
||||
checkPreventDoubleMouse(): any {
|
||||
if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Mousedown handler
|
||||
*/
|
||||
onMouseDown(event: MouseEvent): any {
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
}
|
||||
if (!this.checkPreventDoubleMouse()) {
|
||||
return;
|
||||
}
|
||||
this.touchPostMoveVelocity = new Vector(0, 0);
|
||||
if (event.button === 0) {
|
||||
this.combinedSingleTouchStartHandler(event.clientX, event.clientY);
|
||||
}
|
||||
else if (event.button === 1) {
|
||||
this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.middle);
|
||||
}
|
||||
else if (event.button === 2) {
|
||||
this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.right);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Mousemove handler
|
||||
*/
|
||||
onMouseMove(event: MouseEvent): any {
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
}
|
||||
if (!this.checkPreventDoubleMouse()) {
|
||||
return;
|
||||
}
|
||||
if (event.button === 0) {
|
||||
this.combinedSingleTouchMoveHandler(event.clientX, event.clientY);
|
||||
}
|
||||
// Clamp everything afterwards
|
||||
this.clampZoomLevel();
|
||||
this.clampToBounds();
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Mouseup handler
|
||||
*/
|
||||
onMouseUp(event: MouseEvent=): any {
|
||||
if (event) {
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
}
|
||||
}
|
||||
if (!this.checkPreventDoubleMouse()) {
|
||||
return;
|
||||
}
|
||||
this.combinedSingleTouchStopHandler(event.clientX, event.clientY);
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Mousewheel event
|
||||
*/
|
||||
onMouseWheel(event: WheelEvent): any {
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
}
|
||||
const prevZoom: any = this.zoomLevel;
|
||||
const scale: any = 1 + 0.15 * this.root.app.settings.getScrollWheelSensitivity();
|
||||
assert(Number.isFinite(scale), "Got invalid scale in mouse wheel event: " + event.deltaY);
|
||||
assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *before* wheel: " + this.zoomLevel);
|
||||
this.zoomLevel *= event.deltaY < 0 ? scale : 1 / scale;
|
||||
assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *after* wheel: " + this.zoomLevel);
|
||||
this.clampZoomLevel();
|
||||
this.desiredZoom = null;
|
||||
let mousePosition: any = this.root.app.mousePosition;
|
||||
if (!this.root.app.settings.getAllSettings().zoomToCursor) {
|
||||
mousePosition = new Vector(this.root.gameWidth / 2, this.root.gameHeight / 2);
|
||||
}
|
||||
if (mousePosition) {
|
||||
const worldPos: any = this.root.camera.screenToWorld(mousePosition);
|
||||
const worldDelta: any = worldPos.sub(this.center);
|
||||
const actualDelta: any = this.zoomLevel / prevZoom - 1;
|
||||
this.center = this.center.add(worldDelta.multiplyScalar(actualDelta));
|
||||
this.desiredCenter = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Touch start handler
|
||||
*/
|
||||
onTouchStart(event: TouchEvent): any {
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
}
|
||||
clickDetectorGlobals.lastTouchTime = performance.now();
|
||||
this.touchPostMoveVelocity = new Vector(0, 0);
|
||||
if (event.touches.length === 1) {
|
||||
const touch: any = event.touches[0];
|
||||
this.combinedSingleTouchStartHandler(touch.clientX, touch.clientY);
|
||||
}
|
||||
else if (event.touches.length === 2) {
|
||||
// if (this.pinchPreHandler.dispatch() === STOP_PROPAGATION) {
|
||||
// // Something prevented pinching
|
||||
// return false;
|
||||
// }
|
||||
const touch1: any = event.touches[0];
|
||||
const touch2: any = event.touches[1];
|
||||
this.currentlyMoving = false;
|
||||
this.currentlyPinching = true;
|
||||
this.lastPinchPositions = [
|
||||
new Vector(touch1.clientX, touch1.clientY),
|
||||
new Vector(touch2.clientX, touch2.clientY),
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Touch move handler
|
||||
*/
|
||||
onTouchMove(event: TouchEvent): any {
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
}
|
||||
clickDetectorGlobals.lastTouchTime = performance.now();
|
||||
if (event.touches.length === 1) {
|
||||
const touch: any = event.touches[0];
|
||||
this.combinedSingleTouchMoveHandler(touch.clientX, touch.clientY);
|
||||
}
|
||||
else if (event.touches.length === 2) {
|
||||
if (this.currentlyPinching) {
|
||||
const touch1: any = event.touches[0];
|
||||
const touch2: any = event.touches[1];
|
||||
const newPinchPositions: any = [
|
||||
new Vector(touch1.clientX, touch1.clientY),
|
||||
new Vector(touch2.clientX, touch2.clientY),
|
||||
];
|
||||
// Get distance of taps last time and now
|
||||
const lastDistance: any = this.lastPinchPositions[0].distance(this.lastPinchPositions[1]);
|
||||
const thisDistance: any = newPinchPositions[0].distance(newPinchPositions[1]);
|
||||
// IMPORTANT to do math max here to avoid NaN and causing an invalid zoom level
|
||||
const difference: any = thisDistance / Math.max(0.001, lastDistance);
|
||||
// Find old center of zoom
|
||||
let oldCenter: any = this.lastPinchPositions[0].centerPoint(this.lastPinchPositions[1]);
|
||||
// Find new center of zoom
|
||||
let center: any = newPinchPositions[0].centerPoint(newPinchPositions[1]);
|
||||
// Compute movement
|
||||
let movement: any = oldCenter.sub(center);
|
||||
this.center.x += movement.x / this.zoomLevel;
|
||||
this.center.y += movement.y / this.zoomLevel;
|
||||
// Compute zoom
|
||||
center = center.sub(new Vector(this.root.gameWidth / 2, this.root.gameHeight / 2));
|
||||
// Apply zoom
|
||||
assert(Number.isFinite(difference), "Invalid pinch difference: " +
|
||||
difference +
|
||||
"(last=" +
|
||||
lastDistance +
|
||||
", new = " +
|
||||
thisDistance +
|
||||
")");
|
||||
this.zoomLevel *= difference;
|
||||
// Stick to pivot point
|
||||
const correcture: any = center.multiplyScalar(difference - 1).divideScalar(this.zoomLevel);
|
||||
this.center = this.center.add(correcture);
|
||||
this.lastPinchPositions = newPinchPositions;
|
||||
this.userInteraction.dispatch(USER_INTERACT_MOVE);
|
||||
// Since we zoomed, abort any programmed zooming
|
||||
if (this.desiredZoom) {
|
||||
this.desiredZoom = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clamp everything afterwards
|
||||
this.clampZoomLevel();
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Touch end and cancel handler
|
||||
*/
|
||||
onTouchEnd(event: TouchEvent=): any {
|
||||
if (event) {
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
}
|
||||
}
|
||||
clickDetectorGlobals.lastTouchTime = performance.now();
|
||||
if (event.changedTouches.length === 0) {
|
||||
logger.warn("Touch end without changed touches");
|
||||
}
|
||||
const touch: any = event.changedTouches[0];
|
||||
this.combinedSingleTouchStopHandler(touch.clientX, touch.clientY);
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Internal touch start handler
|
||||
*/
|
||||
combinedSingleTouchStartHandler(x: number, y: number): any {
|
||||
const pos: any = new Vector(x, y);
|
||||
if (this.downPreHandler.dispatch(pos, enumMouseButton.left) === STOP_PROPAGATION) {
|
||||
// Somebody else captured it
|
||||
return;
|
||||
}
|
||||
this.touchPostMoveVelocity = new Vector(0, 0);
|
||||
this.currentlyMoving = true;
|
||||
this.lastMovingPosition = pos;
|
||||
this.lastMovingPositionLastTick = null;
|
||||
this.numTicksStandingStill = 0;
|
||||
this.didMoveSinceTouchStart = false;
|
||||
}
|
||||
/**
|
||||
* Internal touch move handler
|
||||
*/
|
||||
combinedSingleTouchMoveHandler(x: number, y: number): any {
|
||||
const pos: any = new Vector(x, y);
|
||||
if (this.movePreHandler.dispatch(pos) === STOP_PROPAGATION) {
|
||||
// Somebody else captured it
|
||||
return;
|
||||
}
|
||||
if (!this.currentlyMoving) {
|
||||
return false;
|
||||
}
|
||||
let delta: any = this.lastMovingPosition.sub(pos).divideScalar(this.zoomLevel);
|
||||
if (G_IS_DEV && globalConfig.debug.testCulling) {
|
||||
// When testing culling, we see everything from the same distance
|
||||
delta = delta.multiplyScalar(this.zoomLevel * -2);
|
||||
}
|
||||
this.didMoveSinceTouchStart = this.didMoveSinceTouchStart || delta.length() > 0;
|
||||
this.center = this.center.add(delta);
|
||||
this.touchPostMoveVelocity = this.touchPostMoveVelocity
|
||||
.multiplyScalar(velocitySmoothing)
|
||||
.add(delta.multiplyScalar(1 - velocitySmoothing));
|
||||
this.lastMovingPosition = pos;
|
||||
this.userInteraction.dispatch(USER_INTERACT_MOVE);
|
||||
// Since we moved, abort any programmed moving
|
||||
if (this.desiredCenter) {
|
||||
this.desiredCenter = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Internal touch stop handler
|
||||
*/
|
||||
combinedSingleTouchStopHandler(x: any, y: any): any {
|
||||
if (this.currentlyMoving || this.currentlyPinching) {
|
||||
this.currentlyMoving = false;
|
||||
this.currentlyPinching = false;
|
||||
this.lastMovingPosition = null;
|
||||
this.lastMovingPositionLastTick = null;
|
||||
this.numTicksStandingStill = 0;
|
||||
this.lastPinchPositions = null;
|
||||
this.userInteraction.dispatch(USER_INTERACT_TOUCHEND);
|
||||
this.didMoveSinceTouchStart = false;
|
||||
}
|
||||
this.upPostHandler.dispatch(new Vector(x, y));
|
||||
}
|
||||
/**
|
||||
* Clamps the camera zoom level within the allowed range
|
||||
*/
|
||||
clampZoomLevel(): any {
|
||||
if (G_IS_DEV && globalConfig.debug.disableZoomLimits) {
|
||||
return;
|
||||
}
|
||||
assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel);
|
||||
this.zoomLevel = clamp(this.zoomLevel, this.getMinimumZoom(), this.getMaximumZoom());
|
||||
assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *after* clamp: " + this.zoomLevel);
|
||||
if (this.desiredZoom) {
|
||||
this.desiredZoom = clamp(this.desiredZoom, this.getMinimumZoom(), this.getMaximumZoom());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clamps the center within set boundaries
|
||||
*/
|
||||
clampToBounds(): any {
|
||||
const bounds: any = this.root.gameMode.getCameraBounds();
|
||||
if (!bounds) {
|
||||
return;
|
||||
}
|
||||
const tileScaleBounds: any = this.root.gameMode.getCameraBounds().allScaled(globalConfig.tileSize);
|
||||
this.center.x = clamp(this.center.x, tileScaleBounds.x, tileScaleBounds.x + tileScaleBounds.w);
|
||||
this.center.y = clamp(this.center.y, tileScaleBounds.y, tileScaleBounds.y + tileScaleBounds.h);
|
||||
}
|
||||
/**
|
||||
* Updates the camera
|
||||
*/
|
||||
update(dt: number): any {
|
||||
dt = Math.min(dt, 33);
|
||||
this.cameraUpdateTimeBucket += dt;
|
||||
// Simulate movement of N FPS
|
||||
const updatesPerFrame: any = 4;
|
||||
const physicsStepSizeMs: any = 1000.0 / (60.0 * updatesPerFrame);
|
||||
let now: any = this.root.time.systemNow() - 3 * physicsStepSizeMs;
|
||||
while (this.cameraUpdateTimeBucket > physicsStepSizeMs) {
|
||||
now += physicsStepSizeMs;
|
||||
this.cameraUpdateTimeBucket -= physicsStepSizeMs;
|
||||
this.internalUpdatePanning(now, physicsStepSizeMs);
|
||||
this.internalUpdateMousePanning(now, physicsStepSizeMs);
|
||||
this.internalUpdateZooming(now, physicsStepSizeMs);
|
||||
this.internalUpdateCentering(now, physicsStepSizeMs);
|
||||
this.internalUpdateShake(now, physicsStepSizeMs);
|
||||
this.internalUpdateKeyboardForce(now, physicsStepSizeMs);
|
||||
}
|
||||
this.clampZoomLevel();
|
||||
}
|
||||
/**
|
||||
* Prepares a context to transform it
|
||||
*/
|
||||
transform(context: CanvasRenderingContext2D): any {
|
||||
if (G_IS_DEV && globalConfig.debug.testCulling) {
|
||||
context.transform(1, 0, 0, 1, 100, 100);
|
||||
return;
|
||||
}
|
||||
this.clampZoomLevel();
|
||||
const zoom: any = this.zoomLevel;
|
||||
context.transform(
|
||||
// Scale, skew, rotate
|
||||
zoom, 0, 0, zoom,
|
||||
// Translate
|
||||
-zoom * this.getViewportLeft(), -zoom * this.getViewportTop());
|
||||
}
|
||||
/**
|
||||
* Internal shake handler
|
||||
*/
|
||||
internalUpdateShake(now: number, dt: number): any {
|
||||
this.currentShake = this.currentShake.multiplyScalar(0.92);
|
||||
}
|
||||
/**
|
||||
* Internal pan handler
|
||||
*/
|
||||
internalUpdatePanning(now: number, dt: number): any {
|
||||
const baseStrength: any = velocityStrength * this.root.app.platformWrapper.getTouchPanStrength();
|
||||
this.touchPostMoveVelocity = this.touchPostMoveVelocity.multiplyScalar(velocityFade);
|
||||
// Check if the camera is being dragged but standing still: if not, zero out `touchPostMoveVelocity`.
|
||||
if (this.currentlyMoving && this.desiredCenter === null) {
|
||||
if (this.lastMovingPositionLastTick !== null &&
|
||||
this.lastMovingPositionLastTick.equalsEpsilon(this.lastMovingPosition)) {
|
||||
this.numTicksStandingStill++;
|
||||
}
|
||||
else {
|
||||
this.numTicksStandingStill = 0;
|
||||
}
|
||||
this.lastMovingPositionLastTick = this.lastMovingPosition.copy();
|
||||
if (this.numTicksStandingStill >= ticksBeforeErasingVelocity) {
|
||||
this.touchPostMoveVelocity.x = 0;
|
||||
this.touchPostMoveVelocity.y = 0;
|
||||
}
|
||||
}
|
||||
// Check influence of past points
|
||||
if (!this.currentlyMoving && !this.currentlyPinching) {
|
||||
const len: any = this.touchPostMoveVelocity.length();
|
||||
if (len >= velocityMax) {
|
||||
this.touchPostMoveVelocity.x = (this.touchPostMoveVelocity.x * velocityMax) / len;
|
||||
this.touchPostMoveVelocity.y = (this.touchPostMoveVelocity.y * velocityMax) / len;
|
||||
}
|
||||
this.center = this.center.add(this.touchPostMoveVelocity.multiplyScalar(baseStrength));
|
||||
// Panning
|
||||
this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06);
|
||||
this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel));
|
||||
this.clampToBounds();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Internal screen panning handler
|
||||
*/
|
||||
internalUpdateMousePanning(now: number, dt: number): any {
|
||||
if (!this.root.app.focused) {
|
||||
return;
|
||||
}
|
||||
if (!this.root.app.settings.getAllSettings().enableMousePan) {
|
||||
// Not enabled
|
||||
return;
|
||||
}
|
||||
const mousePos: any = this.root.app.mousePosition;
|
||||
if (!mousePos) {
|
||||
return;
|
||||
}
|
||||
if (this.root.hud.shouldPauseGame() || this.root.hud.hasBlockingOverlayOpen()) {
|
||||
return;
|
||||
}
|
||||
if (this.desiredCenter || this.desiredZoom || this.currentlyMoving || this.currentlyPinching) {
|
||||
// Performing another method of movement right now
|
||||
return;
|
||||
}
|
||||
if (mousePos.x < 0 ||
|
||||
mousePos.y < 0 ||
|
||||
mousePos.x > this.root.gameWidth ||
|
||||
mousePos.y > this.root.gameHeight) {
|
||||
// Out of screen
|
||||
return;
|
||||
}
|
||||
const panAreaPixels: any = 2;
|
||||
const panVelocity: any = new Vector();
|
||||
if (mousePos.x < panAreaPixels) {
|
||||
panVelocity.x -= 1;
|
||||
}
|
||||
if (mousePos.x > this.root.gameWidth - panAreaPixels) {
|
||||
panVelocity.x += 1;
|
||||
}
|
||||
if (mousePos.y < panAreaPixels) {
|
||||
panVelocity.y -= 1;
|
||||
}
|
||||
if (mousePos.y > this.root.gameHeight - panAreaPixels) {
|
||||
panVelocity.y += 1;
|
||||
}
|
||||
this.center = this.center.add(panVelocity.multiplyScalar(((0.5 * dt) / this.zoomLevel) * this.root.app.settings.getMovementSpeed()));
|
||||
this.clampToBounds();
|
||||
}
|
||||
/**
|
||||
* Updates the non user interaction zooming
|
||||
*/
|
||||
internalUpdateZooming(now: number, dt: number): any {
|
||||
if (!this.currentlyPinching && this.desiredZoom !== null) {
|
||||
const diff: any = this.zoomLevel - this.desiredZoom;
|
||||
if (Math.abs(diff) > 0.0001) {
|
||||
let fade: any = 0.94;
|
||||
if (diff > 0) {
|
||||
// Zoom out faster than in
|
||||
fade = 0.9;
|
||||
}
|
||||
assert(Number.isFinite(this.desiredZoom), "Desired zoom is NaN: " + this.desiredZoom);
|
||||
assert(Number.isFinite(fade), "Zoom fade is NaN: " + fade);
|
||||
this.zoomLevel = this.zoomLevel * fade + this.desiredZoom * (1 - fade);
|
||||
assert(Number.isFinite(this.zoomLevel), "Zoom level is NaN after fade: " + this.zoomLevel);
|
||||
}
|
||||
else {
|
||||
this.zoomLevel = this.desiredZoom;
|
||||
this.desiredZoom = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Updates the non user interaction centering
|
||||
*/
|
||||
internalUpdateCentering(now: number, dt: number): any {
|
||||
if (!this.currentlyMoving && this.desiredCenter !== null) {
|
||||
const diff: any = this.center.direction(this.desiredCenter);
|
||||
const length: any = diff.length();
|
||||
const tolerance: any = 1 / this.zoomLevel;
|
||||
if (length > tolerance) {
|
||||
const movement: any = diff.multiplyScalar(Math.min(1, dt * 0.008));
|
||||
this.center.x += movement.x;
|
||||
this.center.y += movement.y;
|
||||
}
|
||||
else {
|
||||
this.desiredCenter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Updates the keyboard forces
|
||||
*/
|
||||
internalUpdateKeyboardForce(now: number, dt: number): any {
|
||||
if (!this.currentlyMoving && this.desiredCenter == null) {
|
||||
const limitingDimension: any = Math.min(this.root.gameWidth, this.root.gameHeight);
|
||||
const moveAmount: any = ((limitingDimension / 2048) * dt) / this.zoomLevel;
|
||||
let forceX: any = 0;
|
||||
let forceY: any = 0;
|
||||
const actionMapper: any = this.root.keyMapper;
|
||||
if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).pressed) {
|
||||
forceY -= 1;
|
||||
}
|
||||
if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).pressed) {
|
||||
forceY += 1;
|
||||
}
|
||||
if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).pressed) {
|
||||
forceX -= 1;
|
||||
}
|
||||
if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).pressed) {
|
||||
forceX += 1;
|
||||
}
|
||||
let movementSpeed: any = this.root.app.settings.getMovementSpeed() *
|
||||
(actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveFaster).pressed ? 4 : 1);
|
||||
this.center.x += moveAmount * forceX * movementSpeed;
|
||||
this.center.y += moveAmount * forceY * movementSpeed;
|
||||
this.clampToBounds();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/** @enum {string} */
|
||||
export const enumColors: any = {
|
||||
red: "red",
|
||||
green: "green",
|
||||
blue: "blue",
|
||||
yellow: "yellow",
|
||||
purple: "purple",
|
||||
cyan: "cyan",
|
||||
white: "white",
|
||||
uncolored: "uncolored",
|
||||
};
|
||||
const c: any = enumColors;
|
||||
/** @enum {string} */
|
||||
export const enumColorToShortcode: any = {
|
||||
[c.red]: "r",
|
||||
[c.green]: "g",
|
||||
[c.blue]: "b",
|
||||
[c.yellow]: "y",
|
||||
[c.purple]: "p",
|
||||
[c.cyan]: "c",
|
||||
[c.white]: "w",
|
||||
[c.uncolored]: "u",
|
||||
};
|
||||
/** @enum {enumColors} */
|
||||
export const enumShortcodeToColor: any = {};
|
||||
for (const key: any in enumColorToShortcode) {
|
||||
enumShortcodeToColor[enumColorToShortcode[key]] = key;
|
||||
}
|
||||
/** @enum {string} */
|
||||
export const enumColorsToHexCode: any = {
|
||||
[c.red]: "#ff666a",
|
||||
[c.green]: "#78ff66",
|
||||
[c.blue]: "#66a7ff",
|
||||
// red + green
|
||||
[c.yellow]: "#fcf52a",
|
||||
// red + blue
|
||||
[c.purple]: "#dd66ff",
|
||||
// blue + green
|
||||
[c.cyan]: "#00fcff",
|
||||
// blue + green + red
|
||||
[c.white]: "#ffffff",
|
||||
[c.uncolored]: "#aaaaaa",
|
||||
};
|
||||
/** @enum {Object.<string, string>} */
|
||||
export const enumColorMixingResults: any = {};
|
||||
const bitfieldToColor: any = [
|
||||
/* 000 */ c.uncolored,
|
||||
/* 001 */ c.red,
|
||||
/* 010 */ c.green,
|
||||
/* 011 */ c.yellow,
|
||||
/* 100 */ c.blue,
|
||||
/* 101 */ c.purple,
|
||||
/* 110 */ c.cyan,
|
||||
/* 111 */ c.white,
|
||||
];
|
||||
for (let i: any = 0; i < 1 << 3; ++i) {
|
||||
enumColorMixingResults[bitfieldToColor[i]] = {};
|
||||
for (let j: any = 0; j < 1 << 3; ++j) {
|
||||
enumColorMixingResults[bitfieldToColor[i]][bitfieldToColor[j]] = bitfieldToColor[i | j];
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import { BasicSerializableObject } from "../savegame/serialization";
|
||||
export class Component extends BasicSerializableObject {
|
||||
/**
|
||||
* Returns the components unique id
|
||||
* {}
|
||||
* @abstract
|
||||
*/
|
||||
static getId(): string {
|
||||
abstract;
|
||||
return "unknown-component";
|
||||
}
|
||||
/**
|
||||
* Should return the schema used for serialization
|
||||
*/
|
||||
static getSchema(): any {
|
||||
return {};
|
||||
}
|
||||
/**
|
||||
* Copy the current state to another component
|
||||
*/
|
||||
copyAdditionalStateTo(otherComponent: Component): any { }
|
||||
/**
|
||||
* Clears all items and state
|
||||
*/
|
||||
clear(): any { }
|
||||
/* dev:start */
|
||||
/**
|
||||
* Fixes typeof DerivedComponent is not assignable to typeof Component, compiled out
|
||||
* in non-dev builds
|
||||
*/
|
||||
|
||||
constructor(...args) {
|
||||
super();
|
||||
}
|
||||
/**
|
||||
* Returns a string representing the components data, only in dev builds
|
||||
* {}
|
||||
*/
|
||||
getDebugString(): string {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,55 @@
|
||||
import { gComponentRegistry } from "../core/global_registries";
|
||||
import { StaticMapEntityComponent } from "./components/static_map_entity";
|
||||
import { BeltComponent } from "./components/belt";
|
||||
import { ItemEjectorComponent } from "./components/item_ejector";
|
||||
import { ItemAcceptorComponent } from "./components/item_acceptor";
|
||||
import { MinerComponent } from "./components/miner";
|
||||
import { ItemProcessorComponent } from "./components/item_processor";
|
||||
import { UndergroundBeltComponent } from "./components/underground_belt";
|
||||
import { HubComponent } from "./components/hub";
|
||||
import { StorageComponent } from "./components/storage";
|
||||
import { WiredPinsComponent } from "./components/wired_pins";
|
||||
import { BeltUnderlaysComponent } from "./components/belt_underlays";
|
||||
import { WireComponent } from "./components/wire";
|
||||
import { ConstantSignalComponent } from "./components/constant_signal";
|
||||
import { LogicGateComponent } from "./components/logic_gate";
|
||||
import { LeverComponent } from "./components/lever";
|
||||
import { WireTunnelComponent } from "./components/wire_tunnel";
|
||||
import { DisplayComponent } from "./components/display";
|
||||
import { BeltReaderComponent } from "./components/belt_reader";
|
||||
import { FilterComponent } from "./components/filter";
|
||||
import { ItemProducerComponent } from "./components/item_producer";
|
||||
import { GoalAcceptorComponent } from "./components/goal_acceptor";
|
||||
export function initComponentRegistry(): any {
|
||||
const components: any = [
|
||||
StaticMapEntityComponent,
|
||||
BeltComponent,
|
||||
ItemEjectorComponent,
|
||||
ItemAcceptorComponent,
|
||||
MinerComponent,
|
||||
ItemProcessorComponent,
|
||||
UndergroundBeltComponent,
|
||||
HubComponent,
|
||||
StorageComponent,
|
||||
WiredPinsComponent,
|
||||
BeltUnderlaysComponent,
|
||||
WireComponent,
|
||||
ConstantSignalComponent,
|
||||
LogicGateComponent,
|
||||
LeverComponent,
|
||||
WireTunnelComponent,
|
||||
DisplayComponent,
|
||||
BeltReaderComponent,
|
||||
FilterComponent,
|
||||
ItemProducerComponent,
|
||||
GoalAcceptorComponent,
|
||||
];
|
||||
components.forEach((component: any): any => gComponentRegistry.register(component));
|
||||
// IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS
|
||||
// Sanity check - If this is thrown, you forgot to add a new component here
|
||||
assert(
|
||||
// @ts-ignore
|
||||
require.context("./components", false, /.*\.js/i).keys().length ===
|
||||
gComponentRegistry.getNumEntries(), "Not all components are registered");
|
||||
console.log("📦 There are", gComponentRegistry.getNumEntries(), "components");
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { types } from "../../savegame/serialization";
|
||||
import { BeltPath } from "../belt_path";
|
||||
import { Component } from "../component";
|
||||
export const curvedBeltLength: any = 0.78;
|
||||
export const FAKE_BELT_ACCEPTOR_SLOT: import("./item_acceptor").ItemAcceptorSlot = {
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.bottom,
|
||||
};
|
||||
export const FAKE_BELT_EJECTOR_SLOT_BY_DIRECTION: {
|
||||
[idx: enumDirection]: import("./item_ejector").ItemEjectorSlot;
|
||||
} = {
|
||||
[enumDirection.top]: {
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
item: null,
|
||||
progress: 0,
|
||||
},
|
||||
[enumDirection.right]: {
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.right,
|
||||
item: null,
|
||||
progress: 0,
|
||||
},
|
||||
[enumDirection.left]: {
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
item: null,
|
||||
progress: 0,
|
||||
},
|
||||
};
|
||||
export class BeltComponent extends Component {
|
||||
static getId(): any {
|
||||
return "Belt";
|
||||
}
|
||||
public direction = direction;
|
||||
public assignedPath: BeltPath = null;
|
||||
|
||||
constructor({ direction = enumDirection.top }) {
|
||||
super();
|
||||
}
|
||||
clear(): any {
|
||||
if (this.assignedPath) {
|
||||
this.assignedPath.clearAllItems();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the effective length of this belt in tile space
|
||||
* {}
|
||||
*/
|
||||
getEffectiveLengthTiles(): number {
|
||||
return this.direction === enumDirection.top ? 1.0 : curvedBeltLength;
|
||||
}
|
||||
/**
|
||||
* Returns fake acceptor slot used for matching
|
||||
* {}
|
||||
*/
|
||||
getFakeAcceptorSlot(): import("./item_acceptor").ItemAcceptorSlot {
|
||||
return FAKE_BELT_ACCEPTOR_SLOT;
|
||||
}
|
||||
/**
|
||||
* Returns fake acceptor slot used for matching
|
||||
* {}
|
||||
*/
|
||||
getFakeEjectorSlot(): import("./item_ejector").ItemEjectorSlot {
|
||||
assert(FAKE_BELT_EJECTOR_SLOT_BY_DIRECTION[this.direction], "Invalid belt direction: ", this.direction);
|
||||
return FAKE_BELT_EJECTOR_SLOT_BY_DIRECTION[this.direction];
|
||||
}
|
||||
/**
|
||||
* Converts from belt space (0 = start of belt ... 1 = end of belt) to the local
|
||||
* belt coordinates (-0.5|-0.5 to 0.5|0.5)
|
||||
* {}
|
||||
*/
|
||||
transformBeltToLocalSpace(progress: number): Vector {
|
||||
assert(progress >= 0.0, "Invalid progress ( < 0): " + progress);
|
||||
switch (this.direction) {
|
||||
case enumDirection.top:
|
||||
assert(progress <= 1.02, "Invalid progress: " + progress);
|
||||
return new Vector(0, 0.5 - progress);
|
||||
case enumDirection.right: {
|
||||
assert(progress <= curvedBeltLength + 0.02, "Invalid progress 2: " + progress);
|
||||
const arcProgress: any = (progress / curvedBeltLength) * 0.5 * Math.PI;
|
||||
return new Vector(0.5 - 0.5 * Math.cos(arcProgress), 0.5 - 0.5 * Math.sin(arcProgress));
|
||||
}
|
||||
case enumDirection.left: {
|
||||
assert(progress <= curvedBeltLength + 0.02, "Invalid progress 3: " + progress);
|
||||
const arcProgress: any = (progress / curvedBeltLength) * 0.5 * Math.PI;
|
||||
return new Vector(-0.5 + 0.5 * Math.cos(arcProgress), 0.5 - 0.5 * Math.sin(arcProgress));
|
||||
}
|
||||
default:
|
||||
assertAlways(false, "Invalid belt direction: " + this.direction);
|
||||
return new Vector(0, 0);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { Component } from "../component";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
import { types } from "../../savegame/serialization";
|
||||
/** @enum {string} */
|
||||
export const enumBeltReaderType: any = {
|
||||
wired: "wired",
|
||||
wireless: "wireless",
|
||||
};
|
||||
export class BeltReaderComponent extends Component {
|
||||
static getId(): any {
|
||||
return "BeltReader";
|
||||
}
|
||||
static getSchema(): any {
|
||||
return {
|
||||
lastItem: types.nullable(typeItemSingleton),
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.clear();
|
||||
}
|
||||
clear(): any {
|
||||
/**
|
||||
* Which items went through the reader, we only store the time
|
||||
*/
|
||||
this.lastItemTimes = [];
|
||||
/**
|
||||
* Which item passed the reader last
|
||||
*/
|
||||
this.lastItem = null;
|
||||
/**
|
||||
* Stores the last throughput we computed
|
||||
*/
|
||||
this.lastThroughput = 0;
|
||||
/**
|
||||
* Stores when we last computed the throughput
|
||||
*/
|
||||
this.lastThroughputComputation = 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { Component } from "../component";
|
||||
/**
|
||||
* Store which type an underlay is, this is cached so we can easily
|
||||
* render it.
|
||||
*
|
||||
* Full: Render underlay at top and bottom of tile
|
||||
* Bottom Only: Only render underlay at the bottom half
|
||||
* Top Only:
|
||||
* @enum {string}
|
||||
*/
|
||||
export const enumClippedBeltUnderlayType: any = {
|
||||
full: "full",
|
||||
bottomOnly: "bottomOnly",
|
||||
topOnly: "topOnly",
|
||||
none: "none",
|
||||
};
|
||||
export type BeltUnderlayTile = {
|
||||
pos: Vector;
|
||||
direction: enumDirection;
|
||||
cachedType?: enumClippedBeltUnderlayType;
|
||||
};
|
||||
|
||||
export class BeltUnderlaysComponent extends Component {
|
||||
static getId(): any {
|
||||
return "BeltUnderlays";
|
||||
}
|
||||
public underlays = underlays;
|
||||
|
||||
constructor({ underlays = [] }) {
|
||||
super();
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { types } from "../../savegame/serialization";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { Component } from "../component";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
export class ConstantSignalComponent extends Component {
|
||||
static getId(): any {
|
||||
return "ConstantSignal";
|
||||
}
|
||||
static getSchema(): any {
|
||||
return {
|
||||
signal: types.nullable(typeItemSingleton),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Copy the current state to another component
|
||||
*/
|
||||
copyAdditionalStateTo(otherComponent: ConstantSignalComponent): any {
|
||||
otherComponent.signal = this.signal;
|
||||
}
|
||||
public signal = signal;
|
||||
|
||||
constructor({ signal = null }) {
|
||||
super();
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { Component } from "../component";
|
||||
export class DisplayComponent extends Component {
|
||||
static getId(): any {
|
||||
return "Display";
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import { types } from "../../savegame/serialization";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { Component } from "../component";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
export type PendingFilterItem = {
|
||||
item: BaseItem;
|
||||
progress: number;
|
||||
};
|
||||
|
||||
export class FilterComponent extends Component {
|
||||
static getId(): any {
|
||||
return "Filter";
|
||||
}
|
||||
duplicateWithoutContents(): any {
|
||||
return new FilterComponent();
|
||||
}
|
||||
static getSchema(): any {
|
||||
return {
|
||||
pendingItemsToLeaveThrough: types.array(types.structured({
|
||||
item: typeItemSingleton,
|
||||
progress: types.ufloat,
|
||||
})),
|
||||
pendingItemsToReject: types.array(types.structured({
|
||||
item: typeItemSingleton,
|
||||
progress: types.ufloat,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.clear();
|
||||
}
|
||||
clear(): any {
|
||||
/**
|
||||
* Items in queue to leave through
|
||||
*/
|
||||
this.pendingItemsToLeaveThrough = [];
|
||||
/**
|
||||
* Items in queue to reject
|
||||
*/
|
||||
this.pendingItemsToReject = [];
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { Component } from "../component";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
export class GoalAcceptorComponent extends Component {
|
||||
static getId(): any {
|
||||
return "GoalAcceptor";
|
||||
}
|
||||
static getSchema(): any {
|
||||
return {
|
||||
item: typeItemSingleton,
|
||||
};
|
||||
}
|
||||
public item: BaseItem | undefined = item;
|
||||
|
||||
constructor({ item = null, rate = null }) {
|
||||
super();
|
||||
this.clear();
|
||||
}
|
||||
clear(): any {
|
||||
/**
|
||||
* The last item we delivered
|
||||
*/
|
||||
this.lastDelivery = null;
|
||||
// The amount of items we delivered so far
|
||||
this.currentDeliveredItems = 0;
|
||||
// Used for animations
|
||||
this.displayPercentage = 0;
|
||||
}
|
||||
/**
|
||||
* Clears items but doesn't instantly reset the progress bar
|
||||
*/
|
||||
clearItems(): any {
|
||||
this.lastDelivery = null;
|
||||
this.currentDeliveredItems = 0;
|
||||
}
|
||||
getRequiredSecondsPerItem(): any {
|
||||
return (globalConfig.goalAcceptorsPerProducer /
|
||||
(globalConfig.puzzleModeSpeed * globalConfig.beltSpeedItemsPerSecond));
|
||||
}
|
||||
/**
|
||||
* Copy the current state to another component
|
||||
*/
|
||||
copyAdditionalStateTo(otherComponent: GoalAcceptorComponent): any {
|
||||
otherComponent.item = this.item;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { Component } from "../component";
|
||||
export class HubComponent extends Component {
|
||||
static getId(): any {
|
||||
return "Hub";
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
import { enumDirection, enumInvertedDirections, Vector } from "../../core/vector";
|
||||
import { types } from "../../savegame/serialization";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { Component } from "../component";
|
||||
export type ItemAcceptorSlot = {
|
||||
pos: Vector;
|
||||
direction: enumDirection;
|
||||
filter?: ItemType;
|
||||
};
|
||||
export type ItemAcceptorLocatedSlot = {
|
||||
slot: ItemAcceptorSlot;
|
||||
index: number;
|
||||
};
|
||||
export type ItemAcceptorSlotConfig = {
|
||||
pos: Vector;
|
||||
direction: enumDirection;
|
||||
filter?: ItemType;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export class ItemAcceptorComponent extends Component {
|
||||
static getId(): any {
|
||||
return "ItemAcceptor";
|
||||
}
|
||||
|
||||
constructor({ slots = [] }) {
|
||||
super();
|
||||
this.setSlots(slots);
|
||||
this.clear();
|
||||
}
|
||||
clear(): any {
|
||||
/**
|
||||
* Fixes belt animations
|
||||
*/
|
||||
this.itemConsumptionAnimations = [];
|
||||
}
|
||||
setSlots(slots: Array<ItemAcceptorSlotConfig>): any {
|
||||
this.slots = [];
|
||||
for (let i: any = 0; i < slots.length; ++i) {
|
||||
const slot: any = slots[i];
|
||||
this.slots.push({
|
||||
pos: slot.pos,
|
||||
direction: slot.direction,
|
||||
// Which type of item to accept (shape | color | all) @see ItemType
|
||||
filter: slot.filter,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns if this acceptor can accept a new item at slot N
|
||||
*
|
||||
* NOTICE: The belt path ignores this for performance reasons and does his own check
|
||||
*/
|
||||
canAcceptItem(slotIndex: number, item: BaseItem=): any {
|
||||
const slot: any = this.slots[slotIndex];
|
||||
return !slot.filter || slot.filter === item.getItemType();
|
||||
}
|
||||
/**
|
||||
* Called when an item has been accepted so that
|
||||
*/
|
||||
onItemAccepted(slotIndex: number, direction: enumDirection, item: BaseItem, remainingProgress: number = 0.0): any {
|
||||
this.itemConsumptionAnimations.push({
|
||||
item,
|
||||
slotIndex,
|
||||
direction,
|
||||
animProgress: Math.min(1, remainingProgress * 2),
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Tries to find a slot which accepts the current item
|
||||
* {}
|
||||
*/
|
||||
findMatchingSlot(targetLocalTile: Vector, fromLocalDirection: enumDirection): ItemAcceptorLocatedSlot | null {
|
||||
// We need to invert our direction since the acceptor specifies *from* which direction
|
||||
// it accepts items, but the ejector specifies *into* which direction it ejects items.
|
||||
// E.g.: Ejector ejects into "right" direction but acceptor accepts from "left" direction.
|
||||
const desiredDirection: any = enumInvertedDirections[fromLocalDirection];
|
||||
// Go over all slots and try to find a target slot
|
||||
for (let slotIndex: any = 0; slotIndex < this.slots.length; ++slotIndex) {
|
||||
const slot: any = this.slots[slotIndex];
|
||||
// Make sure the acceptor slot is on the right position
|
||||
if (!slot.pos.equals(targetLocalTile)) {
|
||||
continue;
|
||||
}
|
||||
// Check if the acceptor slot accepts items from our direction
|
||||
if (desiredDirection === slot.direction) {
|
||||
return {
|
||||
slot,
|
||||
index: slotIndex,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
import { enumDirection, enumDirectionToVector, Vector } from "../../core/vector";
|
||||
import { types } from "../../savegame/serialization";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { BeltPath } from "../belt_path";
|
||||
import { Component } from "../component";
|
||||
import { Entity } from "../entity";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
export type ItemEjectorSlot = {
|
||||
pos: Vector;
|
||||
direction: enumDirection;
|
||||
item: BaseItem;
|
||||
lastItem: BaseItem;
|
||||
progress: ?number;
|
||||
cachedDestSlot?: import("./item_acceptor").ItemAcceptorLocatedSlot;
|
||||
cachedBeltPath?: BeltPath;
|
||||
cachedTargetEntity?: Entity;
|
||||
};
|
||||
|
||||
export class ItemEjectorComponent extends Component {
|
||||
static getId(): any {
|
||||
return "ItemEjector";
|
||||
}
|
||||
static getSchema(): any {
|
||||
// The cachedDestSlot, cachedTargetEntity fields are not serialized.
|
||||
return {
|
||||
slots: types.fixedSizeArray(types.structured({
|
||||
item: types.nullable(typeItemSingleton),
|
||||
progress: types.float,
|
||||
})),
|
||||
};
|
||||
}
|
||||
public renderFloatingItems = renderFloatingItems;
|
||||
|
||||
constructor({ slots = [], renderFloatingItems = true }) {
|
||||
super();
|
||||
this.setSlots(slots);
|
||||
}
|
||||
clear(): any {
|
||||
for (const slot: any of this.slots) {
|
||||
slot.item = null;
|
||||
slot.lastItem = null;
|
||||
slot.progress = 0;
|
||||
}
|
||||
}
|
||||
setSlots(slots: Array<{
|
||||
pos: Vector;
|
||||
direction: enumDirection;
|
||||
}>): any {
|
||||
this.slots = [];
|
||||
for (let i: any = 0; i < slots.length; ++i) {
|
||||
const slot: any = slots[i];
|
||||
this.slots.push({
|
||||
pos: slot.pos,
|
||||
direction: slot.direction,
|
||||
item: null,
|
||||
lastItem: null,
|
||||
progress: 0,
|
||||
cachedDestSlot: null,
|
||||
cachedTargetEntity: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns where this slot ejects to
|
||||
* {}
|
||||
*/
|
||||
getSlotTargetLocalTile(slot: ItemEjectorSlot): Vector {
|
||||
const directionVector: any = enumDirectionToVector[slot.direction];
|
||||
return slot.pos.add(directionVector);
|
||||
}
|
||||
/**
|
||||
* Returns whether any slot ejects to the given local tile
|
||||
*/
|
||||
anySlotEjectsToLocalTile(tile: Vector): any {
|
||||
for (let i: any = 0; i < this.slots.length; ++i) {
|
||||
if (this.getSlotTargetLocalTile(this.slots[i]).equals(tile)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Returns if we can eject on a given slot
|
||||
* {}
|
||||
*/
|
||||
canEjectOnSlot(slotIndex: number): boolean {
|
||||
assert(slotIndex >= 0 && slotIndex < this.slots.length, "Invalid ejector slot: " + slotIndex);
|
||||
return !this.slots[slotIndex].item;
|
||||
}
|
||||
/**
|
||||
* Returns the first free slot on this ejector or null if there is none
|
||||
* {}
|
||||
*/
|
||||
getFirstFreeSlot(): ?number {
|
||||
for (let i: any = 0; i < this.slots.length; ++i) {
|
||||
if (this.canEjectOnSlot(i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Tries to eject a given item
|
||||
* {}
|
||||
*/
|
||||
tryEject(slotIndex: number, item: BaseItem): boolean {
|
||||
if (!this.canEjectOnSlot(slotIndex)) {
|
||||
return false;
|
||||
}
|
||||
this.slots[slotIndex].item = item;
|
||||
this.slots[slotIndex].lastItem = item;
|
||||
this.slots[slotIndex].progress = 0;
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Clears the given slot and returns the item it had
|
||||
* {}
|
||||
*/
|
||||
takeSlotItem(slotIndex: number): BaseItem | null {
|
||||
const slot: any = this.slots[slotIndex];
|
||||
const item: any = slot.item;
|
||||
slot.item = null;
|
||||
slot.progress = 0.0;
|
||||
return item;
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
import { types } from "../../savegame/serialization";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { Component } from "../component";
|
||||
/** @enum {string} */
|
||||
export const enumItemProcessorTypes: any = {
|
||||
balancer: "balancer",
|
||||
cutter: "cutter",
|
||||
cutterQuad: "cutterQuad",
|
||||
rotater: "rotater",
|
||||
rotaterCCW: "rotaterCCW",
|
||||
rotater180: "rotater180",
|
||||
stacker: "stacker",
|
||||
trash: "trash",
|
||||
mixer: "mixer",
|
||||
painter: "painter",
|
||||
painterDouble: "painterDouble",
|
||||
painterQuad: "painterQuad",
|
||||
hub: "hub",
|
||||
filter: "filter",
|
||||
reader: "reader",
|
||||
goal: "goal",
|
||||
};
|
||||
/** @enum {string} */
|
||||
export const enumItemProcessorRequirements: any = {
|
||||
painterQuad: "painterQuad",
|
||||
};
|
||||
export type EjectorItemToEject = {
|
||||
item: BaseItem;
|
||||
requiredSlot?: number;
|
||||
preferredSlot?: number;
|
||||
};
|
||||
export type EjectorCharge = {
|
||||
remainingTime: number;
|
||||
items: Array<EjectorItemToEject>;
|
||||
};
|
||||
|
||||
|
||||
export class ItemProcessorComponent extends Component {
|
||||
static getId(): any {
|
||||
return "ItemProcessor";
|
||||
}
|
||||
static getSchema(): any {
|
||||
return {
|
||||
nextOutputSlot: types.uint,
|
||||
};
|
||||
}
|
||||
public inputsPerCharge = inputsPerCharge;
|
||||
public type = processorType;
|
||||
public processingRequirement = processingRequirement;
|
||||
public inputSlots: Map<number, BaseItem> = new Map();
|
||||
|
||||
constructor({ processorType = enumItemProcessorTypes.balancer, processingRequirement = null, inputsPerCharge = 1, }) {
|
||||
super();
|
||||
this.clear();
|
||||
}
|
||||
clear(): any {
|
||||
// Which slot to emit next, this is only a preference and if it can't emit
|
||||
// it will take the other one. Some machines ignore this (e.g. the balancer) to make
|
||||
// sure the outputs always match
|
||||
this.nextOutputSlot = 0;
|
||||
this.inputSlots.clear();
|
||||
/**
|
||||
* Current input count
|
||||
*/
|
||||
this.inputCount = 0;
|
||||
/**
|
||||
* What we are currently processing, empty if we don't produce anything rn
|
||||
* requiredSlot: Item *must* be ejected on this slot
|
||||
* preferredSlot: Item *can* be ejected on this slot, but others are fine too if the one is not usable
|
||||
*/
|
||||
this.ongoingCharges = [];
|
||||
/**
|
||||
* How much processing time we have left from the last tick
|
||||
*/
|
||||
this.bonusTime = 0;
|
||||
this.queuedEjects = [];
|
||||
}
|
||||
/**
|
||||
* Tries to take the item
|
||||
*/
|
||||
tryTakeItem(item: BaseItem, sourceSlot: number): any {
|
||||
if (this.type === enumItemProcessorTypes.hub ||
|
||||
this.type === enumItemProcessorTypes.trash ||
|
||||
this.type === enumItemProcessorTypes.goal) {
|
||||
// Hub has special logic .. not really nice but efficient.
|
||||
this.inputSlots.set(this.inputCount, item);
|
||||
this.inputCount++;
|
||||
return true;
|
||||
}
|
||||
// Check that we only take one item per slot
|
||||
if (this.inputSlots.has(sourceSlot)) {
|
||||
return false;
|
||||
}
|
||||
this.inputSlots.set(sourceSlot, item);
|
||||
this.inputCount++;
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { Component } from "../component";
|
||||
export class ItemProducerComponent extends Component {
|
||||
static getId(): any {
|
||||
return "ItemProducer";
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { Component } from "../component";
|
||||
import { types } from "../../savegame/serialization";
|
||||
export class LeverComponent extends Component {
|
||||
static getId(): any {
|
||||
return "Lever";
|
||||
}
|
||||
static getSchema(): any {
|
||||
return {
|
||||
toggled: types.bool,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Copy the current state to another component
|
||||
*/
|
||||
copyAdditionalStateTo(otherComponent: LeverComponent): any {
|
||||
otherComponent.toggled = this.toggled;
|
||||
}
|
||||
public toggled = toggled;
|
||||
|
||||
constructor({ toggled = false }) {
|
||||
super();
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { Component } from "../component";
|
||||
/** @enum {string} */
|
||||
export const enumLogicGateType: any = {
|
||||
and: "and",
|
||||
not: "not",
|
||||
xor: "xor",
|
||||
or: "or",
|
||||
transistor: "transistor",
|
||||
analyzer: "analyzer",
|
||||
rotater: "rotater",
|
||||
unstacker: "unstacker",
|
||||
cutter: "cutter",
|
||||
compare: "compare",
|
||||
stacker: "stacker",
|
||||
painter: "painter",
|
||||
};
|
||||
export class LogicGateComponent extends Component {
|
||||
static getId(): any {
|
||||
return "LogicGate";
|
||||
}
|
||||
public type = type;
|
||||
|
||||
constructor({ type = enumLogicGateType.and }) {
|
||||
super();
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { types } from "../../savegame/serialization";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { Component } from "../component";
|
||||
import { Entity } from "../entity";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
const chainBufferSize: any = 6;
|
||||
export class MinerComponent extends Component {
|
||||
static getId(): any {
|
||||
return "Miner";
|
||||
}
|
||||
static getSchema(): any {
|
||||
// cachedMinedItem is not serialized.
|
||||
return {
|
||||
lastMiningTime: types.ufloat,
|
||||
itemChainBuffer: types.array(typeItemSingleton),
|
||||
};
|
||||
}
|
||||
public lastMiningTime = 0;
|
||||
public chainable = chainable;
|
||||
public cachedMinedItem: BaseItem = null;
|
||||
public cachedChainedMiner: Entity | null | false = null;
|
||||
|
||||
constructor({ chainable = false }) {
|
||||
super();
|
||||
this.clear();
|
||||
}
|
||||
clear(): any {
|
||||
/**
|
||||
* Stores items from other miners which were chained to this
|
||||
* miner.
|
||||
*/
|
||||
this.itemChainBuffer = [];
|
||||
}
|
||||
tryAcceptChainedItem(item: BaseItem): any {
|
||||
if (this.itemChainBuffer.length > chainBufferSize) {
|
||||
// Well, this one is full
|
||||
return false;
|
||||
}
|
||||
this.itemChainBuffer.push(item);
|
||||
return true;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue