diff --git a/src/ts/application.ts b/src/ts/application.ts index a5f1c02f..6436762f 100644 --- a/src/ts/application.ts +++ b/src/ts/application.ts @@ -38,11 +38,13 @@ 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; + +import type { AchievementProviderInterface } from "./platform/achievement_provider"; +import type { SoundInterface } from "./platform/sound"; +import type { StorageInterface } from "./platform/storage"; 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") { @@ -50,71 +52,87 @@ if (typeof document.hidden !== "undefined") { pageHiddenPropName = "hidden"; pageVisibilityEventName = "visibilitychange"; // @ts-ignore -} -else if (typeof document.msHidden !== "undefined") { +} else if (typeof document.msHidden !== "undefined") { pageHiddenPropName = "msHidden"; pageVisibilityEventName = "msvisibilitychange"; // @ts-ignore -} -else if (typeof document.webkitHidden !== "undefined") { +} else if (typeof document.webkitHidden !== "undefined") { pageHiddenPropName = "webkitHidden"; pageVisibilityEventName = "webkitvisibilitychange"; } + export class Application { + public unloaded = true; + + // Global stuff + public settings = new ApplicationSettings(this); + public ticker = new AnimationFrame(); + public stateMgr = new StateManager(this); + public savegameMgr = new SavegameManager(this); + public inputMgr = new InputDistributor(this); + public backgroundResourceLoader = new BackgroundResourcesLoader(this); + public clientApi = new ClientAPI(this); + + // Restrictions (Like demo etc) + public restrictionMgr = new RestrictionManager(this); + + // Platform dependent stuff + public storage: StorageInterface = null; + public sound: SoundInterface = new SoundImplBrowser(this); + public platformWrapper: PlatformWrapperInterface = G_IS_STANDALONE ? new PlatformWrapperImplElectron(this) : new PlatformWrapperImplBrowser(this); + public achievementProvider: AchievementProviderInterface = new NoAchievementProvider(this); + public adProvider: AdProviderInterface = new NoAdProvider(this); + public analytics: AnalyticsInterface = new GoogleAnalyticsImpl(this); + public gameAnalytics = new ShapezGameAnalytics(this); + + // Track if the window is focused (only relevant for browser) + public focused = true; + + // Track if the window is visible + public pageVisible = true; + + // Track if the app is paused (cordova) + public applicationPaused = false; + public trackedIsRenderable = new TrackedState(this.onAppRenderableStateChanged, this); + public trackedIsPlaying = new TrackedState(this.onAppPlayingStateChanged, this); + + // Dimensions + public screenWidth = 0; + public screenHeight = 0; + // Store the timestamp where we last checked for a screen resize, since orientationchange is unreliable with cordova + public lastResizeCheck: number = null; + // Store the mouse position, or null if not available + public mousePosition: Vector = null; + + + /** * Boots the application */ - async boot(): any { + async boot(): Promise { 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"); @@ -122,35 +140,21 @@ export class Application { 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 { + registerStates(): void { const states: Array = [ WegameSplashState, PreloadState, @@ -165,39 +169,47 @@ export class Application { LoginState, ModsState, ]; - for (let i: any = 0; i < states.length; ++i) { + for (let i = 0; i < states.length; ++i) { this.stateMgr.register(states[i]); } } + /** * Registers all event listeners */ - registerEventListeners(): any { + registerEventListeners(): void { 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("resize", () => this.checkResize(), true); + window.addEventListener("orientationchange", () => 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); + updateFocusAfterUserInteraction(event: TouchEvent): void { + const target = (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 @@ -206,16 +218,18 @@ export class Application { 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 { + handleVisibilityChange(event: Event): void { window.focus(); const pageVisible: any = !document[pageHiddenPropName]; if (pageVisible !== this.pageVisible) { @@ -224,31 +238,36 @@ export class Application { this.trackedIsRenderable.set(this.isRenderable()); } } + /** * Handles a mouse move event */ - handleMousemove(event: MouseEvent): any { + handleMousemove(event: MouseEvent): void { this.mousePosition = new Vector(event.clientX, event.clientY); } + /** * Internal on focus handler */ - onFocus(): any { + onFocus(): void { this.focused = true; } + /** * Internal blur handler */ - onBlur(): any { + onBlur(): void { this.focused = false; } + /** * Returns if the app is currently visible */ - isRenderable(): any { + isRenderable(): boolean { return !this.applicationPaused && this.pageVisible; } - onAppRenderableStateChanged(renderable: any): any { + + onAppRenderableStateChanged(renderable: boolean): void { logger.log("Application renderable:", renderable); window.focus(); const currentState: any = this.stateMgr.getCurrentState(); @@ -263,9 +282,11 @@ export class Application { } this.checkResize(); } + this.sound.onPageRenderableStateChanged(renderable); } - onAppPlayingStateChanged(playing: any): any { + + onAppPlayingStateChanged(playing: boolean): void { try { this.adProvider.setPlayStatus(playing); } @@ -273,12 +294,14 @@ export class Application { console.warn("Play status changed"); } } + /** * Internal before-unload handler */ - onBeforeUnload(event: any): any { + onBeforeUnload(event: BeforeUnloadEvent): void { logSection("BEFORE UNLOAD HANDLER", "#f77"); - const currentState: any = this.stateMgr.getCurrentState(); + const currentState: GameState = 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" @@ -287,71 +310,82 @@ export class Application { } } } + /** * Deinitializes the application */ - deinitialize(): any { + deinitialize(): Promise { return this.sound.deinitialize(); } + /** * Background frame update callback */ - onBackgroundFrame(dt: number): any { + onBackgroundFrame(dt: number): void { if (this.isRenderable()) { return; } + const currentState: any = this.stateMgr.getCurrentState(); if (currentState) { currentState.onBackgroundTick(dt); } } + /** * Frame update callback */ - onFrameEmitted(dt: number): any { + onFrameEmitted(dt: number): void { 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; + checkResize(forceUpdate: boolean = false): void { + const w = window.innerWidth; + const h = window.innerHeight; if (this.screenWidth !== w || this.screenHeight !== h || forceUpdate) { this.screenWidth = w; this.screenHeight = h; - const currentState: any = this.stateMgr.getCurrentState(); + const currentState: GameState = this.stateMgr.getCurrentState(); if (currentState) { currentState.onResized(this.screenWidth, this.screenHeight); } - const scale: any = this.getEffectiveUiScale(); + + const scale: number = this.getEffectiveUiScale(); waitNextFrame().then((): any => document.documentElement.style.setProperty("--ui-scale", `${scale}`)); window.focus(); } } + /** * Returns the effective ui sclae */ - getEffectiveUiScale(): any { + getEffectiveUiScale(): number { return this.platformWrapper.getUiScale() * this.settings.getInterfaceScaleValue(); } + /** * Callback after ui scale has changed */ - updateAfterUiScaleChanged(): any { + updateAfterUiScaleChanged(): void { this.checkResize(true); } } diff --git a/src/ts/changelog.ts b/src/ts/changelog.ts index ff3194ef..feeaa0bb 100644 --- a/src/ts/changelog.ts +++ b/src/ts/changelog.ts @@ -1,4 +1,9 @@ -export const CHANGELOG: any = [ +export const CHANGELOG: { + version: string, + date: string, + entries: string[], + skin?: string +}[] = [ { version: "1.5.6", date: "09.12.2022", diff --git a/src/ts/main.ts b/src/ts/main.ts index dd7b22ba..fc66b78e 100644 --- a/src/ts/main.ts +++ b/src/ts/main.ts @@ -1,6 +1,8 @@ import "./core/polyfills"; import "./core/assert"; + import "./mods/modloader"; + import { createLogger, logSection } from "./core/logging"; import { Application } from "./application"; import { IS_DEBUG } from "./core/config"; @@ -10,36 +12,48 @@ import { initItemRegistry } from "./game/item_registry"; import { initMetaBuildingRegistry } from "./game/meta_building_registry"; import { initGameModeRegistry } from "./game/game_mode_registry"; import { initGameSpeedRegistry } from "./game/game_speed_registry"; + const logger: any = createLogger("main"); + if (window.coreThreadLoadedCb) { logger.log("Javascript parsed, calling html thread"); window.coreThreadLoadedCb(); } + console.log(`%cshapez.io ️%c\n© 2022 tobspr Games\nCommit %c${G_BUILD_COMMIT_HASH}%c on %c${new Date(G_BUILD_TIME).toLocaleString()}\n`, "font-size: 35px; font-family: Arial;font-weight: bold; padding: 10px 0;", "color: #aaa", "color: #7f7", "color: #aaa", "color: #7f7"); + console.log("Environment: %c" + G_APP_ENVIRONMENT, "color: #fff"); + if (G_IS_DEV && IS_DEBUG) { console.log("\n%c🛑 DEBUG ENVIRONMENT 🛑\n", "color: #f77"); } + /* typehints:start */ // @ts-ignore throw new Error("typehints built in, this should never be the case!"); /* typehints:end */ + /* dev:start */ console.log("%cDEVCODE BUILT IN", "color: #f77"); /* dev:end */ + logSection("Boot Process", "#f9a825"); + initDrawUtils(); initComponentRegistry(); initItemRegistry(); initMetaBuildingRegistry(); initGameModeRegistry(); initGameSpeedRegistry(); -let app: any = null; + +let app: Application = null; + function bootApp(): any { logger.log("Page Loaded"); app = new Application(); app.boot(); } + if (G_IS_STANDALONE) { window.addEventListener("load", bootApp); } diff --git a/src/ts/translations.ts b/src/ts/translations.ts index 0d033f61..258da923 100644 --- a/src/ts/translations.ts +++ b/src/ts/translations.ts @@ -1,15 +1,18 @@ import { globalConfig } from "./core/config"; import { createLogger } from "./core/logging"; import { LANGUAGES } from "./languages"; + const logger: any = createLogger("translations"); // @ts-ignore const baseTranslations: any = require("./built-temp/base-en.json"); export let T: any = baseTranslations; + if (G_IS_DEV && globalConfig.debug.testTranslations) { // Replaces all translations by fake translations to see whats translated and what not const mapTranslations: any = (obj: any): any => { - for (const key: any in obj) { + for (const key in obj) { const value: any = obj[key]; + if (typeof value === "string") { obj[key] = value.replace(/[a-z]/gi, "x"); } @@ -20,14 +23,16 @@ if (G_IS_DEV && globalConfig.debug.testTranslations) { }; mapTranslations(T); } + // Language key is something like de-DE or en or en-US -function mapLanguageCodeToId(languageKey: any): any { - const key: any = languageKey.toLowerCase(); - const shortKey: any = key.split("-")[0]; +function mapLanguageCodeToId(languageKey: string): string { + const key = languageKey.toLowerCase(); + const shortKey = key.split("-")[0]; + // Try to match by key or short key - for (const id: any in LANGUAGES) { - const data: any = LANGUAGES[id]; - const code: any = data.code.toLowerCase(); + for (const id in LANGUAGES) { + const data = LANGUAGES[id]; + const code = data.code.toLowerCase(); if (code === key) { console.log("-> Match", languageKey, "->", id); return id; @@ -37,11 +42,13 @@ function mapLanguageCodeToId(languageKey: any): any { return id; } } + // If none found, try to find a better alternative by using the base language at least - for (const id: any in LANGUAGES) { - const data: any = LANGUAGES[id]; - const code: any = data.code.toLowerCase(); - const shortCode: any = code.split("-")[0]; + for (const id in LANGUAGES) { + const data = LANGUAGES[id]; + const code = data.code.toLowerCase(); + const shortCode = code.split("-")[0]; + if (shortCode === key) { console.log("-> Desperate Match", languageKey, "->", id); return id; @@ -53,12 +60,9 @@ function mapLanguageCodeToId(languageKey: any): any { } return null; } -/** - * Tries to auto-detect a language - * {} - */ +/** Tries to auto-detect a language */ export function autoDetectLanguageId(): string { - let languages: any = []; + let languages: string[] = []; if (navigator.languages) { languages = navigator.languages.slice(); } @@ -68,24 +72,28 @@ export function autoDetectLanguageId(): string { else { logger.warn("Navigator has no languages prop"); } - for (let i: any = 0; i < languages.length; ++i) { + + for (let i = 0; i < languages.length; ++i) { logger.log("Trying to find language target for", languages[i]); - const trans: any = mapLanguageCodeToId(languages[i]); + const trans = mapLanguageCodeToId(languages[i]); if (trans) { return trans; } } + // Fallback return "en"; } -export function matchDataRecursive(dest: any, src: any, addNewKeys: any = false): any { + +export function matchDataRecursive(dest: any, src: any, addNewKeys: boolean = false): void { if (typeof dest !== "object" || typeof src !== "object") { return; } if (dest === null || src === null) { return; } - for (const key: any in dest) { + + for (const key in dest) { if (src[key]) { // console.log("copy", key); const data: any = dest[key]; @@ -101,21 +109,26 @@ export function matchDataRecursive(dest: any, src: any, addNewKeys: any = false) } } } + if (addNewKeys) { - for (const key: any in src) { + for (const key in src) { if (!dest[key]) { dest[key] = JSON.parse(JSON.stringify(src[key])); } } } } -export function updateApplicationLanguage(id: any): any { + +export function updateApplicationLanguage(id: string): void { logger.log("Setting application language:", id); + const data: any = LANGUAGES[id]; + if (!data) { logger.error("Unknown language:", id); return; } + if (data.data) { logger.log("Applying translations ..."); matchDataRecursive(T, data.data);