diff --git a/src/js/application.js b/src/js/application.js index 1a8ca21f..d9ca7641 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -29,6 +29,7 @@ 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"; /** * @typedef {import("./platform/game_analytics").GameAnalyticsInterface} GameAnalyticsInterface @@ -70,6 +71,9 @@ export class Application { this.inputMgr = new InputDistributor(this); this.backgroundResourceLoader = new BackgroundResourcesLoader(this); + // Restrictions (Like demo etc) + this.restrictionMgr = new RestrictionManager(this); + // Platform dependent stuff /** @type {StorageInterface} */ diff --git a/src/js/core/config.js b/src/js/core/config.js index c01f7c76..cb25890e 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -7,11 +7,6 @@ export const IS_DEBUG = (window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) && window.location.search.indexOf("nodebug") < 0; -export const IS_DEMO = queryParamOptions.fullVersion - ? false - : (!G_IS_DEV && !G_IS_STANDALONE) || - (typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0); - export const SUPPORT_TOUCH = false; const smoothCanvas = true; diff --git a/src/js/core/read_write_proxy.js b/src/js/core/read_write_proxy.js index 6d26fa2b..7c96149b 100644 --- a/src/js/core/read_write_proxy.js +++ b/src/js/core/read_write_proxy.js @@ -81,10 +81,6 @@ export class ReadWriteProxy { return this.writeAsync(); } - getCurrentData() { - return this.currentData; - } - /** * * @param {object} obj diff --git a/src/js/core/restriction_manager.js b/src/js/core/restriction_manager.js new file mode 100644 index 00000000..615e80ab --- /dev/null +++ b/src/js/core/restriction_manager.js @@ -0,0 +1,97 @@ +import { Application } from "../application"; +import { ExplainedResult } from "./explained_result"; +import { queryParamOptions } from "./query_parameters"; +import { ReadWriteProxy } from "./read_write_proxy"; + +export class RestrictionManager extends ReadWriteProxy { + /** + * @param {Application} app + */ + constructor(app) { + super(app, "restriction-flags.bin"); + } + + // -- RW Proxy Impl + + /** + * @param {any} data + */ + verify(data) { + return ExplainedResult.good(); + } + + /** + */ + getDefaultData() { + return { + savegameV1119Imported: false, + }; + } + + /** + */ + getCurrentVersion() { + return 1; + } + + /** + * @param {any} data + */ + migrate(data) { + // Todo + return ExplainedResult.good(); + } + + // -- End RW Proxy Impl + + /** + * Returns if the app is currently running as the limited version + * @returns {boolean} + */ + isLimitedVersion() { + return queryParamOptions.fullVersion + ? false + : (!G_IS_DEV && !G_IS_STANDALONE) || + (typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0); + } + + /** + * Returns if the app markets the standalone version on steam + * @returns {boolean} + */ + getIsStandaloneMarketingActive() { + return this.isLimitedVersion(); + } + + /** + * Returns if exporting the base as a screenshot is possible + * @returns {boolean} + */ + getIsExportingScreenshotsPossible() { + return !this.isLimitedVersion(); + } + + /** + * Returns the maximum number of supported waypoints + * @returns {number} + */ + getMaximumWaypoints() { + return this.isLimitedVersion() ? 2 : 1e20; + } + + /** + * Returns if the user has unlimited savegames + * @returns {boolean} + */ + getHasUnlimitedSavegames() { + return !this.isLimitedVersion(); + } + + /** + * Returns if the app has all settings available + * @returns {boolean} + */ + getHasExtendedSettings() { + return !this.isLimitedVersion(); + } +} diff --git a/src/js/core/utils.js b/src/js/core/utils.js index 30573c81..6982fb28 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -681,3 +681,35 @@ export function fillInLinkIntoTranslation(translation, link) { .replace("", "") .replace("", ""); } + +/** + * Generates a file download + * @param {string} filename + * @param {string} text + */ +export function generateFileDownload(filename, text) { + var element = document.createElement("a"); + element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text)); + element.setAttribute("download", filename); + + element.style.display = "none"; + document.body.appendChild(element); + + element.click(); + document.body.removeChild(element); +} + +/** + * Starts a file chooser + * @param {string} acceptedType + */ +export function startFileChoose(acceptedType = ".bin") { + var input = document.createElement("input"); + input.type = "file"; + input.accept = acceptedType; + + return new Promise(resolve => { + input.onchange = _ => resolve(input.files[0]); + input.click(); + }); +} diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index 75501960..e7a1fea7 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -1,4 +1,4 @@ -import { globalConfig, IS_DEMO } from "../core/config"; +import { globalConfig } from "../core/config"; import { RandomNumberGenerator } from "../core/rng"; import { clamp } from "../core/utils"; import { BasicSerializableObject, types } from "../savegame/serialization"; @@ -281,11 +281,6 @@ export class HubGoals extends BasicSerializableObject { return false; } - if (IS_DEMO && currentLevel >= 4) { - // DEMO - return false; - } - if (G_IS_DEV && globalConfig.debug.upgradesNoCost) { return true; } diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index 9033b3cc..bcfa9b54 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -15,7 +15,7 @@ import { HUDKeybindingOverlay } from "./parts/keybinding_overlay"; import { HUDUnlockNotification } from "./parts/unlock_notification"; import { HUDGameMenu } from "./parts/game_menu"; import { HUDShop } from "./parts/shop"; -import { IS_MOBILE, globalConfig, IS_DEMO } from "../../core/config"; +import { IS_MOBILE, globalConfig } from "../../core/config"; import { HUDMassSelector } from "./parts/mass_selector"; import { HUDVignetteOverlay } from "./parts/vignette_overlay"; import { HUDStatistics } from "./parts/statistics"; @@ -116,7 +116,7 @@ export class GameHUD { this.parts.entityDebugger = new HUDEntityDebugger(this.root); } - if (IS_DEMO) { + if (this.root.app.restrictionMgr.getIsStandaloneMarketingActive()) { this.parts.watermark = new HUDWatermark(this.root); this.parts.standaloneAdvantages = new HUDStandaloneAdvantages(this.root); this.parts.catMemes = new HUDCatMemes(this.root); diff --git a/src/js/game/hud/parts/screenshot_exporter.js b/src/js/game/hud/parts/screenshot_exporter.js index 1cba1208..dd81f8b6 100644 --- a/src/js/game/hud/parts/screenshot_exporter.js +++ b/src/js/game/hud/parts/screenshot_exporter.js @@ -1,13 +1,13 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { KEYMAPPINGS } from "../../key_action_mapper"; -import { IS_DEMO, globalConfig } from "../../../core/config"; -import { T } from "../../../translations"; -import { createLogger } from "../../../core/logging"; -import { StaticMapEntityComponent } from "../../components/static_map_entity"; -import { Vector } from "../../../core/vector"; import { makeOffscreenBuffer } from "../../../core/buffer_utils"; +import { globalConfig } from "../../../core/config"; import { DrawParameters } from "../../../core/draw_parameters"; +import { createLogger } from "../../../core/logging"; import { Rectangle } from "../../../core/rectangle"; +import { Vector } from "../../../core/vector"; +import { T } from "../../../translations"; +import { StaticMapEntityComponent } from "../../components/static_map_entity"; +import { KEYMAPPINGS } from "../../key_action_mapper"; +import { BaseHUDPart } from "../base_hud_part"; const logger = createLogger("screenshot_exporter"); @@ -19,7 +19,7 @@ export class HUDScreenshotExporter extends BaseHUDPart { } startExport() { - if (IS_DEMO) { + if (!this.root.app.restrictionMgr.getIsExportingScreenshotsPossible()) { this.root.hud.parts.dialogs.showFeatureRestrictionInfo(T.demo.features.exportingBase); return; } diff --git a/src/js/game/hud/parts/waypoints.js b/src/js/game/hud/parts/waypoints.js index 1aed7df2..a6f37b93 100644 --- a/src/js/game/hud/parts/waypoints.js +++ b/src/js/game/hud/parts/waypoints.js @@ -1,5 +1,5 @@ import { makeOffscreenBuffer } from "../../../core/buffer_utils"; -import { globalConfig, IS_DEMO, THIRDPARTY_URLS } from "../../../core/config"; +import { globalConfig, THIRDPARTY_URLS } from "../../../core/config"; import { DrawParameters } from "../../../core/draw_parameters"; import { Loader } from "../../../core/loader"; import { DialogWithForm } from "../../../core/modal_dialog_elements"; @@ -302,7 +302,7 @@ export class HUDWaypoints extends BaseHUDPart { // Show info that you can have only N markers in the demo, // actually show this *after* entering the name so you want the // standalone even more (I'm evil :P) - if (IS_DEMO && this.waypoints.length > 2) { + if (this.waypoints.length > this.root.app.restrictionMgr.getMaximumWaypoints()) { this.root.hud.parts.dialogs.showFeatureRestrictionInfo( "", T.dialogs.markerDemoLimit.desc diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index 396d81c9..25cc8390 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -11,6 +11,7 @@ const blueprintShape = "CbCbCbRb:CwCwCwCw"; const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1]; +// @FIXME @TODO const numEndgameUpgrades = !IS_DEMO ? 20 - fixedImprovements.length - 1 : 0; function generateEndgameUpgrades() { diff --git a/src/js/game/systems/hub.js b/src/js/game/systems/hub.js index 2f1f7c05..2002b66e 100644 --- a/src/js/game/systems/hub.js +++ b/src/js/game/systems/hub.js @@ -1,4 +1,4 @@ -import { globalConfig, IS_DEMO } from "../../core/config"; +import { globalConfig } from "../../core/config"; import { smoothenDpi } from "../../core/dpi_manager"; import { DrawParameters } from "../../core/draw_parameters"; import { drawSpriteClipped } from "../../core/draw_utils"; diff --git a/src/js/platform/browser/wrapper.js b/src/js/platform/browser/wrapper.js index 56705025..232a743b 100644 --- a/src/js/platform/browser/wrapper.js +++ b/src/js/platform/browser/wrapper.js @@ -1,214 +1,202 @@ -import { globalConfig, IS_DEMO, IS_MOBILE } from "../../core/config"; -import { createLogger } from "../../core/logging"; -import { queryParamOptions } from "../../core/query_parameters"; -import { clamp } from "../../core/utils"; -import { GamedistributionAdProvider } from "../ad_providers/gamedistribution"; -import { NoAdProvider } from "../ad_providers/no_ad_provider"; -import { PlatformWrapperInterface } from "../wrapper"; -import { StorageImplBrowser } from "./storage"; -import { StorageImplBrowserIndexedDB } from "./storage_indexed_db"; - -const logger = createLogger("platform/browser"); - -export class PlatformWrapperImplBrowser extends PlatformWrapperInterface { - initialize() { - this.recaptchaTokenCallback = null; - - this.embedProvider = { - id: "shapezio-website", - adProvider: NoAdProvider, - iframed: false, - externalLinks: true, - iogLink: true, - unlimitedSavegames: IS_DEMO ? false : true, - showDemoBadge: IS_DEMO, - }; - - if (!G_IS_STANDALONE && queryParamOptions.embedProvider) { - const providerId = queryParamOptions.embedProvider; - this.embedProvider.iframed = true; - this.embedProvider.iogLink = false; - - switch (providerId) { - case "armorgames": { - this.embedProvider.id = "armorgames"; - break; - } - - case "iogames.space": { - this.embedProvider.id = "iogames.space"; - this.embedProvider.iogLink = true; - this.embedProvider.unlimitedSavegames = true; - this.embedProvider.showDemoBadge = false; - break; - } - - case "miniclip": { - this.embedProvider.id = "miniclip"; - break; - } - - case "gamedistribution": { - this.embedProvider.id = "gamedistribution"; - this.embedProvider.externalLinks = false; - this.embedProvider.adProvider = GamedistributionAdProvider; - break; - } - - case "kongregate": { - this.embedProvider.id = "kongregate"; - break; - } - - case "crazygames": { - this.embedProvider.id = "crazygames"; - break; - } - - default: { - logger.error("Got unsupported embed provider:", providerId); - } - } - } - - logger.log("Embed provider:", this.embedProvider.id); - - return this.detectStorageImplementation() - .then(() => this.initializeAdProvider()) - .then(() => super.initialize()); - } - - detectStorageImplementation() { - return new Promise(resolve => { - logger.log("Detecting storage"); - - if (!window.indexedDB) { - logger.log("Indexed DB not supported"); - this.app.storage = new StorageImplBrowser(this.app); - resolve(); - return; - } - - // Try accessing the indexedb - let request; - try { - request = window.indexedDB.open("indexeddb_feature_detection", 1); - } catch (ex) { - logger.warn("Error while opening indexed db:", ex); - this.app.storage = new StorageImplBrowser(this.app); - resolve(); - return; - } - request.onerror = err => { - logger.log("Indexed DB can *not* be accessed: ", err); - logger.log("Using fallback to local storage"); - this.app.storage = new StorageImplBrowser(this.app); - resolve(); - }; - request.onsuccess = () => { - logger.log("Indexed DB *can* be accessed"); - this.app.storage = new StorageImplBrowserIndexedDB(this.app); - resolve(); - }; - }); - } - - getHasUnlimitedSavegames() { - return this.embedProvider.unlimitedSavegames; - } - - getShowDemoBadges() { - return this.embedProvider.showDemoBadge; - } - - getId() { - return "browser@" + this.embedProvider.id; - } - - getUiScale() { - if (IS_MOBILE) { - return 1; - } - - const avgDims = Math.min(this.app.screenWidth, this.app.screenHeight); - return clamp((avgDims / 1000.0) * 1.9, 0.1, 10); - } - - getSupportsRestart() { - return true; - } - - getTouchPanStrength() { - return IS_MOBILE ? 1 : 0.5; - } - - openExternalLink(url, force = false) { - logger.log("Opening external:", url); - if (force || this.embedProvider.externalLinks) { - window.open(url); - } else { - // Do nothing - alert( - "This platform does not allow opening external links. You can play on https://shapez.io directly to open them.\n\nClicked Link: " + - url - ); - } - } - - performRestart() { - logger.log("Performing restart"); - window.location.reload(true); - } - - /** - * Detects if there is an adblocker installed - * @returns {Promise} - */ - detectAdblock() { - return Promise.race([ - new Promise(resolve => { - // If the request wasn't blocked within a very short period of time, this means - // the adblocker is not active and the request was actually made -> ignore it then - setTimeout(() => resolve(false), 30); - }), - new Promise(resolve => { - fetch("https://googleads.g.doubleclick.net/pagead/id", { - method: "HEAD", - mode: "no-cors", - }) - .then(res => { - resolve(false); - }) - .catch(err => { - resolve(true); - }); - }), - ]); - } - - initializeAdProvider() { - if (G_IS_DEV && !globalConfig.debug.testAds) { - logger.log("Ads disabled in local environment"); - return Promise.resolve(); - } - - // First, detect adblocker - return this.detectAdblock().then(hasAdblocker => { - if (hasAdblocker) { - logger.log("Adblock detected"); - return; - } - - const adProvider = this.embedProvider.adProvider; - this.app.adProvider = new adProvider(this.app); - return this.app.adProvider.initialize().catch(err => { - logger.error("Failed to initialize ad provider, disabling ads:", err); - this.app.adProvider = new NoAdProvider(this.app); - }); - }); - } - - exitApp() { - // Can not exit app - } -} +import { globalConfig, IS_MOBILE } from "../../core/config"; +import { createLogger } from "../../core/logging"; +import { queryParamOptions } from "../../core/query_parameters"; +import { clamp } from "../../core/utils"; +import { GamedistributionAdProvider } from "../ad_providers/gamedistribution"; +import { NoAdProvider } from "../ad_providers/no_ad_provider"; +import { PlatformWrapperInterface } from "../wrapper"; +import { StorageImplBrowser } from "./storage"; +import { StorageImplBrowserIndexedDB } from "./storage_indexed_db"; + +const logger = createLogger("platform/browser"); + +export class PlatformWrapperImplBrowser extends PlatformWrapperInterface { + initialize() { + this.recaptchaTokenCallback = null; + + this.embedProvider = { + id: "shapezio-website", + adProvider: NoAdProvider, + iframed: false, + externalLinks: true, + iogLink: true, + }; + + if (!G_IS_STANDALONE && queryParamOptions.embedProvider) { + const providerId = queryParamOptions.embedProvider; + this.embedProvider.iframed = true; + this.embedProvider.iogLink = false; + + switch (providerId) { + case "armorgames": { + this.embedProvider.id = "armorgames"; + break; + } + + case "iogames.space": { + this.embedProvider.id = "iogames.space"; + this.embedProvider.iogLink = true; + break; + } + + case "miniclip": { + this.embedProvider.id = "miniclip"; + break; + } + + case "gamedistribution": { + this.embedProvider.id = "gamedistribution"; + this.embedProvider.externalLinks = false; + this.embedProvider.adProvider = GamedistributionAdProvider; + break; + } + + case "kongregate": { + this.embedProvider.id = "kongregate"; + break; + } + + case "crazygames": { + this.embedProvider.id = "crazygames"; + break; + } + + default: { + logger.error("Got unsupported embed provider:", providerId); + } + } + } + + logger.log("Embed provider:", this.embedProvider.id); + + return this.detectStorageImplementation() + .then(() => this.initializeAdProvider()) + .then(() => super.initialize()); + } + + detectStorageImplementation() { + return new Promise(resolve => { + logger.log("Detecting storage"); + + if (!window.indexedDB) { + logger.log("Indexed DB not supported"); + this.app.storage = new StorageImplBrowser(this.app); + resolve(); + return; + } + + // Try accessing the indexedb + let request; + try { + request = window.indexedDB.open("indexeddb_feature_detection", 1); + } catch (ex) { + logger.warn("Error while opening indexed db:", ex); + this.app.storage = new StorageImplBrowser(this.app); + resolve(); + return; + } + request.onerror = err => { + logger.log("Indexed DB can *not* be accessed: ", err); + logger.log("Using fallback to local storage"); + this.app.storage = new StorageImplBrowser(this.app); + resolve(); + }; + request.onsuccess = () => { + logger.log("Indexed DB *can* be accessed"); + this.app.storage = new StorageImplBrowserIndexedDB(this.app); + resolve(); + }; + }); + } + + getId() { + return "browser@" + this.embedProvider.id; + } + + getUiScale() { + if (IS_MOBILE) { + return 1; + } + + const avgDims = Math.min(this.app.screenWidth, this.app.screenHeight); + return clamp((avgDims / 1000.0) * 1.9, 0.1, 10); + } + + getSupportsRestart() { + return true; + } + + getTouchPanStrength() { + return IS_MOBILE ? 1 : 0.5; + } + + openExternalLink(url, force = false) { + logger.log("Opening external:", url); + if (force || this.embedProvider.externalLinks) { + window.open(url); + } else { + // Do nothing + alert( + "This platform does not allow opening external links. You can play on https://shapez.io directly to open them.\n\nClicked Link: " + + url + ); + } + } + + performRestart() { + logger.log("Performing restart"); + window.location.reload(true); + } + + /** + * Detects if there is an adblocker installed + * @returns {Promise} + */ + detectAdblock() { + return Promise.race([ + new Promise(resolve => { + // If the request wasn't blocked within a very short period of time, this means + // the adblocker is not active and the request was actually made -> ignore it then + setTimeout(() => resolve(false), 30); + }), + new Promise(resolve => { + fetch("https://googleads.g.doubleclick.net/pagead/id", { + method: "HEAD", + mode: "no-cors", + }) + .then(res => { + resolve(false); + }) + .catch(err => { + resolve(true); + }); + }), + ]); + } + + initializeAdProvider() { + if (G_IS_DEV && !globalConfig.debug.testAds) { + logger.log("Ads disabled in local environment"); + return Promise.resolve(); + } + + // First, detect adblocker + return this.detectAdblock().then(hasAdblocker => { + if (hasAdblocker) { + logger.log("Adblock detected"); + return; + } + + const adProvider = this.embedProvider.adProvider; + this.app.adProvider = new adProvider(this.app); + return this.app.adProvider.initialize().catch(err => { + logger.error("Failed to initialize ad provider, disabling ads:", err); + this.app.adProvider = new NoAdProvider(this.app); + }); + }); + } + + exitApp() { + // Can not exit app + } +} diff --git a/src/js/platform/electron/wrapper.js b/src/js/platform/electron/wrapper.js index 69bc9695..941aff44 100644 --- a/src/js/platform/electron/wrapper.js +++ b/src/js/platform/electron/wrapper.js @@ -1,65 +1,57 @@ -import { PlatformWrapperImplBrowser } from "../browser/wrapper"; -import { getIPCRenderer } from "../../core/utils"; -import { createLogger } from "../../core/logging"; -import { StorageImplElectron } from "./storage"; -import { PlatformWrapperInterface } from "../wrapper"; - -const logger = createLogger("electron-wrapper"); - -export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser { - initialize() { - this.app.storage = new StorageImplElectron(this); - return PlatformWrapperInterface.prototype.initialize.call(this); - } - - getId() { - return "electron"; - } - - getSupportsRestart() { - return true; - } - - openExternalLink(url) { - logger.log(this, "Opening external:", url); - window.open(url, "about:blank"); - } - - getSupportsAds() { - return false; - } - - getHasUnlimitedSavegames() { - return true; - } - - getShowDemoBadges() { - return false; - } - - performRestart() { - logger.log(this, "Performing restart"); - window.location.reload(true); - } - - initializeAdProvider() { - return Promise.resolve(); - } - - getSupportsFullscreen() { - return true; - } - - setFullscreen(flag) { - getIPCRenderer().send("set-fullscreen", flag); - } - - getSupportsAppExit() { - return true; - } - - exitApp() { - logger.log(this, "Sending app exit signal"); - getIPCRenderer().send("exit-app"); - } -} +import { PlatformWrapperImplBrowser } from "../browser/wrapper"; +import { getIPCRenderer } from "../../core/utils"; +import { createLogger } from "../../core/logging"; +import { StorageImplElectron } from "./storage"; +import { PlatformWrapperInterface } from "../wrapper"; + +const logger = createLogger("electron-wrapper"); + +export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser { + initialize() { + this.app.storage = new StorageImplElectron(this); + return PlatformWrapperInterface.prototype.initialize.call(this); + } + + getId() { + return "electron"; + } + + getSupportsRestart() { + return true; + } + + openExternalLink(url) { + logger.log(this, "Opening external:", url); + window.open(url, "about:blank"); + } + + getSupportsAds() { + return false; + } + + performRestart() { + logger.log(this, "Performing restart"); + window.location.reload(true); + } + + initializeAdProvider() { + return Promise.resolve(); + } + + getSupportsFullscreen() { + return true; + } + + setFullscreen(flag) { + getIPCRenderer().send("set-fullscreen", flag); + } + + getSupportsAppExit() { + return true; + } + + exitApp() { + logger.log(this, "Sending app exit signal"); + getIPCRenderer().send("exit-app"); + } +} diff --git a/src/js/platform/sound.js b/src/js/platform/sound.js index 51dca8d4..9d5a8461 100644 --- a/src/js/platform/sound.js +++ b/src/js/platform/sound.js @@ -6,7 +6,7 @@ import { GameRoot } from "../game/root"; import { newEmptyMap, clamp } from "../core/utils"; import { createLogger } from "../core/logging"; -import { globalConfig, IS_DEMO } from "../core/config"; +import { globalConfig } from "../core/config"; const logger = createLogger("sound"); @@ -29,7 +29,9 @@ export const SOUNDS = { }; export const MUSIC = { - theme: IS_DEMO ? "theme-short" : "theme-full", + // The theme always depends on the standalone only, even if running the full + // version in the browser + theme: G_IS_STANDALONE ? "theme-full" : "theme-short", menu: "menu", }; diff --git a/src/js/platform/wrapper.js b/src/js/platform/wrapper.js index 9c35a8e4..f80c2fd6 100644 --- a/src/js/platform/wrapper.js +++ b/src/js/platform/wrapper.js @@ -1,142 +1,131 @@ -/* typehints:start */ -import { Application } from "../application"; -/* typehints:end */ - -import { IS_MOBILE } from "../core/config"; - -export class PlatformWrapperInterface { - constructor(app) { - /** @type {Application} */ - this.app = app; - } - - /** @returns {string} */ - getId() { - abstract; - return "unknown-platform"; - } - - /** - * Returns the UI scale, called on every resize - * @returns {number} */ - getUiScale() { - return 1; - } - - /** @returns {boolean} */ - getSupportsRestart() { - abstract; - return false; - } - - /** - * Whether the user has unlimited savegames - */ - getHasUnlimitedSavegames() { - return true; - } - - getShowDemoBadges() { - return false; - } - - /** - * Returns the strength of touch pans with the mouse - */ - getTouchPanStrength() { - return 1; - } - - /** @returns {Promise} */ - initialize() { - document.documentElement.classList.add("p-" + this.getId()); - return Promise.resolve(); - } - - /** - * Should initialize the apps ad provider in case supported - * @returns {Promise} - */ - initializeAdProvider() { - return Promise.resolve(); - } - - /** - * Should return the minimum supported zoom level - * @returns {number} - */ - getMinimumZoom() { - return 0.1 * this.getScreenScale(); - } - - /** - * Should return the maximum supported zoom level - * @returns {number} - */ - getMaximumZoom() { - return 3.5 * this.getScreenScale(); - } - - getScreenScale() { - return Math.min(window.innerWidth, window.innerHeight) / 1024.0; - } - - /** - * Should return if this platform supports ads at all - */ - getSupportsAds() { - return false; - } - - /** - * Attempt to open an external url - * @param {string} url - * @param {boolean=} force Whether to always open the url even if not allowed - */ - openExternalLink(url, force = false) { - abstract; - } - - /** - * Attempt to restart the app - */ - performRestart() { - abstract; - } - - /** - * Returns whether this platform supports a toggleable fullscreen - */ - getSupportsFullscreen() { - return false; - } - - /** - * Should set the apps fullscreen state to the desired state - * @param {boolean} flag - */ - setFullscreen(flag) { - abstract; - } - - /** - * Returns whether this platform supports quitting the app - */ - getSupportsAppExit() { - return false; - } - - /** - * Attempts to quit the app - */ - exitApp() { - abstract; - } - - /** - * Whether this platform supports a keyboard - */ - getSupportsKeyboard() { - return !IS_MOBILE; - } -} +/* typehints:start */ +import { Application } from "../application"; +/* typehints:end */ + +import { IS_MOBILE } from "../core/config"; + +export class PlatformWrapperInterface { + constructor(app) { + /** @type {Application} */ + this.app = app; + } + + /** @returns {string} */ + getId() { + abstract; + return "unknown-platform"; + } + + /** + * Returns the UI scale, called on every resize + * @returns {number} */ + getUiScale() { + return 1; + } + + /** @returns {boolean} */ + getSupportsRestart() { + abstract; + return false; + } + + /** + * Returns the strength of touch pans with the mouse + */ + getTouchPanStrength() { + return 1; + } + + /** @returns {Promise} */ + initialize() { + document.documentElement.classList.add("p-" + this.getId()); + return Promise.resolve(); + } + + /** + * Should initialize the apps ad provider in case supported + * @returns {Promise} + */ + initializeAdProvider() { + return Promise.resolve(); + } + + /** + * Should return the minimum supported zoom level + * @returns {number} + */ + getMinimumZoom() { + return 0.1 * this.getScreenScale(); + } + + /** + * Should return the maximum supported zoom level + * @returns {number} + */ + getMaximumZoom() { + return 3.5 * this.getScreenScale(); + } + + getScreenScale() { + return Math.min(window.innerWidth, window.innerHeight) / 1024.0; + } + + /** + * Should return if this platform supports ads at all + */ + getSupportsAds() { + return false; + } + + /** + * Attempt to open an external url + * @param {string} url + * @param {boolean=} force Whether to always open the url even if not allowed + */ + openExternalLink(url, force = false) { + abstract; + } + + /** + * Attempt to restart the app + */ + performRestart() { + abstract; + } + + /** + * Returns whether this platform supports a toggleable fullscreen + */ + getSupportsFullscreen() { + return false; + } + + /** + * Should set the apps fullscreen state to the desired state + * @param {boolean} flag + */ + setFullscreen(flag) { + abstract; + } + + /** + * Returns whether this platform supports quitting the app + */ + getSupportsAppExit() { + return false; + } + + /** + * Attempts to quit the app + */ + exitApp() { + abstract; + } + + /** + * Whether this platform supports a keyboard + */ + getSupportsKeyboard() { + return !IS_MOBILE; + } +} diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index ace30eff..04cde3d9 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -6,8 +6,7 @@ import { ReadWriteProxy } from "../core/read_write_proxy"; import { BoolSetting, EnumSetting, RangeSetting, BaseSetting } from "./setting_types"; import { createLogger } from "../core/logging"; import { ExplainedResult } from "../core/explained_result"; -import { THEMES, THEME, applyGameTheme } from "../game/theme"; -import { IS_DEMO } from "../core/config"; +import { THEMES, applyGameTheme } from "../game/theme"; import { T } from "../translations"; import { LANGUAGES } from "../languages"; @@ -187,7 +186,9 @@ export const allApplicationSettings = [ app.platformWrapper.setFullscreen(value); } }, - !IS_DEMO + /** + * @param {Application} app + */ app => app.restrictionMgr.getHasExtendedSettings() ), new BoolSetting( @@ -215,7 +216,9 @@ export const allApplicationSettings = [ applyGameTheme(id); document.documentElement.setAttribute("data-theme", id); }, - enabled: !IS_DEMO, + enabledCb: /** + * @param {Application} app + */ app => app.restrictionMgr.getHasExtendedSettings(), }), new EnumSetting("autosaveInterval", { @@ -271,7 +274,9 @@ export const allApplicationSettings = [ category: enumCategories.performance, restartRequired: false, changeCb: (app, id) => {}, - enabled: !IS_DEMO, + enabledCb: /** + * @param {Application} app + */ app => app.restrictionMgr.getHasExtendedSettings(), }), new BoolSetting("lowQualityMapResources", enumCategories.performance, (app, value) => {}), @@ -355,7 +360,7 @@ export class ApplicationSettings extends ReadWriteProxy { * @returns {SettingsStorage} */ getAllSettings() { - return this.getCurrentData().settings; + return this.currentData.settings; } /** diff --git a/src/js/profile/setting_types.js b/src/js/profile/setting_types.js index 7d07ca99..66c9f01d 100644 --- a/src/js/profile/setting_types.js +++ b/src/js/profile/setting_types.js @@ -13,13 +13,13 @@ export class BaseSetting { * @param {string} id * @param {string} categoryId * @param {function(Application, any):void} changeCb - * @param {boolean} enabled + * @param {function(Application) : boolean=} enabledCb */ - constructor(id, categoryId, changeCb, enabled) { + constructor(id, categoryId, changeCb, enabledCb = null) { this.id = id; this.categoryId = categoryId; this.changeCb = changeCb; - this.enabled = enabled; + this.enabledCb = enabledCb; /** @type {Application} */ this.app = null; @@ -39,6 +39,7 @@ export class BaseSetting { } /** + * Binds all parameters * @param {Application} app * @param {HTMLElement} element * @param {any} dialogs @@ -49,19 +50,37 @@ export class BaseSetting { this.dialogs = dialogs; } - getHtml() { + /** + * Returns the HTML for this setting + * @param {Application} app + */ + getHtml(app) { abstract; return ""; } + /** + * Returns whether this setting is enabled and available + * @param {Application} app + */ + getIsAvailable(app) { + return this.enabledCb ? this.enabledCb(app) : true; + } + syncValueToElement() { abstract; } + /** + * Attempts to modify the setting + */ modify() { abstract; } + /** + * Shows the dialog that a restart is required + */ showRestartRequiredDialog() { const { restart } = this.dialogs.showInfo( T.dialogs.restartRequired.title, @@ -74,6 +93,7 @@ export class BaseSetting { } /** + * Validates the set value * @param {any} value * @returns {boolean} */ @@ -96,10 +116,10 @@ export class EnumSetting extends BaseSetting { iconPrefix = null, changeCb = null, magicValue = null, - enabled = true, + enabledCb = null, } ) { - super(id, category, changeCb, enabled); + super(id, category, changeCb, enabledCb); this.options = options; this.valueGetter = valueGetter; @@ -110,10 +130,14 @@ export class EnumSetting extends BaseSetting { this.magicValue = magicValue; } - getHtml() { + /** + * @param {Application} app + */ + getHtml(app) { + const available = this.getIsAvailable(app); return ` -
- ${this.enabled ? "" : `${T.demo.settingNotAvailable}`} +
+ ${available ? "" : `${T.demo.settingNotAvailable}`}
@@ -180,14 +204,18 @@ export class EnumSetting extends BaseSetting { } export class BoolSetting extends BaseSetting { - constructor(id, category, changeCb = null, enabled = true) { - super(id, category, changeCb, enabled); + constructor(id, category, changeCb = null, enabledCb = null) { + super(id, category, changeCb, enabledCb); } - getHtml() { + /** + * @param {Application} app + */ + getHtml(app) { + const available = this.getIsAvailable(app); return ` -
- ${this.enabled ? "" : `${T.demo.settingNotAvailable}`} +
+ ${available ? "" : `${T.demo.settingNotAvailable}`}
@@ -226,13 +254,13 @@ export class RangeSetting extends BaseSetting { id, category, changeCb = null, - enabled = true, defaultValue = 1.0, minValue = 0, maxValue = 1.0, - stepSize = 0.0001 + stepSize = 0.0001, + enabledCb = null ) { - super(id, category, changeCb, enabled); + super(id, category, changeCb, enabledCb); this.defaultValue = defaultValue; this.minValue = minValue; @@ -240,10 +268,14 @@ export class RangeSetting extends BaseSetting { this.stepSize = stepSize; } - getHtml() { + /** + * @param {Application} app + */ + getHtml(app) { + const available = this.getIsAvailable(app); return ` -
- ${this.enabled ? "" : `${T.demo.settingNotAvailable}`} +
+ ${available ? "" : `${T.demo.settingNotAvailable}`}
diff --git a/src/js/savegame/savegame_manager.js b/src/js/savegame/savegame_manager.js index 52f9dc14..587d173f 100644 --- a/src/js/savegame/savegame_manager.js +++ b/src/js/savegame/savegame_manager.js @@ -40,13 +40,6 @@ export class SavegameManager extends ReadWriteProxy { return 1002; } - /** - * @returns {SavegamesData} - */ - getCurrentData() { - return super.getCurrentData(); - } - verify(data) { // TODO / FIXME!!!! return ExplainedResult.good(); diff --git a/src/js/states/keybindings.js b/src/js/states/keybindings.js index b68626c7..a01629f1 100644 --- a/src/js/states/keybindings.js +++ b/src/js/states/keybindings.js @@ -1,179 +1,173 @@ -import { TextualGameState } from "../core/textual_game_state"; -import { SOUNDS } from "../platform/sound"; -import { T } from "../translations"; -import { KEYMAPPINGS, getStringForKeyCode } from "../game/key_action_mapper"; -import { Dialog } from "../core/modal_dialog_elements"; -import { IS_DEMO } from "../core/config"; - -export class KeybindingsState extends TextualGameState { - constructor() { - super("KeybindingsState"); - } - - getStateHeaderTitle() { - return T.keybindings.title; - } - - getMainContentHTML() { - return ` - -
- ${T.keybindings.hint} - - -
- -
- -
- `; - } - - onEnter() { - const keybindingsElem = this.htmlElement.querySelector(".keybindings"); - - this.trackClicks(this.htmlElement.querySelector(".resetBindings"), this.resetBindings); - - for (const category in KEYMAPPINGS) { - const categoryDiv = document.createElement("div"); - categoryDiv.classList.add("category"); - keybindingsElem.appendChild(categoryDiv); - - const labelDiv = document.createElement("strong"); - labelDiv.innerText = T.keybindings.categoryLabels[category]; - labelDiv.classList.add("categoryLabel"); - categoryDiv.appendChild(labelDiv); - - for (const keybindingId in KEYMAPPINGS[category]) { - const mapped = KEYMAPPINGS[category][keybindingId]; - - const elem = document.createElement("div"); - elem.classList.add("entry"); - elem.setAttribute("data-keybinding", keybindingId); - categoryDiv.appendChild(elem); - - const title = document.createElement("span"); - title.classList.add("title"); - title.innerText = T.keybindings.mappings[keybindingId]; - elem.appendChild(title); - - const mappingDiv = document.createElement("span"); - mappingDiv.classList.add("mapping"); - elem.appendChild(mappingDiv); - - const editBtn = document.createElement("button"); - editBtn.classList.add("styledButton", "editKeybinding"); - - const resetBtn = document.createElement("button"); - resetBtn.classList.add("styledButton", "resetKeybinding"); - - if (mapped.builtin) { - editBtn.classList.add("disabled"); - resetBtn.classList.add("disabled"); - } else { - this.trackClicks(editBtn, () => this.editKeybinding(keybindingId)); - this.trackClicks(resetBtn, () => this.resetKeybinding(keybindingId)); - } - elem.appendChild(editBtn); - elem.appendChild(resetBtn); - } - } - this.updateKeybindings(); - } - - editKeybinding(id) { - // if (IS_DEMO) { - // this.dialogs.showFeatureRestrictionInfo(T.demo.features.customizeKeybindings); - // return; - // } - - const dialog = new Dialog({ - app: this.app, - title: T.dialogs.editKeybinding.title, - contentHTML: T.dialogs.editKeybinding.desc, - buttons: ["cancel:good"], - type: "info", - }); - - dialog.inputReciever.keydown.add(({ keyCode, shift, alt, event }) => { - if (keyCode === 27) { - this.dialogs.closeDialog(dialog); - return; - } - - if (event) { - event.preventDefault(); - } - - if (event.target && event.target.tagName === "BUTTON" && keyCode === 1) { - return; - } - - if ( - // Enter - keyCode === 13 - ) { - // Ignore builtins - return; - } - - this.app.settings.updateKeybindingOverride(id, keyCode); - - this.dialogs.closeDialog(dialog); - this.updateKeybindings(); - }); - - dialog.inputReciever.backButton.add(() => {}); - this.dialogs.internalShowDialog(dialog); - - this.app.sound.playUiSound(SOUNDS.dialogOk); - } - - updateKeybindings() { - const overrides = this.app.settings.getKeybindingOverrides(); - for (const category in KEYMAPPINGS) { - for (const keybindingId in KEYMAPPINGS[category]) { - const mapped = KEYMAPPINGS[category][keybindingId]; - - const container = this.htmlElement.querySelector("[data-keybinding='" + keybindingId + "']"); - assert(container, "Container for keybinding not found: " + keybindingId); - - let keyCode = mapped.keyCode; - if (overrides[keybindingId]) { - keyCode = overrides[keybindingId]; - } - - const mappingDiv = container.querySelector(".mapping"); - mappingDiv.innerHTML = getStringForKeyCode(keyCode); - mappingDiv.classList.toggle("changed", !!overrides[keybindingId]); - - const resetBtn = container.querySelector("button.resetKeybinding"); - resetBtn.classList.toggle("disabled", mapped.builtin || !overrides[keybindingId]); - } - } - } - - resetKeybinding(id) { - this.app.settings.resetKeybindingOverride(id); - this.updateKeybindings(); - } - - resetBindings() { - const { reset } = this.dialogs.showWarning( - T.dialogs.resetKeybindingsConfirmation.title, - T.dialogs.resetKeybindingsConfirmation.desc, - ["cancel:good", "reset:bad"] - ); - - reset.add(() => { - this.app.settings.resetKeybindingOverrides(); - this.updateKeybindings(); - - this.dialogs.showInfo(T.dialogs.keybindingsResetOk.title, T.dialogs.keybindingsResetOk.desc); - }); - } - - getDefaultPreviousState() { - return "SettingsState"; - } -} +import { Dialog } from "../core/modal_dialog_elements"; +import { TextualGameState } from "../core/textual_game_state"; +import { getStringForKeyCode, KEYMAPPINGS } from "../game/key_action_mapper"; +import { SOUNDS } from "../platform/sound"; +import { T } from "../translations"; + +export class KeybindingsState extends TextualGameState { + constructor() { + super("KeybindingsState"); + } + + getStateHeaderTitle() { + return T.keybindings.title; + } + + getMainContentHTML() { + return ` + +
+ ${T.keybindings.hint} + + +
+ +
+ +
+ `; + } + + onEnter() { + const keybindingsElem = this.htmlElement.querySelector(".keybindings"); + + this.trackClicks(this.htmlElement.querySelector(".resetBindings"), this.resetBindings); + + for (const category in KEYMAPPINGS) { + const categoryDiv = document.createElement("div"); + categoryDiv.classList.add("category"); + keybindingsElem.appendChild(categoryDiv); + + const labelDiv = document.createElement("strong"); + labelDiv.innerText = T.keybindings.categoryLabels[category]; + labelDiv.classList.add("categoryLabel"); + categoryDiv.appendChild(labelDiv); + + for (const keybindingId in KEYMAPPINGS[category]) { + const mapped = KEYMAPPINGS[category][keybindingId]; + + const elem = document.createElement("div"); + elem.classList.add("entry"); + elem.setAttribute("data-keybinding", keybindingId); + categoryDiv.appendChild(elem); + + const title = document.createElement("span"); + title.classList.add("title"); + title.innerText = T.keybindings.mappings[keybindingId]; + elem.appendChild(title); + + const mappingDiv = document.createElement("span"); + mappingDiv.classList.add("mapping"); + elem.appendChild(mappingDiv); + + const editBtn = document.createElement("button"); + editBtn.classList.add("styledButton", "editKeybinding"); + + const resetBtn = document.createElement("button"); + resetBtn.classList.add("styledButton", "resetKeybinding"); + + if (mapped.builtin) { + editBtn.classList.add("disabled"); + resetBtn.classList.add("disabled"); + } else { + this.trackClicks(editBtn, () => this.editKeybinding(keybindingId)); + this.trackClicks(resetBtn, () => this.resetKeybinding(keybindingId)); + } + elem.appendChild(editBtn); + elem.appendChild(resetBtn); + } + } + this.updateKeybindings(); + } + + editKeybinding(id) { + const dialog = new Dialog({ + app: this.app, + title: T.dialogs.editKeybinding.title, + contentHTML: T.dialogs.editKeybinding.desc, + buttons: ["cancel:good"], + type: "info", + }); + + dialog.inputReciever.keydown.add(({ keyCode, shift, alt, event }) => { + if (keyCode === 27) { + this.dialogs.closeDialog(dialog); + return; + } + + if (event) { + event.preventDefault(); + } + + if (event.target && event.target.tagName === "BUTTON" && keyCode === 1) { + return; + } + + if ( + // Enter + keyCode === 13 + ) { + // Ignore builtins + return; + } + + this.app.settings.updateKeybindingOverride(id, keyCode); + + this.dialogs.closeDialog(dialog); + this.updateKeybindings(); + }); + + dialog.inputReciever.backButton.add(() => {}); + this.dialogs.internalShowDialog(dialog); + + this.app.sound.playUiSound(SOUNDS.dialogOk); + } + + updateKeybindings() { + const overrides = this.app.settings.getKeybindingOverrides(); + for (const category in KEYMAPPINGS) { + for (const keybindingId in KEYMAPPINGS[category]) { + const mapped = KEYMAPPINGS[category][keybindingId]; + + const container = this.htmlElement.querySelector("[data-keybinding='" + keybindingId + "']"); + assert(container, "Container for keybinding not found: " + keybindingId); + + let keyCode = mapped.keyCode; + if (overrides[keybindingId]) { + keyCode = overrides[keybindingId]; + } + + const mappingDiv = container.querySelector(".mapping"); + mappingDiv.innerHTML = getStringForKeyCode(keyCode); + mappingDiv.classList.toggle("changed", !!overrides[keybindingId]); + + const resetBtn = container.querySelector("button.resetKeybinding"); + resetBtn.classList.toggle("disabled", mapped.builtin || !overrides[keybindingId]); + } + } + } + + resetKeybinding(id) { + this.app.settings.resetKeybindingOverride(id); + this.updateKeybindings(); + } + + resetBindings() { + const { reset } = this.dialogs.showWarning( + T.dialogs.resetKeybindingsConfirmation.title, + T.dialogs.resetKeybindingsConfirmation.desc, + ["cancel:good", "reset:bad"] + ); + + reset.add(() => { + this.app.settings.resetKeybindingOverrides(); + this.updateKeybindings(); + + this.dialogs.showInfo(T.dialogs.keybindingsResetOk.title, T.dialogs.keybindingsResetOk.desc); + }); + } + + getDefaultPreviousState() { + return "SettingsState"; + } +} diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 0d186ffd..a3f06ca0 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -1,21 +1,23 @@ -import { GameState } from "../core/game_state"; import { cachebust } from "../core/cachebust"; -import { A_B_TESTING_LINK_TYPE, globalConfig, IS_DEMO, THIRDPARTY_URLS } from "../core/config"; +import { A_B_TESTING_LINK_TYPE, globalConfig, THIRDPARTY_URLS } from "../core/config"; +import { GameState } from "../core/game_state"; +import { DialogWithForm } from "../core/modal_dialog_elements"; +import { FormElementInput } from "../core/modal_dialog_forms"; +import { ReadWriteProxy } from "../core/read_write_proxy"; import { - makeDiv, - makeButtonElement, formatSecondsToTimeAgo, - waitNextFrame, + generateFileDownload, isSupportedBrowser, makeButton, + makeButtonElement, + makeDiv, removeAllChildren, + startFileChoose, + waitNextFrame, } from "../core/utils"; -import { ReadWriteProxy } from "../core/read_write_proxy"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; -import { T } from "../translations"; import { getApplicationSettingById } from "../profile/application_settings"; -import { FormElementInput } from "../core/modal_dialog_forms"; -import { DialogWithForm } from "../core/modal_dialog_elements"; +import { T } from "../translations"; const trim = require("trim"); @@ -24,23 +26,6 @@ const trim = require("trim"); * @typedef {import("../profile/setting_types").EnumSetting} EnumSetting */ -/** - * Generates a file download - * @param {string} filename - * @param {string} text - */ -function generateFileDownload(filename, text) { - var element = document.createElement("a"); - element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text)); - element.setAttribute("download", filename); - - element.style.display = "none"; - document.body.appendChild(element); - - element.click(); - document.body.removeChild(element); -} - export class MainMenuState extends GameState { constructor() { super("MainMenuState"); @@ -49,18 +34,16 @@ export class MainMenuState extends GameState { getInnerHTML() { const bannerHtml = `

${T.demoBanners.title}

-

${T.demoBanners.intro}

- Get the shapez.io standalone! `; - return ` + const showDemoBadges = this.app.restrictionMgr.getIsStandaloneMarketingActive(); + return `
- ${ G_IS_STANDALONE || G_IS_DEV ? ` @@ -74,17 +57,14 @@ export class MainMenuState extends GameState { - - -
- +
- ${IS_DEMO ? `
${bannerHtml}
` : ""} + ${showDemoBadges ? `
${bannerHtml}
` : ""}
@@ -95,12 +75,9 @@ export class MainMenuState extends GameState { }
- -
-
`; } + /** + * Asks the user to import a savegame + */ requestImportSavegame() { if ( - IS_DEMO && this.app.savegameMgr.getSavegamesMetaData().length > 0 && - !this.app.platformWrapper.getHasUnlimitedSavegames() + !this.app.restrictionMgr.getHasUnlimitedSavegames() ) { this.app.analytics.trackUiClick("importgame_slot_limit_show"); this.showSavegameSlotLimit(); return; } - var input = document.createElement("input"); - input.type = "file"; - input.accept = ".bin"; - - input.onchange = e => { - const file = input.files[0]; + // Create a 'fake' file-input to accept savegames + startFileChoose(".bin").then(file => { if (file) { + const closeLoader = this.dialogs.showLoadingDialog(); waitNextFrame().then(() => { this.app.analytics.trackUiClick("import_savegame"); - const closeLoader = this.dialogs.showLoadingDialog(); const reader = new FileReader(); reader.addEventListener("load", event => { const contents = event.target.result; @@ -194,8 +168,7 @@ export class MainMenuState extends GameState { reader.readAsText(file, "utf-8"); }); } - }; - input.click(); + }); } onBackButton() { @@ -557,9 +530,8 @@ export class MainMenuState extends GameState { onPlayButtonClicked() { if ( - IS_DEMO && this.app.savegameMgr.getSavegamesMetaData().length > 0 && - !this.app.platformWrapper.getHasUnlimitedSavegames() + !this.app.restrictionMgr.getHasUnlimitedSavegames() ) { this.app.analytics.trackUiClick("startgame_slot_limit_show"); this.showSavegameSlotLimit(); diff --git a/src/js/states/settings.js b/src/js/states/settings.js index 5e22492a..36dee5d8 100644 --- a/src/js/states/settings.js +++ b/src/js/states/settings.js @@ -1,169 +1,169 @@ -import { TextualGameState } from "../core/textual_game_state"; -import { formatSecondsToTimeAgo } from "../core/utils"; -import { allApplicationSettings, enumCategories } from "../profile/application_settings"; -import { T } from "../translations"; - -export class SettingsState extends TextualGameState { - constructor() { - super("SettingsState"); - } - - getStateHeaderTitle() { - return T.settings.title; - } - - getMainContentHTML() { - return ` - - - -
- ${this.getSettingsHtml()} -
- - `; - } - - getCategoryButtonsHtml() { - return Object.keys(enumCategories) - .map(key => enumCategories[key]) - .map( - category => - ` - - ` - ) - .join(""); - } - - getSettingsHtml() { - const categoriesHTML = {}; - - Object.keys(enumCategories).forEach(key => { - const catName = enumCategories[key]; - categoriesHTML[catName] = `
`; - }); - - for (let i = 0; i < allApplicationSettings.length; ++i) { - const setting = allApplicationSettings[i]; - - categoriesHTML[setting.categoryId] += setting.getHtml(); - } - - return Object.keys(categoriesHTML) - .map(k => categoriesHTML[k] + "
") - .join(""); - } - - renderBuildText() { - const labelVersion = this.htmlElement.querySelector(".buildVersion"); - const lastBuildMs = new Date().getTime() - G_BUILD_TIME; - const lastBuildText = formatSecondsToTimeAgo(lastBuildMs / 1000.0); - - const version = T.settings.versionBadges[G_APP_ENVIRONMENT]; - - labelVersion.innerHTML = ` - - ${G_BUILD_VERSION} @ ${version} @ ${G_BUILD_COMMIT_HASH} - - - ${T.settings.buildDate.replace("", lastBuildText)}
-
`; - } - - onEnter(payload) { - this.renderBuildText(); - this.trackClicks(this.htmlElement.querySelector(".about"), this.onAboutClicked, { - preventDefault: false, - }); - - const keybindingsButton = this.htmlElement.querySelector(".editKeybindings"); - - if (keybindingsButton) { - this.trackClicks(keybindingsButton, this.onKeybindingsClicked, { preventDefault: false }); - } - - this.initSettings(); - this.initCategoryButtons(); - - this.htmlElement.querySelector(".category").classList.add("active"); - this.htmlElement.querySelector(".categoryButton").classList.add("active"); - } - - setActiveCategory(category) { - const previousCategory = this.htmlElement.querySelector(".category.active"); - const previousCategoryButton = this.htmlElement.querySelector(".categoryButton.active"); - - if (previousCategory.getAttribute("data-category") == category) { - return; - } - - previousCategory.classList.remove("active"); - previousCategoryButton.classList.remove("active"); - - const newCategory = this.htmlElement.querySelector("[data-category='" + category + "']"); - const newCategoryButton = this.htmlElement.querySelector("[data-category-btn='" + category + "']"); - - newCategory.classList.add("active"); - newCategoryButton.classList.add("active"); - } - - initSettings() { - allApplicationSettings.forEach(setting => { - /** @type {HTMLElement} */ - const element = this.htmlElement.querySelector("[data-setting='" + setting.id + "']"); - setting.bind(this.app, element, this.dialogs); - setting.syncValueToElement(); - this.trackClicks( - element, - () => { - setting.modify(); - }, - { preventDefault: false } - ); - }); - } - - initCategoryButtons() { - Object.keys(enumCategories).forEach(key => { - const category = enumCategories[key]; - const button = this.htmlElement.querySelector("[data-category-btn='" + category + "']"); - this.trackClicks( - button, - () => { - this.setActiveCategory(category); - }, - { preventDefault: false } - ); - }); - } - - onAboutClicked() { - this.moveToStateAddGoBack("AboutState"); - } - - onKeybindingsClicked() { - this.moveToStateAddGoBack("KeybindingsState"); - } -} +import { TextualGameState } from "../core/textual_game_state"; +import { formatSecondsToTimeAgo } from "../core/utils"; +import { allApplicationSettings, enumCategories } from "../profile/application_settings"; +import { T } from "../translations"; + +export class SettingsState extends TextualGameState { + constructor() { + super("SettingsState"); + } + + getStateHeaderTitle() { + return T.settings.title; + } + + getMainContentHTML() { + return ` + + + +
+ ${this.getSettingsHtml()} +
+ + `; + } + + getCategoryButtonsHtml() { + return Object.keys(enumCategories) + .map(key => enumCategories[key]) + .map( + category => + ` + + ` + ) + .join(""); + } + + getSettingsHtml() { + const categoriesHTML = {}; + + Object.keys(enumCategories).forEach(key => { + const catName = enumCategories[key]; + categoriesHTML[catName] = `
`; + }); + + for (let i = 0; i < allApplicationSettings.length; ++i) { + const setting = allApplicationSettings[i]; + + categoriesHTML[setting.categoryId] += setting.getHtml(this.app); + } + + return Object.keys(categoriesHTML) + .map(k => categoriesHTML[k] + "
") + .join(""); + } + + renderBuildText() { + const labelVersion = this.htmlElement.querySelector(".buildVersion"); + const lastBuildMs = new Date().getTime() - G_BUILD_TIME; + const lastBuildText = formatSecondsToTimeAgo(lastBuildMs / 1000.0); + + const version = T.settings.versionBadges[G_APP_ENVIRONMENT]; + + labelVersion.innerHTML = ` + + ${G_BUILD_VERSION} @ ${version} @ ${G_BUILD_COMMIT_HASH} + + + ${T.settings.buildDate.replace("", lastBuildText)}
+
`; + } + + onEnter(payload) { + this.renderBuildText(); + this.trackClicks(this.htmlElement.querySelector(".about"), this.onAboutClicked, { + preventDefault: false, + }); + + const keybindingsButton = this.htmlElement.querySelector(".editKeybindings"); + + if (keybindingsButton) { + this.trackClicks(keybindingsButton, this.onKeybindingsClicked, { preventDefault: false }); + } + + this.initSettings(); + this.initCategoryButtons(); + + this.htmlElement.querySelector(".category").classList.add("active"); + this.htmlElement.querySelector(".categoryButton").classList.add("active"); + } + + setActiveCategory(category) { + const previousCategory = this.htmlElement.querySelector(".category.active"); + const previousCategoryButton = this.htmlElement.querySelector(".categoryButton.active"); + + if (previousCategory.getAttribute("data-category") == category) { + return; + } + + previousCategory.classList.remove("active"); + previousCategoryButton.classList.remove("active"); + + const newCategory = this.htmlElement.querySelector("[data-category='" + category + "']"); + const newCategoryButton = this.htmlElement.querySelector("[data-category-btn='" + category + "']"); + + newCategory.classList.add("active"); + newCategoryButton.classList.add("active"); + } + + initSettings() { + allApplicationSettings.forEach(setting => { + /** @type {HTMLElement} */ + const element = this.htmlElement.querySelector("[data-setting='" + setting.id + "']"); + setting.bind(this.app, element, this.dialogs); + setting.syncValueToElement(); + this.trackClicks( + element, + () => { + setting.modify(); + }, + { preventDefault: false } + ); + }); + } + + initCategoryButtons() { + Object.keys(enumCategories).forEach(key => { + const category = enumCategories[key]; + const button = this.htmlElement.querySelector("[data-category-btn='" + category + "']"); + this.trackClicks( + button, + () => { + this.setActiveCategory(category); + }, + { preventDefault: false } + ); + }); + } + + onAboutClicked() { + this.moveToStateAddGoBack("AboutState"); + } + + onKeybindingsClicked() { + this.moveToStateAddGoBack("KeybindingsState"); + } +}