From 5a1c24c9dfba66275a16160260fa850edb262179 Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sat, 13 Nov 2021 14:41:04 +0100 Subject: [PATCH 01/24] Add seed to `GameCreationPayload` --- src/js/states/ingame.js | 966 ++++++++++++++++++++-------------------- 1 file changed, 486 insertions(+), 480 deletions(-) diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js index 0dd6c72a..9cf88f04 100644 --- a/src/js/states/ingame.js +++ b/src/js/states/ingame.js @@ -1,480 +1,486 @@ -import { APPLICATION_ERROR_OCCURED } from "../core/error_handler"; -import { GameState } from "../core/game_state"; -import { logSection, createLogger } from "../core/logging"; -import { waitNextFrame } from "../core/utils"; -import { globalConfig } from "../core/config"; -import { GameLoadingOverlay } from "../game/game_loading_overlay"; -import { KeyActionMapper } from "../game/key_action_mapper"; -import { Savegame } from "../savegame/savegame"; -import { GameCore } from "../game/core"; -import { MUSIC } from "../platform/sound"; -import { enumGameModeIds } from "../game/game_mode"; - -const logger = createLogger("state/ingame"); - -// Different sub-states -const stages = { - s3_createCore: "🌈 3: Create core", - s4_A_initEmptyGame: "🌈 4/A: Init empty game", - s4_B_resumeGame: "🌈 4/B: Resume game", - - s5_firstUpdate: "🌈 5: First game update", - s6_postLoadHook: "🌈 6: Post load hook", - s7_warmup: "🌈 7: Warmup", - - s10_gameRunning: "🌈 10: Game finally running", - - leaving: "🌈 Saving, then leaving the game", - destroyed: "🌈 DESTROYED: Core is empty and waits for state leave", - initFailed: "🌈 ERROR: Initialization failed!", -}; - -export const gameCreationAction = { - new: "new-game", - resume: "resume-game", -}; - -// Typehints -export class GameCreationPayload { - constructor() { - /** @type {boolean|undefined} */ - this.fastEnter; - - /** @type {string} */ - this.gameModeId; - - /** @type {Savegame} */ - this.savegame; - - /** @type {object|undefined} */ - this.gameModeParameters; - } -} - -export class InGameState extends GameState { - constructor() { - super("InGameState"); - - /** @type {GameCreationPayload} */ - this.creationPayload = null; - - // Stores current stage - this.stage = ""; - - /** @type {GameCore} */ - this.core = null; - - /** @type {KeyActionMapper} */ - this.keyActionMapper = null; - - /** @type {GameLoadingOverlay} */ - this.loadingOverlay = null; - - /** @type {Savegame} */ - this.savegame = null; - - this.boundInputFilter = this.filterInput.bind(this); - - /** - * Whether we are currently saving the game - * @TODO: This doesn't realy fit here - */ - this.currentSavePromise = null; - } - - /** - * Switches the game into another sub-state - * @param {string} stage - */ - switchStage(stage) { - assert(stage, "Got empty stage"); - if (stage !== this.stage) { - this.stage = stage; - logger.log(this.stage); - return true; - } else { - // log(this, "Re entering", stage); - return false; - } - } - - // GameState implementation - getInnerHTML() { - return ""; - } - - getThemeMusic() { - if (this.creationPayload.gameModeId && this.creationPayload.gameModeId.includes("puzzle")) { - return MUSIC.puzzle; - } - return MUSIC.theme; - } - - onBeforeExit() { - // logger.log("Saving before quitting"); - // return this.doSave().then(() => { - // logger.log(this, "Successfully saved"); - // // this.stageDestroyed(); - // }); - } - - onAppPause() { - // if (this.stage === stages.s10_gameRunning) { - // logger.log("Saving because app got paused"); - // this.doSave(); - // } - } - - getHasFadeIn() { - return false; - } - - getPauseOnFocusLost() { - return false; - } - - getHasUnloadConfirmation() { - return true; - } - - onLeave() { - if (this.core) { - this.stageDestroyed(); - } - this.app.inputMgr.dismountFilter(this.boundInputFilter); - } - - onResized(w, h) { - super.onResized(w, h); - if (this.stage === stages.s10_gameRunning) { - this.core.resize(w, h); - } - } - - // ---- End of GameState implementation - - /** - * Goes back to the menu state - */ - goBackToMenu() { - if ([enumGameModeIds.puzzleEdit, enumGameModeIds.puzzlePlay].includes(this.gameModeId)) { - this.saveThenGoToState("PuzzleMenuState"); - } else { - this.saveThenGoToState("MainMenuState"); - } - } - - /** - * Goes back to the settings state - */ - goToSettings() { - this.saveThenGoToState("SettingsState", { - backToStateId: this.key, - backToStatePayload: this.creationPayload, - }); - } - - /** - * Goes back to the settings state - */ - goToKeybindings() { - this.saveThenGoToState("KeybindingsState", { - backToStateId: this.key, - backToStatePayload: this.creationPayload, - }); - } - - /** - * Moves to a state outside of the game - * @param {string} stateId - * @param {any=} payload - */ - saveThenGoToState(stateId, payload) { - if (this.stage === stages.leaving || this.stage === stages.destroyed) { - logger.warn( - "Tried to leave game twice or during destroy:", - this.stage, - "(attempted to move to", - stateId, - ")" - ); - return; - } - this.stageLeavingGame(); - this.doSave().then(() => { - this.stageDestroyed(); - this.moveToState(stateId, payload); - }); - } - - onBackButton() { - // do nothing - } - - /** - * Called when the game somehow failed to initialize. Resets everything to basic state and - * then goes to the main menu, showing the error - * @param {string} err - */ - onInitializationFailure(err) { - if (this.switchStage(stages.initFailed)) { - logger.error("Init failure:", err); - this.stageDestroyed(); - this.moveToState("MainMenuState", { loadError: err }); - } - } - - // STAGES - - /** - * Creates the game core instance, and thus the root - */ - stage3CreateCore() { - if (this.switchStage(stages.s3_createCore)) { - logger.log("Creating new game core"); - this.core = new GameCore(this.app); - - this.core.initializeRoot(this, this.savegame, this.gameModeId); - - if (this.savegame.hasGameDump()) { - this.stage4bResumeGame(); - } else { - this.app.gameAnalytics.handleGameStarted(); - this.stage4aInitEmptyGame(); - } - } - } - - /** - * Initializes a new empty game - */ - stage4aInitEmptyGame() { - if (this.switchStage(stages.s4_A_initEmptyGame)) { - this.core.initNewGame(); - this.stage5FirstUpdate(); - } - } - - /** - * Resumes an existing game - */ - stage4bResumeGame() { - if (this.switchStage(stages.s4_B_resumeGame)) { - if (!this.core.initExistingGame()) { - this.onInitializationFailure("Savegame is corrupt and can not be restored."); - return; - } - this.app.gameAnalytics.handleGameResumed(); - this.stage5FirstUpdate(); - } - } - - /** - * Performs the first game update on the game which initializes most caches - */ - stage5FirstUpdate() { - if (this.switchStage(stages.s5_firstUpdate)) { - this.core.root.logicInitialized = true; - this.core.updateLogic(); - this.stage6PostLoadHook(); - } - } - - /** - * Call the post load hook, this means that we have loaded the game, and all systems - * can operate and start to work now. - */ - stage6PostLoadHook() { - if (this.switchStage(stages.s6_postLoadHook)) { - logger.log("Post load hook"); - this.core.postLoadHook(); - this.stage7Warmup(); - } - } - - /** - * This makes the game idle and draw for a while, because we run most code this way - * the V8 engine can already start to optimize it. Also this makes sure the resources - * are in the VRAM and we have a smooth experience once we start. - */ - stage7Warmup() { - if (this.switchStage(stages.s7_warmup)) { - if (this.creationPayload.fastEnter) { - this.warmupTimeSeconds = globalConfig.warmupTimeSecondsFast; - } else { - this.warmupTimeSeconds = globalConfig.warmupTimeSecondsRegular; - } - } - } - - /** - * The final stage where this game is running and updating regulary. - */ - stage10GameRunning() { - if (this.switchStage(stages.s10_gameRunning)) { - this.core.root.signals.readyToRender.dispatch(); - - logSection("GAME STARTED", "#26a69a"); - - // Initial resize, might have changed during loading (this is possible) - this.core.resize(this.app.screenWidth, this.app.screenHeight); - } - } - - /** - * This stage destroys the whole game, used to cleanup - */ - stageDestroyed() { - if (this.switchStage(stages.destroyed)) { - // Cleanup all api calls - this.cancelAllAsyncOperations(); - - if (this.syncer) { - this.syncer.cancelSync(); - this.syncer = null; - } - - // Cleanup core - if (this.core) { - this.core.destruct(); - this.core = null; - } - } - } - - /** - * When leaving the game - */ - stageLeavingGame() { - if (this.switchStage(stages.leaving)) { - // ... - } - } - - // END STAGES - - /** - * Filters the input (keybindings) - */ - filterInput() { - return this.stage === stages.s10_gameRunning; - } - - /** - * @param {GameCreationPayload} payload - */ - onEnter(payload) { - this.app.inputMgr.installFilter(this.boundInputFilter); - - this.creationPayload = payload; - this.savegame = payload.savegame; - this.gameModeId = payload.gameModeId; - - this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement()); - this.loadingOverlay.showBasic(); - - // Remove unneded default element - document.body.querySelector(".modalDialogParent").remove(); - - this.asyncChannel - .watch(waitNextFrame()) - .then(() => this.stage3CreateCore()) - .catch(ex => { - logger.error(ex); - throw ex; - }); - } - - /** - * Render callback - * @param {number} dt - */ - onRender(dt) { - if (APPLICATION_ERROR_OCCURED) { - // Application somehow crashed, do not do anything - return; - } - - if (this.stage === stages.s7_warmup) { - this.core.draw(); - this.warmupTimeSeconds -= dt / 1000.0; - if (this.warmupTimeSeconds < 0) { - logger.log("Warmup completed"); - this.stage10GameRunning(); - } - } - - if (this.stage === stages.s10_gameRunning) { - this.core.tick(dt); - } - - // If the stage is still active (This might not be the case if tick() moved us to game over) - if (this.stage === stages.s10_gameRunning) { - // Only draw if page visible - if (this.app.pageVisible) { - this.core.draw(); - } - - this.loadingOverlay.removeIfAttached(); - } else { - if (!this.loadingOverlay.isAttached()) { - this.loadingOverlay.showBasic(); - } - } - } - - onBackgroundTick(dt) { - this.onRender(dt); - } - - /** - * Saves the game - */ - - doSave() { - if (!this.savegame || !this.savegame.isSaveable()) { - return Promise.resolve(); - } - - if (APPLICATION_ERROR_OCCURED) { - logger.warn("skipping save because application crashed"); - return Promise.resolve(); - } - - if ( - this.stage !== stages.s10_gameRunning && - this.stage !== stages.s7_warmup && - this.stage !== stages.leaving - ) { - logger.warn("Skipping save because game is not ready"); - return Promise.resolve(); - } - - if (this.currentSavePromise) { - logger.warn("Skipping double save and returning same promise"); - return this.currentSavePromise; - } - - if (!this.core.root.gameMode.getIsSaveable()) { - return Promise.resolve(); - } - - logger.log("Starting to save game ..."); - this.savegame.updateData(this.core.root); - - this.currentSavePromise = this.savegame - .writeSavegameAndMetadata() - .catch(err => { - // Catch errors - logger.warn("Failed to save:", err); - }) - .then(() => { - // Clear promise - logger.log("Saved!"); - this.core.root.signals.gameSaved.dispatch(); - this.currentSavePromise = null; - }); - - return this.currentSavePromise; - } -} +import { APPLICATION_ERROR_OCCURED } from "../core/error_handler"; +import { GameState } from "../core/game_state"; +import { logSection, createLogger } from "../core/logging"; +import { waitNextFrame } from "../core/utils"; +import { globalConfig } from "../core/config"; +import { GameLoadingOverlay } from "../game/game_loading_overlay"; +import { KeyActionMapper } from "../game/key_action_mapper"; +import { Savegame } from "../savegame/savegame"; +import { GameCore } from "../game/core"; +import { MUSIC } from "../platform/sound"; +import { enumGameModeIds } from "../game/game_mode"; + +const logger = createLogger("state/ingame"); + +// Different sub-states +const stages = { + s3_createCore: "🌈 3: Create core", + s4_A_initEmptyGame: "🌈 4/A: Init empty game", + s4_B_resumeGame: "🌈 4/B: Resume game", + + s5_firstUpdate: "🌈 5: First game update", + s6_postLoadHook: "🌈 6: Post load hook", + s7_warmup: "🌈 7: Warmup", + + s10_gameRunning: "🌈 10: Game finally running", + + leaving: "🌈 Saving, then leaving the game", + destroyed: "🌈 DESTROYED: Core is empty and waits for state leave", + initFailed: "🌈 ERROR: Initialization failed!", +}; + +export const gameCreationAction = { + new: "new-game", + resume: "resume-game", +}; + +// Typehints +export class GameCreationPayload { + constructor() { + /** @type {boolean|undefined} */ + this.fastEnter; + + /** @type {string} */ + this.gameModeId; + + /** @type {Savegame} */ + this.savegame; + + /** @type {object|undefined} */ + this.gameModeParameters; + + /** @type {number} */ + this.seed; + } +} + +export class InGameState extends GameState { + constructor() { + super("InGameState"); + + /** @type {GameCreationPayload} */ + this.creationPayload = null; + + // Stores current stage + this.stage = ""; + + /** @type {GameCore} */ + this.core = null; + + /** @type {KeyActionMapper} */ + this.keyActionMapper = null; + + /** @type {GameLoadingOverlay} */ + this.loadingOverlay = null; + + /** @type {Savegame} */ + this.savegame = null; + + /** @type {number} */ + this.seed = null; + + this.boundInputFilter = this.filterInput.bind(this); + + /** + * Whether we are currently saving the game + * @TODO: This doesn't realy fit here + */ + this.currentSavePromise = null; + } + + /** + * Switches the game into another sub-state + * @param {string} stage + */ + switchStage(stage) { + assert(stage, "Got empty stage"); + if (stage !== this.stage) { + this.stage = stage; + logger.log(this.stage); + return true; + } else { + // log(this, "Re entering", stage); + return false; + } + } + + // GameState implementation + getInnerHTML() { + return ""; + } + + getThemeMusic() { + if (this.creationPayload.gameModeId && this.creationPayload.gameModeId.includes("puzzle")) { + return MUSIC.puzzle; + } + return MUSIC.theme; + } + + onBeforeExit() { + // logger.log("Saving before quitting"); + // return this.doSave().then(() => { + // logger.log(this, "Successfully saved"); + // // this.stageDestroyed(); + // }); + } + + onAppPause() { + // if (this.stage === stages.s10_gameRunning) { + // logger.log("Saving because app got paused"); + // this.doSave(); + // } + } + + getHasFadeIn() { + return false; + } + + getPauseOnFocusLost() { + return false; + } + + getHasUnloadConfirmation() { + return true; + } + + onLeave() { + if (this.core) { + this.stageDestroyed(); + } + this.app.inputMgr.dismountFilter(this.boundInputFilter); + } + + onResized(w, h) { + super.onResized(w, h); + if (this.stage === stages.s10_gameRunning) { + this.core.resize(w, h); + } + } + + // ---- End of GameState implementation + + /** + * Goes back to the menu state + */ + goBackToMenu() { + if ([enumGameModeIds.puzzleEdit, enumGameModeIds.puzzlePlay].includes(this.gameModeId)) { + this.saveThenGoToState("PuzzleMenuState"); + } else { + this.saveThenGoToState("MainMenuState"); + } + } + + /** + * Goes back to the settings state + */ + goToSettings() { + this.saveThenGoToState("SettingsState", { + backToStateId: this.key, + backToStatePayload: this.creationPayload, + }); + } + + /** + * Goes back to the settings state + */ + goToKeybindings() { + this.saveThenGoToState("KeybindingsState", { + backToStateId: this.key, + backToStatePayload: this.creationPayload, + }); + } + + /** + * Moves to a state outside of the game + * @param {string} stateId + * @param {any=} payload + */ + saveThenGoToState(stateId, payload) { + if (this.stage === stages.leaving || this.stage === stages.destroyed) { + logger.warn( + "Tried to leave game twice or during destroy:", + this.stage, + "(attempted to move to", + stateId, + ")" + ); + return; + } + this.stageLeavingGame(); + this.doSave().then(() => { + this.stageDestroyed(); + this.moveToState(stateId, payload); + }); + } + + onBackButton() { + // do nothing + } + + /** + * Called when the game somehow failed to initialize. Resets everything to basic state and + * then goes to the main menu, showing the error + * @param {string} err + */ + onInitializationFailure(err) { + if (this.switchStage(stages.initFailed)) { + logger.error("Init failure:", err); + this.stageDestroyed(); + this.moveToState("MainMenuState", { loadError: err }); + } + } + + // STAGES + + /** + * Creates the game core instance, and thus the root + */ + stage3CreateCore() { + if (this.switchStage(stages.s3_createCore)) { + logger.log("Creating new game core"); + this.core = new GameCore(this.app); + + this.core.initializeRoot(this, this.savegame, this.gameModeId); + + if (this.savegame.hasGameDump()) { + this.stage4bResumeGame(); + } else { + this.app.gameAnalytics.handleGameStarted(); + this.stage4aInitEmptyGame(); + } + } + } + + /** + * Initializes a new empty game + */ + stage4aInitEmptyGame() { + if (this.switchStage(stages.s4_A_initEmptyGame)) { + this.core.initNewGame(); + this.stage5FirstUpdate(); + } + } + + /** + * Resumes an existing game + */ + stage4bResumeGame() { + if (this.switchStage(stages.s4_B_resumeGame)) { + if (!this.core.initExistingGame()) { + this.onInitializationFailure("Savegame is corrupt and can not be restored."); + return; + } + this.app.gameAnalytics.handleGameResumed(); + this.stage5FirstUpdate(); + } + } + + /** + * Performs the first game update on the game which initializes most caches + */ + stage5FirstUpdate() { + if (this.switchStage(stages.s5_firstUpdate)) { + this.core.root.logicInitialized = true; + this.core.updateLogic(); + this.stage6PostLoadHook(); + } + } + + /** + * Call the post load hook, this means that we have loaded the game, and all systems + * can operate and start to work now. + */ + stage6PostLoadHook() { + if (this.switchStage(stages.s6_postLoadHook)) { + logger.log("Post load hook"); + this.core.postLoadHook(); + this.stage7Warmup(); + } + } + + /** + * This makes the game idle and draw for a while, because we run most code this way + * the V8 engine can already start to optimize it. Also this makes sure the resources + * are in the VRAM and we have a smooth experience once we start. + */ + stage7Warmup() { + if (this.switchStage(stages.s7_warmup)) { + if (this.creationPayload.fastEnter) { + this.warmupTimeSeconds = globalConfig.warmupTimeSecondsFast; + } else { + this.warmupTimeSeconds = globalConfig.warmupTimeSecondsRegular; + } + } + } + + /** + * The final stage where this game is running and updating regulary. + */ + stage10GameRunning() { + if (this.switchStage(stages.s10_gameRunning)) { + this.core.root.signals.readyToRender.dispatch(); + + logSection("GAME STARTED", "#26a69a"); + + // Initial resize, might have changed during loading (this is possible) + this.core.resize(this.app.screenWidth, this.app.screenHeight); + } + } + + /** + * This stage destroys the whole game, used to cleanup + */ + stageDestroyed() { + if (this.switchStage(stages.destroyed)) { + // Cleanup all api calls + this.cancelAllAsyncOperations(); + + if (this.syncer) { + this.syncer.cancelSync(); + this.syncer = null; + } + + // Cleanup core + if (this.core) { + this.core.destruct(); + this.core = null; + } + } + } + + /** + * When leaving the game + */ + stageLeavingGame() { + if (this.switchStage(stages.leaving)) { + // ... + } + } + + // END STAGES + + /** + * Filters the input (keybindings) + */ + filterInput() { + return this.stage === stages.s10_gameRunning; + } + + /** + * @param {GameCreationPayload} payload + */ + onEnter(payload) { + this.app.inputMgr.installFilter(this.boundInputFilter); + + this.creationPayload = payload; + this.savegame = payload.savegame; + this.gameModeId = payload.gameModeId; + + this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement()); + this.loadingOverlay.showBasic(); + + // Remove unneded default element + document.body.querySelector(".modalDialogParent").remove(); + + this.asyncChannel + .watch(waitNextFrame()) + .then(() => this.stage3CreateCore()) + .catch(ex => { + logger.error(ex); + throw ex; + }); + } + + /** + * Render callback + * @param {number} dt + */ + onRender(dt) { + if (APPLICATION_ERROR_OCCURED) { + // Application somehow crashed, do not do anything + return; + } + + if (this.stage === stages.s7_warmup) { + this.core.draw(); + this.warmupTimeSeconds -= dt / 1000.0; + if (this.warmupTimeSeconds < 0) { + logger.log("Warmup completed"); + this.stage10GameRunning(); + } + } + + if (this.stage === stages.s10_gameRunning) { + this.core.tick(dt); + } + + // If the stage is still active (This might not be the case if tick() moved us to game over) + if (this.stage === stages.s10_gameRunning) { + // Only draw if page visible + if (this.app.pageVisible) { + this.core.draw(); + } + + this.loadingOverlay.removeIfAttached(); + } else { + if (!this.loadingOverlay.isAttached()) { + this.loadingOverlay.showBasic(); + } + } + } + + onBackgroundTick(dt) { + this.onRender(dt); + } + + /** + * Saves the game + */ + + doSave() { + if (!this.savegame || !this.savegame.isSaveable()) { + return Promise.resolve(); + } + + if (APPLICATION_ERROR_OCCURED) { + logger.warn("skipping save because application crashed"); + return Promise.resolve(); + } + + if ( + this.stage !== stages.s10_gameRunning && + this.stage !== stages.s7_warmup && + this.stage !== stages.leaving + ) { + logger.warn("Skipping save because game is not ready"); + return Promise.resolve(); + } + + if (this.currentSavePromise) { + logger.warn("Skipping double save and returning same promise"); + return this.currentSavePromise; + } + + if (!this.core.root.gameMode.getIsSaveable()) { + return Promise.resolve(); + } + + logger.log("Starting to save game ..."); + this.savegame.updateData(this.core.root); + + this.currentSavePromise = this.savegame + .writeSavegameAndMetadata() + .catch(err => { + // Catch errors + logger.warn("Failed to save:", err); + }) + .then(() => { + // Clear promise + logger.log("Saved!"); + this.core.root.signals.gameSaved.dispatch(); + this.currentSavePromise = null; + }); + + return this.currentSavePromise; + } +} From 8c10ff8a6f8aa8c663d22b6365d147f0a6a8bce9 Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sun, 14 Nov 2021 13:13:20 +0100 Subject: [PATCH 02/24] Added `param1.seed` to `initNewGame` --- src/js/game/core.js | 1149 ++++++++++++++++++++------------------- src/js/states/ingame.js | 2 +- 2 files changed, 577 insertions(+), 574 deletions(-) diff --git a/src/js/game/core.js b/src/js/game/core.js index a0ee3713..56305aa1 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -1,573 +1,576 @@ -/* typehints:start */ -import { Application } from "../application"; -/* typehints:end */ -import { BufferMaintainer } from "../core/buffer_maintainer"; -import { - disableImageSmoothing, - enableImageSmoothing, - getBufferStats, - registerCanvas, -} from "../core/buffer_utils"; -import { globalConfig } from "../core/config"; -import { getDeviceDPI, resizeHighDPICanvas } from "../core/dpi_manager"; -import { DrawParameters } from "../core/draw_parameters"; -import { gMetaBuildingRegistry } from "../core/global_registries"; -import { createLogger } from "../core/logging"; -import { Rectangle } from "../core/rectangle"; -import { ORIGINAL_SPRITE_SCALE } from "../core/sprites"; -import { lerp, randomInt, round2Digits } from "../core/utils"; -import { Vector } from "../core/vector"; -import { Savegame } from "../savegame/savegame"; -import { SavegameSerializer } from "../savegame/savegame_serializer"; -import { AutomaticSave } from "./automatic_save"; -import { MetaHubBuilding } from "./buildings/hub"; -import { Camera } from "./camera"; -import { DynamicTickrate } from "./dynamic_tickrate"; -import { EntityManager } from "./entity_manager"; -import { GameSystemManager } from "./game_system_manager"; -import { HubGoals } from "./hub_goals"; -import { GameHUD } from "./hud/hud"; -import { KeyActionMapper } from "./key_action_mapper"; -import { GameLogic } from "./logic"; -import { MapView } from "./map_view"; -import { defaultBuildingVariant } from "./meta_building"; -import { GameMode } from "./game_mode"; -import { ProductionAnalytics } from "./production_analytics"; -import { GameRoot } from "./root"; -import { ShapeDefinitionManager } from "./shape_definition_manager"; -import { AchievementProxy } from "./achievement_proxy"; -import { SoundProxy } from "./sound_proxy"; -import { GameTime } from "./time/game_time"; - -const logger = createLogger("ingame/core"); - -// Store the canvas so we can reuse it later -/** @type {HTMLCanvasElement} */ -let lastCanvas = null; -/** @type {CanvasRenderingContext2D} */ -let lastContext = null; - -/** - * The core manages the root and represents the whole game. It wraps the root, since - * the root class is just a data holder. - */ -export class GameCore { - /** @param {Application} app */ - constructor(app) { - this.app = app; - - /** @type {GameRoot} */ - this.root = null; - - /** - * Set to true at the beginning of a logic update and cleared when its finished. - * This is to prevent doing a recursive logic update which can lead to unexpected - * behaviour. - */ - this.duringLogicUpdate = false; - - // Cached - this.boundInternalTick = this.updateLogic.bind(this); - - /** - * Opacity of the overview alpha - * @TODO Doesn't belong here - */ - this.overlayAlpha = 0; - } - - /** - * Initializes the root object which stores all game related data. The state - * is required as a back reference (used sometimes) - * @param {import("../states/ingame").InGameState} parentState - * @param {Savegame} savegame - */ - initializeRoot(parentState, savegame, gameModeId) { - logger.log("initializing root"); - - // Construct the root element, this is the data representation of the game - this.root = new GameRoot(this.app); - this.root.gameState = parentState; - this.root.keyMapper = parentState.keyActionMapper; - this.root.savegame = savegame; - this.root.gameWidth = this.app.screenWidth; - this.root.gameHeight = this.app.screenHeight; - - // Initialize canvas element & context - this.internalInitCanvas(); - - // Members - const root = this.root; - - // This isn't nice, but we need it right here - root.keyMapper = new KeyActionMapper(root, this.root.gameState.inputReciever); - - // Init game mode - root.gameMode = GameMode.create(root, gameModeId, parentState.creationPayload.gameModeParameters); - - // Needs to come first - root.dynamicTickrate = new DynamicTickrate(root); - - // Init classes - root.camera = new Camera(root); - root.map = new MapView(root); - root.logic = new GameLogic(root); - root.hud = new GameHUD(root); - root.time = new GameTime(root); - root.achievementProxy = new AchievementProxy(root); - root.automaticSave = new AutomaticSave(root); - root.soundProxy = new SoundProxy(root); - - // Init managers - root.entityMgr = new EntityManager(root); - root.systemMgr = new GameSystemManager(root); - root.shapeDefinitionMgr = new ShapeDefinitionManager(root); - root.hubGoals = new HubGoals(root); - root.productionAnalytics = new ProductionAnalytics(root); - root.buffers = new BufferMaintainer(root); - - // Initialize the hud once everything is loaded - this.root.hud.initialize(); - - // Initial resize event, it might be possible that the screen - // resized later during init tho, which is why will emit it later - // again anyways - this.resize(this.app.screenWidth, this.app.screenHeight); - - if (G_IS_DEV) { - // @ts-ignore - window.globalRoot = root; - } - - // @todo Find better place - if (G_IS_DEV && globalConfig.debug.manualTickOnly) { - this.root.gameState.inputReciever.keydown.add(key => { - if (key.keyCode === 84) { - // 'T' - - // Extract current real time - this.root.time.updateRealtimeNow(); - - // Perform logic ticks - this.root.time.performTicks(this.root.dynamicTickrate.deltaMs, this.boundInternalTick); - - // Update analytics - root.productionAnalytics.update(); - - // Check achievements - root.achievementProxy.update(); - } - }); - } - - logger.log("root initialized"); - } - - /** - * Initializes a new game, this means creating a new map and centering on the - * playerbase - * */ - initNewGame() { - logger.log("Initializing new game"); - this.root.gameIsFresh = true; - this.root.map.seed = randomInt(0, 100000); - - if (!this.root.gameMode.hasHub()) { - return; - } - - // Place the hub - const hub = gMetaBuildingRegistry.findByClass(MetaHubBuilding).createEntity({ - root: this.root, - origin: new Vector(-2, -2), - rotation: 0, - originalRotation: 0, - rotationVariant: 0, - variant: defaultBuildingVariant, - }); - this.root.map.placeStaticEntity(hub); - this.root.entityMgr.registerEntity(hub); - } - - /** - * Inits an existing game by loading the raw savegame data and deserializing it. - * Also runs basic validity checks. - */ - initExistingGame() { - logger.log("Initializing existing game"); - const serializer = new SavegameSerializer(); - - try { - const status = serializer.deserialize(this.root.savegame.getCurrentDump(), this.root); - if (!status.isGood()) { - logger.error("savegame-deserialize-failed:" + status.reason); - return false; - } - } catch (ex) { - logger.error("Exception during deserialization:", ex); - return false; - } - this.root.gameIsFresh = false; - return true; - } - - /** - * Initializes the render canvas - */ - internalInitCanvas() { - let canvas, context; - if (!lastCanvas) { - logger.log("Creating new canvas"); - canvas = document.createElement("canvas"); - canvas.id = "ingame_Canvas"; - canvas.setAttribute("opaque", "true"); - canvas.setAttribute("webkitOpaque", "true"); - canvas.setAttribute("mozOpaque", "true"); - this.root.gameState.getDivElement().appendChild(canvas); - context = canvas.getContext("2d", { alpha: false }); - - lastCanvas = canvas; - lastContext = context; - } else { - logger.log("Reusing canvas"); - if (lastCanvas.parentElement) { - lastCanvas.parentElement.removeChild(lastCanvas); - } - this.root.gameState.getDivElement().appendChild(lastCanvas); - - canvas = lastCanvas; - context = lastContext; - - lastContext.clearRect(0, 0, lastCanvas.width, lastCanvas.height); - } - - canvas.classList.toggle("smoothed", globalConfig.smoothing.smoothMainCanvas); - - // Oof, use :not() instead - canvas.classList.toggle("unsmoothed", !globalConfig.smoothing.smoothMainCanvas); - - if (globalConfig.smoothing.smoothMainCanvas) { - enableImageSmoothing(context); - } else { - disableImageSmoothing(context); - } - - this.root.canvas = canvas; - this.root.context = context; - - registerCanvas(canvas, context); - } - - /** - * Destructs the root, freeing all resources - */ - destruct() { - if (lastCanvas && lastCanvas.parentElement) { - lastCanvas.parentElement.removeChild(lastCanvas); - } - - this.root.destruct(); - delete this.root; - this.root = null; - this.app = null; - } - - tick(deltaMs) { - const root = this.root; - - // Extract current real time - root.time.updateRealtimeNow(); - - // Camera is always updated, no matter what - root.camera.update(deltaMs); - - if (!(G_IS_DEV && globalConfig.debug.manualTickOnly)) { - // Perform logic ticks - this.root.time.performTicks(deltaMs, this.boundInternalTick); - - // Update analytics - root.productionAnalytics.update(); - - // Check achievements - root.achievementProxy.update(); - } - - // Update automatic save after everything finished - root.automaticSave.update(); - - return true; - } - - shouldRender() { - if (this.root.queue.requireRedraw) { - return true; - } - if (this.root.hud.shouldPauseRendering()) { - return false; - } - - // Do not render - if (!this.app.isRenderable()) { - return false; - } - - return true; - } - - updateLogic() { - const root = this.root; - - root.dynamicTickrate.beginTick(); - - if (G_IS_DEV && globalConfig.debug.disableLogicTicks) { - root.dynamicTickrate.endTick(); - return true; - } - - this.duringLogicUpdate = true; - - // Update entities, this removes destroyed entities - root.entityMgr.update(); - - // IMPORTANT: At this point, the game might be game over. Stop if this is the case - if (!this.root) { - logger.log("Root destructed, returning false"); - root.dynamicTickrate.endTick(); - - return false; - } - - root.systemMgr.update(); - // root.particleMgr.update(); - - this.duringLogicUpdate = false; - root.dynamicTickrate.endTick(); - return true; - } - - resize(w, h) { - this.root.gameWidth = w; - this.root.gameHeight = h; - resizeHighDPICanvas(this.root.canvas, w, h, globalConfig.smoothing.smoothMainCanvas); - this.root.signals.resized.dispatch(w, h); - this.root.queue.requireRedraw = true; - } - - postLoadHook() { - logger.log("Dispatching post load hook"); - this.root.signals.postLoadHook.dispatch(); - - if (!this.root.gameIsFresh) { - // Also dispatch game restored hook on restored savegames - this.root.signals.gameRestored.dispatch(); - } - - this.root.gameInitialized = true; - } - - draw() { - const root = this.root; - const systems = root.systemMgr.systems; - - this.root.dynamicTickrate.onFrameRendered(); - - if (!this.shouldRender()) { - // Always update hud tho - root.hud.update(); - return; - } - - this.root.signals.gameFrameStarted.dispatch(); - - root.queue.requireRedraw = false; - - // Gather context and save all state - const context = root.context; - context.save(); - if (G_IS_DEV) { - context.fillStyle = "#a10000"; - context.fillRect(0, 0, window.innerWidth * 3, window.innerHeight * 3); - } - - // Compute optimal zoom level and atlas scale - const zoomLevel = root.camera.zoomLevel; - const lowQuality = root.app.settings.getAllSettings().lowQualityTextures; - const effectiveZoomLevel = - (zoomLevel / globalConfig.assetsDpi) * getDeviceDPI() * globalConfig.assetsSharpness; - - let desiredAtlasScale = "0.25"; - if (effectiveZoomLevel > 0.5 && !lowQuality) { - desiredAtlasScale = ORIGINAL_SPRITE_SCALE; - } else if (effectiveZoomLevel > 0.35 && !lowQuality) { - desiredAtlasScale = "0.5"; - } - - // Construct parameters required for drawing - const params = new DrawParameters({ - context: context, - visibleRect: root.camera.getVisibleRect(), - desiredAtlasScale, - zoomLevel, - root: root, - }); - - if (G_IS_DEV && globalConfig.debug.testCulling) { - context.clearRect(0, 0, root.gameWidth, root.gameHeight); - } - - // Transform to world space - - if (G_IS_DEV && globalConfig.debug.testClipping) { - params.visibleRect = params.visibleRect.expandedInAllDirections( - -200 / this.root.camera.zoomLevel - ); - } - - root.camera.transform(context); - - assert(context.globalAlpha === 1.0, "Global alpha not 1 on frame start"); - - // Update hud - root.hud.update(); - - // Main rendering order - // ----- - - const desiredOverlayAlpha = this.root.camera.getIsMapOverlayActive() ? 1 : 0; - this.overlayAlpha = lerp(this.overlayAlpha, desiredOverlayAlpha, 0.25); - - // On low performance, skip the fade - if (this.root.entityMgr.entities.length > 5000 || this.root.dynamicTickrate.averageFps < 50) { - this.overlayAlpha = desiredOverlayAlpha; - } - - if (this.overlayAlpha < 0.99) { - // Background (grid, resources, etc) - root.map.drawBackground(params); - - // Belt items - systems.belt.drawBeltItems(params); - - // Miner & Static map entities etc. - root.map.drawForeground(params); - - // HUB Overlay - systems.hub.draw(params); - - // Green wires overlay - if (root.hud.parts.wiresOverlay) { - root.hud.parts.wiresOverlay.draw(params); - } - - if (this.root.currentLayer === "wires") { - // Static map entities - root.map.drawWiresForegroundLayer(params); - } - } - - if (this.overlayAlpha > 0.01) { - // Map overview - context.globalAlpha = this.overlayAlpha; - root.map.drawOverlay(params); - context.globalAlpha = 1; - } - - if (G_IS_DEV) { - root.map.drawStaticEntityDebugOverlays(params); - } - - if (G_IS_DEV && globalConfig.debug.renderBeltPaths) { - systems.belt.drawBeltPathDebug(params); - } - - // END OF GAME CONTENT - // ----- - - // Finally, draw the hud. Nothing should come after that - root.hud.draw(params); - - assert(context.globalAlpha === 1.0, "Global alpha not 1 on frame end before restore"); - - // Restore to screen space - context.restore(); - - // Restore parameters - params.zoomLevel = 1; - params.desiredAtlasScale = ORIGINAL_SPRITE_SCALE; - params.visibleRect = new Rectangle(0, 0, this.root.gameWidth, this.root.gameHeight); - if (G_IS_DEV && globalConfig.debug.testClipping) { - params.visibleRect = params.visibleRect.expandedInAllDirections(-200); - } - - // Draw overlays, those are screen space - root.hud.drawOverlays(params); - - assert(context.globalAlpha === 1.0, "context.globalAlpha not 1 on frame end"); - - if (G_IS_DEV && globalConfig.debug.simulateSlowRendering) { - let sum = 0; - for (let i = 0; i < 1e8; ++i) { - sum += i; - } - if (Math.random() > 0.95) { - console.log(sum); - } - } - - if (G_IS_DEV && globalConfig.debug.showAtlasInfo) { - context.font = "13px GameFont"; - context.fillStyle = "blue"; - context.fillText( - "Atlas: " + - desiredAtlasScale + - " / Zoom: " + - round2Digits(zoomLevel) + - " / Effective Zoom: " + - round2Digits(effectiveZoomLevel), - 20, - 600 - ); - - const stats = this.root.buffers.getStats(); - - context.fillText( - "Maintained Buffers: " + - stats.rootKeys + - " root keys / " + - stats.subKeys + - " buffers / VRAM: " + - round2Digits(stats.vramBytes / (1024 * 1024)) + - " MB", - 20, - 620 - ); - const internalStats = getBufferStats(); - context.fillText( - "Total Buffers: " + - internalStats.bufferCount + - " buffers / " + - internalStats.backlogSize + - " backlog / " + - internalStats.backlogKeys + - " keys in backlog / VRAM " + - round2Digits(internalStats.vramUsage / (1024 * 1024)) + - " MB / Backlog " + - round2Digits(internalStats.backlogVramUsage / (1024 * 1024)) + - " MB / Created " + - internalStats.numCreated + - " / Reused " + - internalStats.numReused, - 20, - 640 - ); - } - - if (G_IS_DEV && globalConfig.debug.testClipping) { - context.strokeStyle = "red"; - context.lineWidth = 1; - context.beginPath(); - context.rect(200, 200, this.root.gameWidth - 400, this.root.gameHeight - 400); - context.stroke(); - } - } -} +/* typehints:start */ +import { Application } from "../application"; +/* typehints:end */ +import { BufferMaintainer } from "../core/buffer_maintainer"; +import { + disableImageSmoothing, + enableImageSmoothing, + getBufferStats, + registerCanvas, +} from "../core/buffer_utils"; +import { globalConfig } from "../core/config"; +import { getDeviceDPI, resizeHighDPICanvas } from "../core/dpi_manager"; +import { DrawParameters } from "../core/draw_parameters"; +import { gMetaBuildingRegistry } from "../core/global_registries"; +import { createLogger } from "../core/logging"; +import { Rectangle } from "../core/rectangle"; +import { ORIGINAL_SPRITE_SCALE } from "../core/sprites"; +import { lerp, randomInt, round2Digits } from "../core/utils"; +import { Vector } from "../core/vector"; +import { Savegame } from "../savegame/savegame"; +import { SavegameSerializer } from "../savegame/savegame_serializer"; +import { AutomaticSave } from "./automatic_save"; +import { MetaHubBuilding } from "./buildings/hub"; +import { Camera } from "./camera"; +import { DynamicTickrate } from "./dynamic_tickrate"; +import { EntityManager } from "./entity_manager"; +import { GameSystemManager } from "./game_system_manager"; +import { HubGoals } from "./hub_goals"; +import { GameHUD } from "./hud/hud"; +import { KeyActionMapper } from "./key_action_mapper"; +import { GameLogic } from "./logic"; +import { MapView } from "./map_view"; +import { defaultBuildingVariant } from "./meta_building"; +import { GameMode } from "./game_mode"; +import { ProductionAnalytics } from "./production_analytics"; +import { GameRoot } from "./root"; +import { ShapeDefinitionManager } from "./shape_definition_manager"; +import { AchievementProxy } from "./achievement_proxy"; +import { SoundProxy } from "./sound_proxy"; +import { GameTime } from "./time/game_time"; + +const logger = createLogger("ingame/core"); + +// Store the canvas so we can reuse it later +/** @type {HTMLCanvasElement} */ +let lastCanvas = null; +/** @type {CanvasRenderingContext2D} */ +let lastContext = null; + +/** + * The core manages the root and represents the whole game. It wraps the root, since + * the root class is just a data holder. + */ +export class GameCore { + /** @param {Application} app */ + constructor(app) { + this.app = app; + + /** @type {GameRoot} */ + this.root = null; + + /** + * Set to true at the beginning of a logic update and cleared when its finished. + * This is to prevent doing a recursive logic update which can lead to unexpected + * behaviour. + */ + this.duringLogicUpdate = false; + + // Cached + this.boundInternalTick = this.updateLogic.bind(this); + + /** + * Opacity of the overview alpha + * @TODO Doesn't belong here + */ + this.overlayAlpha = 0; + } + + /** + * Initializes the root object which stores all game related data. The state + * is required as a back reference (used sometimes) + * @param {import("../states/ingame").InGameState} parentState + * @param {Savegame} savegame + */ + initializeRoot(parentState, savegame, gameModeId) { + logger.log("initializing root"); + + // Construct the root element, this is the data representation of the game + this.root = new GameRoot(this.app); + this.root.gameState = parentState; + this.root.keyMapper = parentState.keyActionMapper; + this.root.savegame = savegame; + this.root.gameWidth = this.app.screenWidth; + this.root.gameHeight = this.app.screenHeight; + + // Initialize canvas element & context + this.internalInitCanvas(); + + // Members + const root = this.root; + + // This isn't nice, but we need it right here + root.keyMapper = new KeyActionMapper(root, this.root.gameState.inputReciever); + + // Init game mode + root.gameMode = GameMode.create(root, gameModeId, parentState.creationPayload.gameModeParameters); + + // Needs to come first + root.dynamicTickrate = new DynamicTickrate(root); + + // Init classes + root.camera = new Camera(root); + root.map = new MapView(root); + root.logic = new GameLogic(root); + root.hud = new GameHUD(root); + root.time = new GameTime(root); + root.achievementProxy = new AchievementProxy(root); + root.automaticSave = new AutomaticSave(root); + root.soundProxy = new SoundProxy(root); + + // Init managers + root.entityMgr = new EntityManager(root); + root.systemMgr = new GameSystemManager(root); + root.shapeDefinitionMgr = new ShapeDefinitionManager(root); + root.hubGoals = new HubGoals(root); + root.productionAnalytics = new ProductionAnalytics(root); + root.buffers = new BufferMaintainer(root); + + // Initialize the hud once everything is loaded + this.root.hud.initialize(); + + // Initial resize event, it might be possible that the screen + // resized later during init tho, which is why will emit it later + // again anyways + this.resize(this.app.screenWidth, this.app.screenHeight); + + if (G_IS_DEV) { + // @ts-ignore + window.globalRoot = root; + } + + // @todo Find better place + if (G_IS_DEV && globalConfig.debug.manualTickOnly) { + this.root.gameState.inputReciever.keydown.add(key => { + if (key.keyCode === 84) { + // 'T' + + // Extract current real time + this.root.time.updateRealtimeNow(); + + // Perform logic ticks + this.root.time.performTicks(this.root.dynamicTickrate.deltaMs, this.boundInternalTick); + + // Update analytics + root.productionAnalytics.update(); + + // Check achievements + root.achievementProxy.update(); + } + }); + } + + logger.log("root initialized"); + } + + /** + * Initializes a new game, this means creating a new map and centering on the + * playerbase + * + * @param {object} param1 + * @param {number} param1.seed + * */ + initNewGame({ seed }) { + logger.log("Initializing new game"); + this.root.gameIsFresh = true; + this.root.map.seed = randomInt(0, 100000); + + if (!this.root.gameMode.hasHub()) { + return; + } + + // Place the hub + const hub = gMetaBuildingRegistry.findByClass(MetaHubBuilding).createEntity({ + root: this.root, + origin: new Vector(-2, -2), + rotation: 0, + originalRotation: 0, + rotationVariant: 0, + variant: defaultBuildingVariant, + }); + this.root.map.placeStaticEntity(hub); + this.root.entityMgr.registerEntity(hub); + } + + /** + * Inits an existing game by loading the raw savegame data and deserializing it. + * Also runs basic validity checks. + */ + initExistingGame() { + logger.log("Initializing existing game"); + const serializer = new SavegameSerializer(); + + try { + const status = serializer.deserialize(this.root.savegame.getCurrentDump(), this.root); + if (!status.isGood()) { + logger.error("savegame-deserialize-failed:" + status.reason); + return false; + } + } catch (ex) { + logger.error("Exception during deserialization:", ex); + return false; + } + this.root.gameIsFresh = false; + return true; + } + + /** + * Initializes the render canvas + */ + internalInitCanvas() { + let canvas, context; + if (!lastCanvas) { + logger.log("Creating new canvas"); + canvas = document.createElement("canvas"); + canvas.id = "ingame_Canvas"; + canvas.setAttribute("opaque", "true"); + canvas.setAttribute("webkitOpaque", "true"); + canvas.setAttribute("mozOpaque", "true"); + this.root.gameState.getDivElement().appendChild(canvas); + context = canvas.getContext("2d", { alpha: false }); + + lastCanvas = canvas; + lastContext = context; + } else { + logger.log("Reusing canvas"); + if (lastCanvas.parentElement) { + lastCanvas.parentElement.removeChild(lastCanvas); + } + this.root.gameState.getDivElement().appendChild(lastCanvas); + + canvas = lastCanvas; + context = lastContext; + + lastContext.clearRect(0, 0, lastCanvas.width, lastCanvas.height); + } + + canvas.classList.toggle("smoothed", globalConfig.smoothing.smoothMainCanvas); + + // Oof, use :not() instead + canvas.classList.toggle("unsmoothed", !globalConfig.smoothing.smoothMainCanvas); + + if (globalConfig.smoothing.smoothMainCanvas) { + enableImageSmoothing(context); + } else { + disableImageSmoothing(context); + } + + this.root.canvas = canvas; + this.root.context = context; + + registerCanvas(canvas, context); + } + + /** + * Destructs the root, freeing all resources + */ + destruct() { + if (lastCanvas && lastCanvas.parentElement) { + lastCanvas.parentElement.removeChild(lastCanvas); + } + + this.root.destruct(); + delete this.root; + this.root = null; + this.app = null; + } + + tick(deltaMs) { + const root = this.root; + + // Extract current real time + root.time.updateRealtimeNow(); + + // Camera is always updated, no matter what + root.camera.update(deltaMs); + + if (!(G_IS_DEV && globalConfig.debug.manualTickOnly)) { + // Perform logic ticks + this.root.time.performTicks(deltaMs, this.boundInternalTick); + + // Update analytics + root.productionAnalytics.update(); + + // Check achievements + root.achievementProxy.update(); + } + + // Update automatic save after everything finished + root.automaticSave.update(); + + return true; + } + + shouldRender() { + if (this.root.queue.requireRedraw) { + return true; + } + if (this.root.hud.shouldPauseRendering()) { + return false; + } + + // Do not render + if (!this.app.isRenderable()) { + return false; + } + + return true; + } + + updateLogic() { + const root = this.root; + + root.dynamicTickrate.beginTick(); + + if (G_IS_DEV && globalConfig.debug.disableLogicTicks) { + root.dynamicTickrate.endTick(); + return true; + } + + this.duringLogicUpdate = true; + + // Update entities, this removes destroyed entities + root.entityMgr.update(); + + // IMPORTANT: At this point, the game might be game over. Stop if this is the case + if (!this.root) { + logger.log("Root destructed, returning false"); + root.dynamicTickrate.endTick(); + + return false; + } + + root.systemMgr.update(); + // root.particleMgr.update(); + + this.duringLogicUpdate = false; + root.dynamicTickrate.endTick(); + return true; + } + + resize(w, h) { + this.root.gameWidth = w; + this.root.gameHeight = h; + resizeHighDPICanvas(this.root.canvas, w, h, globalConfig.smoothing.smoothMainCanvas); + this.root.signals.resized.dispatch(w, h); + this.root.queue.requireRedraw = true; + } + + postLoadHook() { + logger.log("Dispatching post load hook"); + this.root.signals.postLoadHook.dispatch(); + + if (!this.root.gameIsFresh) { + // Also dispatch game restored hook on restored savegames + this.root.signals.gameRestored.dispatch(); + } + + this.root.gameInitialized = true; + } + + draw() { + const root = this.root; + const systems = root.systemMgr.systems; + + this.root.dynamicTickrate.onFrameRendered(); + + if (!this.shouldRender()) { + // Always update hud tho + root.hud.update(); + return; + } + + this.root.signals.gameFrameStarted.dispatch(); + + root.queue.requireRedraw = false; + + // Gather context and save all state + const context = root.context; + context.save(); + if (G_IS_DEV) { + context.fillStyle = "#a10000"; + context.fillRect(0, 0, window.innerWidth * 3, window.innerHeight * 3); + } + + // Compute optimal zoom level and atlas scale + const zoomLevel = root.camera.zoomLevel; + const lowQuality = root.app.settings.getAllSettings().lowQualityTextures; + const effectiveZoomLevel = + (zoomLevel / globalConfig.assetsDpi) * getDeviceDPI() * globalConfig.assetsSharpness; + + let desiredAtlasScale = "0.25"; + if (effectiveZoomLevel > 0.5 && !lowQuality) { + desiredAtlasScale = ORIGINAL_SPRITE_SCALE; + } else if (effectiveZoomLevel > 0.35 && !lowQuality) { + desiredAtlasScale = "0.5"; + } + + // Construct parameters required for drawing + const params = new DrawParameters({ + context: context, + visibleRect: root.camera.getVisibleRect(), + desiredAtlasScale, + zoomLevel, + root: root, + }); + + if (G_IS_DEV && globalConfig.debug.testCulling) { + context.clearRect(0, 0, root.gameWidth, root.gameHeight); + } + + // Transform to world space + + if (G_IS_DEV && globalConfig.debug.testClipping) { + params.visibleRect = params.visibleRect.expandedInAllDirections( + -200 / this.root.camera.zoomLevel + ); + } + + root.camera.transform(context); + + assert(context.globalAlpha === 1.0, "Global alpha not 1 on frame start"); + + // Update hud + root.hud.update(); + + // Main rendering order + // ----- + + const desiredOverlayAlpha = this.root.camera.getIsMapOverlayActive() ? 1 : 0; + this.overlayAlpha = lerp(this.overlayAlpha, desiredOverlayAlpha, 0.25); + + // On low performance, skip the fade + if (this.root.entityMgr.entities.length > 5000 || this.root.dynamicTickrate.averageFps < 50) { + this.overlayAlpha = desiredOverlayAlpha; + } + + if (this.overlayAlpha < 0.99) { + // Background (grid, resources, etc) + root.map.drawBackground(params); + + // Belt items + systems.belt.drawBeltItems(params); + + // Miner & Static map entities etc. + root.map.drawForeground(params); + + // HUB Overlay + systems.hub.draw(params); + + // Green wires overlay + if (root.hud.parts.wiresOverlay) { + root.hud.parts.wiresOverlay.draw(params); + } + + if (this.root.currentLayer === "wires") { + // Static map entities + root.map.drawWiresForegroundLayer(params); + } + } + + if (this.overlayAlpha > 0.01) { + // Map overview + context.globalAlpha = this.overlayAlpha; + root.map.drawOverlay(params); + context.globalAlpha = 1; + } + + if (G_IS_DEV) { + root.map.drawStaticEntityDebugOverlays(params); + } + + if (G_IS_DEV && globalConfig.debug.renderBeltPaths) { + systems.belt.drawBeltPathDebug(params); + } + + // END OF GAME CONTENT + // ----- + + // Finally, draw the hud. Nothing should come after that + root.hud.draw(params); + + assert(context.globalAlpha === 1.0, "Global alpha not 1 on frame end before restore"); + + // Restore to screen space + context.restore(); + + // Restore parameters + params.zoomLevel = 1; + params.desiredAtlasScale = ORIGINAL_SPRITE_SCALE; + params.visibleRect = new Rectangle(0, 0, this.root.gameWidth, this.root.gameHeight); + if (G_IS_DEV && globalConfig.debug.testClipping) { + params.visibleRect = params.visibleRect.expandedInAllDirections(-200); + } + + // Draw overlays, those are screen space + root.hud.drawOverlays(params); + + assert(context.globalAlpha === 1.0, "context.globalAlpha not 1 on frame end"); + + if (G_IS_DEV && globalConfig.debug.simulateSlowRendering) { + let sum = 0; + for (let i = 0; i < 1e8; ++i) { + sum += i; + } + if (Math.random() > 0.95) { + console.log(sum); + } + } + + if (G_IS_DEV && globalConfig.debug.showAtlasInfo) { + context.font = "13px GameFont"; + context.fillStyle = "blue"; + context.fillText( + "Atlas: " + + desiredAtlasScale + + " / Zoom: " + + round2Digits(zoomLevel) + + " / Effective Zoom: " + + round2Digits(effectiveZoomLevel), + 20, + 600 + ); + + const stats = this.root.buffers.getStats(); + + context.fillText( + "Maintained Buffers: " + + stats.rootKeys + + " root keys / " + + stats.subKeys + + " buffers / VRAM: " + + round2Digits(stats.vramBytes / (1024 * 1024)) + + " MB", + 20, + 620 + ); + const internalStats = getBufferStats(); + context.fillText( + "Total Buffers: " + + internalStats.bufferCount + + " buffers / " + + internalStats.backlogSize + + " backlog / " + + internalStats.backlogKeys + + " keys in backlog / VRAM " + + round2Digits(internalStats.vramUsage / (1024 * 1024)) + + " MB / Backlog " + + round2Digits(internalStats.backlogVramUsage / (1024 * 1024)) + + " MB / Created " + + internalStats.numCreated + + " / Reused " + + internalStats.numReused, + 20, + 640 + ); + } + + if (G_IS_DEV && globalConfig.debug.testClipping) { + context.strokeStyle = "red"; + context.lineWidth = 1; + context.beginPath(); + context.rect(200, 200, this.root.gameWidth - 400, this.root.gameHeight - 400); + context.stroke(); + } + } +} diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js index 9cf88f04..8502db80 100644 --- a/src/js/states/ingame.js +++ b/src/js/states/ingame.js @@ -256,7 +256,7 @@ export class InGameState extends GameState { */ stage4aInitEmptyGame() { if (this.switchStage(stages.s4_A_initEmptyGame)) { - this.core.initNewGame(); + this.core.initNewGame({ seed: this.seed }); this.stage5FirstUpdate(); } } From 6eeb82d857109be4e2d1edb039110d47cd3f057a Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sun, 14 Nov 2021 13:16:46 +0100 Subject: [PATCH 03/24] use `seed` from parameters in `this.root.map.seed` --- src/js/game/core.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/js/game/core.js b/src/js/game/core.js index 56305aa1..1a1b683c 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -173,7 +173,14 @@ export class GameCore { initNewGame({ seed }) { logger.log("Initializing new game"); this.root.gameIsFresh = true; - this.root.map.seed = randomInt(0, 100000); + + if (seed === undefined) { + this.root.map.seed = randomInt(0, 100000); + } else { + this.root.map.seed = seed; + } + + logger.log("this.root.map.seed: ", this.root.map.seed); if (!this.root.gameMode.hasHub()) { return; From 024527c9d2e538e27756af8082d26b3af9a6caca Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sun, 14 Nov 2021 13:18:52 +0100 Subject: [PATCH 04/24] set `this.seed` to `payload.seed` in `InGameState` --- src/js/states/ingame.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js index 8502db80..7dd90343 100644 --- a/src/js/states/ingame.js +++ b/src/js/states/ingame.js @@ -375,6 +375,7 @@ export class InGameState extends GameState { this.creationPayload = payload; this.savegame = payload.savegame; this.gameModeId = payload.gameModeId; + this.seed = payload.seed; this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement()); this.loadingOverlay.showBasic(); From 5171062e0fb4bbdfe0cb3e687e5eabc909c8b25c Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sun, 14 Nov 2021 21:03:02 +0100 Subject: [PATCH 05/24] Start of UI for newSavegame. Currently Includes: Name and Seed --- src/js/states/main_menu.js | 52 +++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 60495a9c..610661c4 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -12,6 +12,7 @@ import { makeButton, makeButtonElement, makeDiv, + randomInt, removeAllChildren, startFileChoose, waitNextFrame, @@ -672,14 +673,53 @@ export class MainMenuState extends GameState { return; } - this.app.analytics.trackUiClick("startgame"); - this.app.adProvider.showVideoAd().then(() => { - const savegame = this.app.savegameMgr.createNewSavegame(); + const regex = /^[a-zA-Z0-9_\- ]{1,20}$/; - this.moveToState("InGameState", { - savegame, + const nameInput = new FormElementInput({ + id: "nameInput", + // label: T.dialogs.newSavegame.nameInputLabel, + label: "Name:", + placeholder: "", + defaultValue: "", + validator: val => val.match(regex) && trim(val).length > 0, + }); + + const seedInput = new FormElementInput({ + id: "seedInput", + // label: T.dialogs.newSavegame.seedInputLabel, + label: "Seed:", + placeholder: "", + defaultValue: randomInt(0, 100000).toString(), + validator: val => Number.isInteger(Number(val)) && Number(val) >= 0 && Number(val) <= 100000, + }); + + const dialog = new DialogWithForm({ + app: this.app, + // title: T.dialogs.newSavegame.title, + // desc: T.dialogs.newSavegame.desc, + title: "New Game Options", + desc: "Configure your new savegame", + formElements: [nameInput, seedInput], + buttons: ["ok:good:enter"], + }); + this.dialogs.internalShowDialog(dialog); + + dialog.buttonSignals.ok.add(() => { + this.app.analytics.trackUiClick("startgame"); + this.app.adProvider.showVideoAd().then(async () => { + const savegame = this.app.savegameMgr.createNewSavegame(); + const savegameMetadata = this.app.savegameMgr.getGameMetaDataByInternalId( + savegame.internalId + ); + savegameMetadata.name = trim(nameInput.getValue()); + await this.app.savegameMgr.writeAsync(); + + this.moveToState("InGameState", { + savegame, + seed: Number(seedInput.getValue()), + }); + this.app.analytics.trackUiClick("startgame_adcomplete"); }); - this.app.analytics.trackUiClick("startgame_adcomplete"); }); } From f29545c01cb180f9075e222c21824626f6b0ef88 Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sun, 14 Nov 2021 22:05:06 +0100 Subject: [PATCH 06/24] Remove redundant setting of the seed. --- src/js/game/core.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/js/game/core.js b/src/js/game/core.js index 1a1b683c..c0012dee 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -174,11 +174,7 @@ export class GameCore { logger.log("Initializing new game"); this.root.gameIsFresh = true; - if (seed === undefined) { - this.root.map.seed = randomInt(0, 100000); - } else { - this.root.map.seed = seed; - } + this.root.map.seed = seed; logger.log("this.root.map.seed: ", this.root.map.seed); From c6b5fdc8a7d8ce090da3ed4156cfbc3e98f0f7aa Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Tue, 16 Nov 2021 15:24:23 +0100 Subject: [PATCH 07/24] Change logger messages to be natural language --- src/js/game/core.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/game/core.js b/src/js/game/core.js index c0012dee..5f34b72e 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -173,10 +173,9 @@ export class GameCore { initNewGame({ seed }) { logger.log("Initializing new game"); this.root.gameIsFresh = true; - this.root.map.seed = seed; - logger.log("this.root.map.seed: ", this.root.map.seed); + logger.log("Initializing newGame with seed: ", this.root.map.seed); if (!this.root.gameMode.hasHub()) { return; @@ -214,6 +213,7 @@ export class GameCore { return false; } this.root.gameIsFresh = false; + logger.log("Initializing existingGame with seed: ", this.root.map.seed); return true; } From 09f30e847351419edb1743d9c033a106d9987b62 Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sat, 20 Nov 2021 16:29:58 +0100 Subject: [PATCH 08/24] feat(FormElement): New element FormElementDetails This implements a wrapper for FormElements that allows you to contain them in the
HTMLElement --- src/css/ingame_hud/dialogs.scss | 19 +++++++++++++ src/js/core/modal_dialog_forms.js | 45 +++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/css/ingame_hud/dialogs.scss b/src/css/ingame_hud/dialogs.scss index cc742d42..131147b6 100644 --- a/src/css/ingame_hud/dialogs.scss +++ b/src/css/ingame_hud/dialogs.scss @@ -214,6 +214,25 @@ } } } + + > .detailsFormElem { + > .object { + pointer-events: all; + + > summary { + transition: opacity 0.1s ease-in-out; + cursor: pointer; + pointer-events: all; + &:hover { + opacity: 0.8; + } + } + > div { + @include S(margin-left, 4px); + cursor: pointer; + } + } + } } > .buttons { diff --git a/src/js/core/modal_dialog_forms.js b/src/js/core/modal_dialog_forms.js index ccf9bfb2..864d13ab 100644 --- a/src/js/core/modal_dialog_forms.js +++ b/src/js/core/modal_dialog_forms.js @@ -47,6 +47,51 @@ export class FormElement { } } +export class FormElementDetails extends FormElement { + /** + * + * @param {object} param0 + * @param {string} param0.id + * @param {string} param0.label + * @param {Array} param0.formElements + */ + constructor({ id, label, formElements }) { + super(id, label); + this.formElements = formElements; + + this.element = null; + } + + getHtml() { + return `
+ ${this.label ? `${this.label}` : ""} +
+ ${this.formElements.map(e => e.getHtml()).join("")} +
`; + } + + bindEvents(parent, clickTrackers) { + this.element = this.getFormElement(parent); + + for (let i = 0; i < this.formElements.length; ++i) { + const elem = this.formElements[i]; + elem.bindEvents(parent, clickTrackers); + elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen); + } + } + + getValue() { + let formElementValues = {}; + for (let i = 0; i < this.formElements.length; ++i) { + const elem = this.formElements[i]; + formElementValues[elem.id] = elem.getValue(); + } + return formElementValues; + } + + focus(parent) {} +} + export class FormElementInput extends FormElement { constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) { super(id, label); From 61ea329659188c268726cc98c9d2c7ac3a9c6e3d Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sun, 21 Nov 2021 17:21:47 +0100 Subject: [PATCH 09/24] light, dark color for purple,yellow,cyan resources --- src/js/game/themes/dark.json | 5 ++++- src/js/game/themes/light.json | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/js/game/themes/dark.json b/src/js/game/themes/dark.json index fec42e28..d943544f 100644 --- a/src/js/game/themes/dark.json +++ b/src/js/game/themes/dark.json @@ -28,7 +28,10 @@ "shape": "#5d5f6a", "red": "#854f56", "green": "#667964", - "blue": "#5e7ca4" + "blue": "#5e7ca4", + "purple": "#8776bc", + "yellow": "#cab57d", + "cyan": "#00b5b8" }, "chunkOverview": { "empty": "#444856", diff --git a/src/js/game/themes/light.json b/src/js/game/themes/light.json index d44a15ab..4f064536 100644 --- a/src/js/game/themes/light.json +++ b/src/js/game/themes/light.json @@ -28,7 +28,10 @@ "shape": "#eaebec", "red": "#ffbfc1", "green": "#cbffc4", - "blue": "#bfdaff" + "blue": "#bfdaff", + "purple": "#ecb3fc", + "yellow": "#fcf99c", + "cyan": "#85fdff" }, "chunkOverview": { From a3bb91ebfb180055cd8041cbe261ae36162c27c9 Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sun, 21 Nov 2021 20:53:17 +0100 Subject: [PATCH 10/24] `this.allowNonPrimaryColors` added to `BaseMap` Signed-off-by: Daan Breur --- src/js/game/map.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/js/game/map.js b/src/js/game/map.js index 67df7db3..8edff48b 100644 --- a/src/js/game/map.js +++ b/src/js/game/map.js @@ -15,6 +15,7 @@ export class BaseMap extends BasicSerializableObject { static getSchema() { return { seed: types.uint, + allowNonPrimaryColors: types.bool, }; } @@ -27,6 +28,7 @@ export class BaseMap extends BasicSerializableObject { this.root = root; this.seed = 0; + this.allowNonPrimaryColors = false; /** * Mapping of 'X|Y' to chunk From f1fab0af96bdc3b8bd41a218e2b99c56b7716fa3 Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sun, 21 Nov 2021 21:25:28 +0100 Subject: [PATCH 11/24] Let non-primarycolors generate after 2 chunks --- src/js/game/map_chunk.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/js/game/map_chunk.js b/src/js/game/map_chunk.js index 54af1125..2283a674 100644 --- a/src/js/game/map_chunk.js +++ b/src/js/game/map_chunk.js @@ -168,6 +168,11 @@ export class MapChunk { let availableColors = [enumColors.red, enumColors.green]; if (distanceToOriginInChunks > 2) { availableColors.push(enumColors.blue); + if (this.root.map.allowNonPrimaryColors) { + availableColors.push(enumColors.yellow); + availableColors.push(enumColors.purple); + availableColors.push(enumColors.cyan); + } } this.internalGeneratePatch(rng, colorPatchSize, COLOR_ITEM_SINGLETONS[rng.choice(availableColors)]); } From f5f790f55a5021404e6f9f30526bc0eaa8ecdecf Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sun, 21 Nov 2021 21:28:32 +0100 Subject: [PATCH 12/24] Get settings to the core. - added `allowNonPrimaryColors` to `type:GameCreationPayload` - added `allowNonPrimaryColors` to `class:InGameState` --- src/js/states/ingame.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js index 7dd90343..d0ad8e99 100644 --- a/src/js/states/ingame.js +++ b/src/js/states/ingame.js @@ -51,6 +51,9 @@ export class GameCreationPayload { /** @type {number} */ this.seed; + + /** @type {boolean} */ + this.allowNonPrimaryColors; } } @@ -79,6 +82,9 @@ export class InGameState extends GameState { /** @type {number} */ this.seed = null; + /** @type {boolean} */ + this.allowNonPrimaryColors = null; + this.boundInputFilter = this.filterInput.bind(this); /** @@ -256,7 +262,7 @@ export class InGameState extends GameState { */ stage4aInitEmptyGame() { if (this.switchStage(stages.s4_A_initEmptyGame)) { - this.core.initNewGame({ seed: this.seed }); + this.core.initNewGame({ seed: this.seed, allowNonPrimaryColors: this.allowNonPrimaryColors }); this.stage5FirstUpdate(); } } @@ -376,6 +382,7 @@ export class InGameState extends GameState { this.savegame = payload.savegame; this.gameModeId = payload.gameModeId; this.seed = payload.seed; + this.allowNonPrimaryColors = payload.allowNonPrimaryColors; this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement()); this.loadingOverlay.showBasic(); From cc8db548310a4e308a50aa30c82e1f8591f8698e Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sun, 21 Nov 2021 21:30:36 +0100 Subject: [PATCH 13/24] add `allowNonPrimaryColors` to `GameCore.initNewGame` --- src/js/game/core.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/game/core.js b/src/js/game/core.js index 5f34b72e..2b49a9fd 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -169,11 +169,13 @@ export class GameCore { * * @param {object} param1 * @param {number} param1.seed + * @param {boolean} param1.allowNonPrimaryColors * */ - initNewGame({ seed }) { + initNewGame({ seed, allowNonPrimaryColors }) { logger.log("Initializing new game"); this.root.gameIsFresh = true; this.root.map.seed = seed; + this.root.map.allowNonPrimaryColors = allowNonPrimaryColors; logger.log("Initializing newGame with seed: ", this.root.map.seed); From 96b76aa631c506c821f317a14f0a1dfe66398b9f Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sun, 21 Nov 2021 21:32:05 +0100 Subject: [PATCH 14/24] UI for savegame - Made use of new `FormElementDetails` - Added todos for translations - Checkbox / Toggle for non-primarycolors --- src/js/states/main_menu.js | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 610661c4..73a435f4 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -3,7 +3,7 @@ import { cachebust } from "../core/cachebust"; 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 { FormElementCheckbox, FormElementDetails, FormElementInput } from "../core/modal_dialog_forms"; import { ReadWriteProxy } from "../core/read_write_proxy"; import { formatSecondsToTimeAgo, @@ -677,29 +677,43 @@ export class MainMenuState extends GameState { const nameInput = new FormElementInput({ id: "nameInput", - // label: T.dialogs.newSavegame.nameInputLabel, + // @TODO: Add translation (T.dialogs.newSavegame.nameInputLabel) label: "Name:", placeholder: "", - defaultValue: "", + defaultValue: "Unnamed", validator: val => val.match(regex) && trim(val).length > 0, }); const seedInput = new FormElementInput({ id: "seedInput", - // label: T.dialogs.newSavegame.seedInputLabel, + // @TODO: Add translation (T.dialogs.newSavegame.seedInputLabel) label: "Seed:", placeholder: "", defaultValue: randomInt(0, 100000).toString(), validator: val => Number.isInteger(Number(val)) && Number(val) >= 0 && Number(val) <= 100000, }); + const allowColorsCheckbox = new FormElementCheckbox({ + id: "allowColorsCheckbox", + // @TODO: Add translation (T.dialogs.newSavegame.allowColorsCheckboxLabel) + label: "Allow non-primarycolors", + defaultValue: false, + }); + + const advancedContainer = new FormElementDetails({ + id: "advancedContainer", + // @TODO Add translation (T.dialogs.newSavegame.advanced) + label: "Advanced Options", + formElements: [seedInput, allowColorsCheckbox], + }); + const dialog = new DialogWithForm({ app: this.app, - // title: T.dialogs.newSavegame.title, - // desc: T.dialogs.newSavegame.desc, + // @TODO: Add translation (T.dialogs.newSavegame.title) title: "New Game Options", + // @TODO: Add translation (T.dialogs.newSavegame.desc) desc: "Configure your new savegame", - formElements: [nameInput, seedInput], + formElements: [nameInput, advancedContainer], buttons: ["ok:good:enter"], }); this.dialogs.internalShowDialog(dialog); @@ -717,6 +731,7 @@ export class MainMenuState extends GameState { this.moveToState("InGameState", { savegame, seed: Number(seedInput.getValue()), + allowNonPrimaryColors: allowColorsCheckbox.getValue(), }); this.app.analytics.trackUiClick("startgame_adcomplete"); }); From d9d72925268009a13fb940c1c1f0dd60e1c0b118 Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Mon, 22 Nov 2021 10:12:43 +0100 Subject: [PATCH 15/24] Added Savegame Migration (version 1010) Signed-off-by: Daan Breur --- src/js/savegame/savegame.js | 8 ++++- .../savegame/savegame_interface_registry.js | 2 ++ src/js/savegame/schemas/1010.js | 30 +++++++++++++++++++ src/js/savegame/schemas/1010.json | 5 ++++ 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/js/savegame/schemas/1010.js create mode 100644 src/js/savegame/schemas/1010.json diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index 36ed884f..3a6893ff 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -14,6 +14,7 @@ import { SavegameInterface_V1006 } from "./schemas/1006"; import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1009 } from "./schemas/1009"; +import { SavegameInterface_V1010 } from "./schemas/1010"; const logger = createLogger("savegame"); @@ -54,7 +55,7 @@ export class Savegame extends ReadWriteProxy { * @returns {number} */ static getCurrentVersion() { - return 1009; + return 1010; } /** @@ -160,6 +161,11 @@ export class Savegame extends ReadWriteProxy { data.version = 1009; } + if (data.version === 1009) { + SavegameInterface_V1010.migrate1009to1010(data); + data.version = 1010; + } + return ExplainedResult.good(); } diff --git a/src/js/savegame/savegame_interface_registry.js b/src/js/savegame/savegame_interface_registry.js index b4dc4233..089b15fc 100644 --- a/src/js/savegame/savegame_interface_registry.js +++ b/src/js/savegame/savegame_interface_registry.js @@ -10,6 +10,7 @@ import { SavegameInterface_V1006 } from "./schemas/1006"; import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1009 } from "./schemas/1009"; +import { SavegameInterface_V1010 } from "./schemas/1010"; /** @type {Object.} */ export const savegameInterfaces = { @@ -23,6 +24,7 @@ export const savegameInterfaces = { 1007: SavegameInterface_V1007, 1008: SavegameInterface_V1008, 1009: SavegameInterface_V1009, + 1010: SavegameInterface_V1010, }; const logger = createLogger("savegame_interface_registry"); diff --git a/src/js/savegame/schemas/1010.js b/src/js/savegame/schemas/1010.js new file mode 100644 index 00000000..e1917581 --- /dev/null +++ b/src/js/savegame/schemas/1010.js @@ -0,0 +1,30 @@ +import { createLogger } from "../../core/logging.js"; +import { SavegameInterface_V1009 } from "./1009.js"; + +const schema = require("./1010.json"); +const logger = createLogger("savegame_interface/1010"); + +export class SavegameInterface_V1010 extends SavegameInterface_V1009 { + getVersion() { + return 1010; + } + + getSchemaUncached() { + return schema; + } + + /** + * @param {import("../savegame_typedefs.js").SavegameData} data + */ + static migrate1009to1010(data) { + logger.log("Migrating 1009 to 1010"); + const dump = data.dump; + if (!dump) { + return true; + } + + if (!dump.map.hasOwnProperty("allowNonPrimaryColors")) { + dump.map.allowNonPrimaryColors = false; + } + } +} diff --git a/src/js/savegame/schemas/1010.json b/src/js/savegame/schemas/1010.json new file mode 100644 index 00000000..6682f615 --- /dev/null +++ b/src/js/savegame/schemas/1010.json @@ -0,0 +1,5 @@ +{ + "type": "object", + "required": [], + "additionalProperties": true +} From 84c5fb775733cc37cf4670c57497020e870ad68d Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Mon, 22 Nov 2021 14:30:27 +0100 Subject: [PATCH 16/24] Added option to FormElements for inline label,input. Signed-off-by: Daan Breur --- src/css/ingame_hud/dialogs.scss | 12 +++++++++++- src/js/core/modal_dialog_forms.js | 22 ++++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/css/ingame_hud/dialogs.scss b/src/css/ingame_hud/dialogs.scss index 131147b6..3aaf4b71 100644 --- a/src/css/ingame_hud/dialogs.scss +++ b/src/css/ingame_hud/dialogs.scss @@ -215,7 +215,17 @@ } } - > .detailsFormElem { + .inline { + display: flex; + flex-direction: row; + align-items: center; + + > * { + @include S(margin-right, 7.5px); + } + } + + .detailsFormElem { > .object { pointer-events: all; diff --git a/src/js/core/modal_dialog_forms.js b/src/js/core/modal_dialog_forms.js index 864d13ab..3a0963fe 100644 --- a/src/js/core/modal_dialog_forms.js +++ b/src/js/core/modal_dialog_forms.js @@ -93,12 +93,21 @@ export class FormElementDetails extends FormElement { } export class FormElementInput extends FormElement { - constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) { + constructor({ + id, + label = null, + placeholder, + defaultValue = "", + inputType = "text", + validator = null, + inline = false, + }) { super(id, label); this.placeholder = placeholder; this.defaultValue = defaultValue; this.inputType = inputType; this.validator = validator; + this.inline = inline; this.element = null; } @@ -128,7 +137,7 @@ export class FormElementInput extends FormElement { } return ` -
+
${this.label ? `` : ""} +
${this.label ? `` : ""}
- -
+ +
`; } From 839be106fc905d82f235ffb08a5d96f46cb9f707 Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Mon, 22 Nov 2021 14:32:03 +0100 Subject: [PATCH 17/24] BugFix, FormElements didnt get style --- src/js/core/modal_dialog_forms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/core/modal_dialog_forms.js b/src/js/core/modal_dialog_forms.js index 3a0963fe..7d6126a5 100644 --- a/src/js/core/modal_dialog_forms.js +++ b/src/js/core/modal_dialog_forms.js @@ -65,7 +65,7 @@ export class FormElementDetails extends FormElement { getHtml() { return `
${this.label ? `${this.label}` : ""} -
+
${this.formElements.map(e => e.getHtml()).join("")}
`; } From 78c8e805f22f7cb8b13a05ee40ce59442a3605a7 Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Mon, 22 Nov 2021 21:53:35 +0100 Subject: [PATCH 18/24] Use the new super cool inline FormElements Signed-off-by: Daan Breur --- src/js/states/main_menu.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 73a435f4..fa579e31 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -682,6 +682,7 @@ export class MainMenuState extends GameState { placeholder: "", defaultValue: "Unnamed", validator: val => val.match(regex) && trim(val).length > 0, + inline: true, }); const seedInput = new FormElementInput({ @@ -691,13 +692,15 @@ export class MainMenuState extends GameState { placeholder: "", defaultValue: randomInt(0, 100000).toString(), validator: val => Number.isInteger(Number(val)) && Number(val) >= 0 && Number(val) <= 100000, + inline: true, }); const allowColorsCheckbox = new FormElementCheckbox({ id: "allowColorsCheckbox", // @TODO: Add translation (T.dialogs.newSavegame.allowColorsCheckboxLabel) - label: "Allow non-primarycolors", + label: "Allow non-primarycolors: ", defaultValue: false, + inline: true, }); const advancedContainer = new FormElementDetails({ From 564085466d1e0a02fad50fea92ad5d4f9aa5b49f Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Mon, 22 Nov 2021 22:08:21 +0100 Subject: [PATCH 19/24] Fixed problem with ts-lint --- src/js/core/modal_dialog_forms.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/core/modal_dialog_forms.js b/src/js/core/modal_dialog_forms.js index 7d6126a5..ee2e22f5 100644 --- a/src/js/core/modal_dialog_forms.js +++ b/src/js/core/modal_dialog_forms.js @@ -89,7 +89,7 @@ export class FormElementDetails extends FormElement { return formElementValues; } - focus(parent) {} + focus() {} } export class FormElementInput extends FormElement { @@ -236,7 +236,7 @@ export class FormElementCheckbox extends FormElement { this.element.classList.toggle("checked", this.value); } - focus(parent) {} + focus() {} } export class FormElementItemChooser extends FormElement { From 3897b41d45e6d09a5bff78e2fe48e25a06fa8d31 Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sat, 27 Nov 2021 15:12:53 +0100 Subject: [PATCH 20/24] Add variables for shape-percentages - Add `fullShapePercentage` and `wierdShapePercentage` to `BaseMap` - added migration for `fullShapePercentage` and `wierdShapePercentage` in `savegame/1010.js` Signed-off-by: Daan Breur --- src/js/game/map.js | 4 ++++ src/js/savegame/schemas/1010.js | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/js/game/map.js b/src/js/game/map.js index 8edff48b..c59341b9 100644 --- a/src/js/game/map.js +++ b/src/js/game/map.js @@ -16,6 +16,8 @@ export class BaseMap extends BasicSerializableObject { return { seed: types.uint, allowNonPrimaryColors: types.bool, + fullShapePercentage: types.uint, + wierdShapePercentage: types.uint, }; } @@ -29,6 +31,8 @@ export class BaseMap extends BasicSerializableObject { this.seed = 0; this.allowNonPrimaryColors = false; + this.fullShapePercentage = 0; + this.wierdShapePercentage = 0; /** * Mapping of 'X|Y' to chunk diff --git a/src/js/savegame/schemas/1010.js b/src/js/savegame/schemas/1010.js index e1917581..56cca297 100644 --- a/src/js/savegame/schemas/1010.js +++ b/src/js/savegame/schemas/1010.js @@ -26,5 +26,13 @@ export class SavegameInterface_V1010 extends SavegameInterface_V1009 { if (!dump.map.hasOwnProperty("allowNonPrimaryColors")) { dump.map.allowNonPrimaryColors = false; } + + if (!dump.map.hasOwnProperty("fullShapePercentage")) { + dump.map.fullShapePercentage = 0; + } + + if (!dump.map.hasOwnProperty("wierdShapePercentage")) { + dump.map.wierdShapePercentage = 0; + } } } From ca12bcaaa337ef48819c232a381d3d841fd5827c Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sat, 27 Nov 2021 15:26:54 +0100 Subject: [PATCH 21/24] Use shape-percentage when generating shapes. Signed-off-by: Daan Breur --- src/js/game/map_chunk.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/js/game/map_chunk.js b/src/js/game/map_chunk.js index 2283a674..00d41408 100644 --- a/src/js/game/map_chunk.js +++ b/src/js/game/map_chunk.js @@ -203,7 +203,19 @@ export class MapChunk { weights[enumSubShape.windmill] = 0; } - if (distanceToOriginInChunks < 10) { + if (rng.nextRange(0, 100) <= this.root.map.fullShapePercentage) { + // Spawn full shape based on percentage. + const subShape = this.internalGenerateRandomSubShape(rng, weights); + subShapes = [subShape, subShape, subShape, subShape]; + } else if (rng.nextRange(0, 100) <= this.root.map.wierdShapePercentage) { + // Spawn wierd shape based on percentage. + subShapes = [ + this.internalGenerateRandomSubShape(rng, weights), + this.internalGenerateRandomSubShape(rng, weights), + this.internalGenerateRandomSubShape(rng, weights), + this.internalGenerateRandomSubShape(rng, weights), + ]; + } else if (distanceToOriginInChunks < 10) { // Initial chunk patches always have the same shape const subShape = this.internalGenerateRandomSubShape(rng, weights); subShapes = [subShape, subShape, subShape, subShape]; From ceb54c69b6be98ffd3165b81b1730724d444c91c Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sat, 27 Nov 2021 15:32:48 +0100 Subject: [PATCH 22/24] Get shape-percentage and use it in root.map Signed-off-by: Daan Breur --- src/js/game/core.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/js/game/core.js b/src/js/game/core.js index 2b49a9fd..2e31ebbf 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -170,12 +170,16 @@ export class GameCore { * @param {object} param1 * @param {number} param1.seed * @param {boolean} param1.allowNonPrimaryColors + * @param {number} param1.fullShapePercentage + * @param {number} param1.wierdShapePercentage * */ - initNewGame({ seed, allowNonPrimaryColors }) { + initNewGame({ seed, allowNonPrimaryColors, fullShapePercentage, wierdShapePercentage }) { logger.log("Initializing new game"); this.root.gameIsFresh = true; this.root.map.seed = seed; this.root.map.allowNonPrimaryColors = allowNonPrimaryColors; + this.root.map.fullShapePercentage = fullShapePercentage; + this.root.map.wierdShapePercentage = wierdShapePercentage; logger.log("Initializing newGame with seed: ", this.root.map.seed); From 37c7ffb6f73a8f28bddb4c4814a90b50bd7a979c Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sat, 27 Nov 2021 15:33:46 +0100 Subject: [PATCH 23/24] Edit GameCreationPayload - `fullShapePercentage` and `wierdShapePercentage` added to `GameCreationPayload` - Use the updated `GameCreationPayload` Signed-off-by: Daan Breur --- src/js/states/ingame.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js index d0ad8e99..220f31ea 100644 --- a/src/js/states/ingame.js +++ b/src/js/states/ingame.js @@ -54,6 +54,12 @@ export class GameCreationPayload { /** @type {boolean} */ this.allowNonPrimaryColors; + + /** @type {number} */ + this.fullShapePercentage; + + /** @type {number} */ + this.wierdShapePercentage; } } @@ -85,6 +91,12 @@ export class InGameState extends GameState { /** @type {boolean} */ this.allowNonPrimaryColors = null; + /** @type {number} */ + this.fullShapePercentage = null; + + /** @type {number} */ + this.wierdShapePercentage = null; + this.boundInputFilter = this.filterInput.bind(this); /** @@ -262,7 +274,12 @@ export class InGameState extends GameState { */ stage4aInitEmptyGame() { if (this.switchStage(stages.s4_A_initEmptyGame)) { - this.core.initNewGame({ seed: this.seed, allowNonPrimaryColors: this.allowNonPrimaryColors }); + this.core.initNewGame({ + seed: this.seed, + allowNonPrimaryColors: this.allowNonPrimaryColors, + fullShapePercentage: this.fullShapePercentage, + wierdShapePercentage: this.wierdShapePercentage, + }); this.stage5FirstUpdate(); } } @@ -383,6 +400,8 @@ export class InGameState extends GameState { this.gameModeId = payload.gameModeId; this.seed = payload.seed; this.allowNonPrimaryColors = payload.allowNonPrimaryColors; + this.fullShapePercentage = payload.fullShapePercentage; + this.wierdShapePercentage = payload.wierdShapePercentage; this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement()); this.loadingOverlay.showBasic(); From dfe90426ce1f19e47445bfaafc526a57bea05a5c Mon Sep 17 00:00:00 2001 From: Daan Breur Date: Sat, 27 Nov 2021 15:35:05 +0100 Subject: [PATCH 24/24] Temp UI for new shape-percentages --- src/js/states/main_menu.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index fa579e31..6380ef32 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -703,11 +703,34 @@ export class MainMenuState extends GameState { inline: true, }); + const fullShapePercentageInput = new FormElementInput({ + id: "fullShapePercentageInput", + label: "fullShape %:", + placeholder: "", + defaultValue: Number(0).toString(), + validator: val => Number.isInteger(Number(val)) && Number(val) >= 0 && Number(val) <= 100, + inline: true, + }); + + const wierdShapePercentageInput = new FormElementInput({ + id: "wierdShapePercentageInput", + label: "wierdShape %:", + placeholder: "", + defaultValue: Number(0).toString(), + validator: val => Number.isInteger(Number(val)) && Number(val) >= 0 && Number(val) <= 100, + inline: true, + }); + const advancedContainer = new FormElementDetails({ id: "advancedContainer", // @TODO Add translation (T.dialogs.newSavegame.advanced) label: "Advanced Options", - formElements: [seedInput, allowColorsCheckbox], + formElements: [ + seedInput, + allowColorsCheckbox, + fullShapePercentageInput, + wierdShapePercentageInput, + ], }); const dialog = new DialogWithForm({ @@ -735,6 +758,8 @@ export class MainMenuState extends GameState { savegame, seed: Number(seedInput.getValue()), allowNonPrimaryColors: allowColorsCheckbox.getValue(), + fullShapePercentage: Number(fullShapePercentageInput.getValue()), + wierdShapePercentage: Number(wierdShapePercentageInput.getValue()), }); this.app.analytics.trackUiClick("startgame_adcomplete"); });