From ca0e17f3dd6a6c9d25cb2e8b14067f715f7a7499 Mon Sep 17 00:00:00 2001 From: tobspr Date: Mon, 18 May 2020 12:53:01 +0200 Subject: [PATCH] Support dynamic tick rates --- src/css/ingame_hud/debug_info.scss | 10 ++ src/css/main.scss | 2 + src/js/core/config.js | 14 +-- src/js/game/components/underground_belt.js | 5 +- src/js/game/core.js | 32 ++--- src/js/game/dynamic_tickrate.js | 104 ++++++++++++++++ src/js/game/hud/hud.js | 2 + src/js/game/hud/parts/debug_info.js | 19 +++ src/js/game/root.js | 4 + src/js/game/systems/belt.js | 136 ++++++++++++--------- src/js/game/systems/item_acceptor.js | 2 +- src/js/game/systems/item_ejector.js | 2 +- src/js/game/systems/item_processor.js | 2 +- src/js/game/systems/underground_belt.js | 2 +- src/js/game/time/game_time.js | 22 ++-- src/js/states/main_menu.js | 7 +- 16 files changed, 258 insertions(+), 107 deletions(-) create mode 100644 src/css/ingame_hud/debug_info.scss create mode 100644 src/js/game/dynamic_tickrate.js create mode 100644 src/js/game/hud/parts/debug_info.js diff --git a/src/css/ingame_hud/debug_info.scss b/src/css/ingame_hud/debug_info.scss new file mode 100644 index 00000000..2e8c2732 --- /dev/null +++ b/src/css/ingame_hud/debug_info.scss @@ -0,0 +1,10 @@ +#ingame_HUD_DebugInfo { + position: absolute; + @include S(bottom, 5px); + @include S(left, 5px); + + font-size: 15px; + display: flex; + line-height: 15px; + flex-direction: column; +} diff --git a/src/css/main.scss b/src/css/main.scss index a17833c8..e998ae05 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -39,6 +39,7 @@ @import "ingame_hud/pinned_shapes"; @import "ingame_hud/notifications"; @import "ingame_hud/settings_menu"; +@import "ingame_hud/debug_info"; // prettier-ignore $elements: @@ -57,6 +58,7 @@ ingame_HUD_GameMenu, ingame_HUD_KeybindingOverlay, ingame_HUD_Notifications, ingame_HUD_MassSelector, +ingame_HUD_DebugInfo, // Overlays ingame_HUD_BetaOverlay, diff --git a/src/js/core/config.js b/src/js/core/config.js index 2999711c..3e56c156 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -23,13 +23,8 @@ export const globalConfig = { statisticsGraphSlices: 100, analyticsSliceDurationSeconds: 10, - // [Calculated] physics step size - physicsDeltaMs: 0, - physicsDeltaSeconds: 0, - - // Update physics at N fps, independent of rendering - // physicsUpdateRate: 55, - physicsUpdateRate: 120, + minimumTickRate: 30, + maximumTickRate: 500, // Map mapChunkSize: 32, @@ -76,7 +71,7 @@ export const globalConfig = { debug: { /* dev:start */ - fastGameEnter: true, + // fastGameEnter: true, noArtificialDelays: true, // disableSavegameWrite: true, showEntityBounds: false, @@ -111,7 +106,4 @@ export const IS_MOBILE = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); // Automatic calculations -globalConfig.physicsDeltaMs = 1000.0 / globalConfig.physicsUpdateRate; -globalConfig.physicsDeltaSeconds = 1.0 / globalConfig.physicsUpdateRate; - globalConfig.minerSpeedItemsPerSecond = globalConfig.beltSpeedItemsPerSecond / 5; diff --git a/src/js/game/components/underground_belt.js b/src/js/game/components/underground_belt.js index 21d3c0b4..113e66ca 100644 --- a/src/js/game/components/underground_belt.js +++ b/src/js/game/components/underground_belt.js @@ -62,7 +62,7 @@ export class UndergroundBeltComponent extends Component { return false; } - this.pendingItems.push([item, 1 / beltSpeed / globalConfig.itemSpacingOnBelts]); + this.pendingItems.push([item, 0]); return true; } @@ -88,7 +88,8 @@ export class UndergroundBeltComponent extends Component { // NOTICE: // This corresponds to the item ejector - it needs 0.5 additional tiles to eject the item. // So instead of adding 1 we add 0.5 only. - const travelDuration = (travelDistance + 0.5) / beltSpeed / globalConfig.itemSpacingOnBelts; + // Additionally it takes 1 tile for the acceptor which we just add on top. + const travelDuration = (travelDistance + 1.5) / beltSpeed / globalConfig.itemSpacingOnBelts; this.pendingItems.push([item, travelDuration]); diff --git a/src/js/game/core.js b/src/js/game/core.js index b69d8227..b9e50856 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -32,6 +32,7 @@ import { GameTime } from "./time/game_time"; import { ProductionAnalytics } from "./production_analytics"; import { randomInt } from "../core/utils"; import { defaultBuildingVariant } from "./meta_building"; +import { DynamicTickrate } from "./dynamic_tickrate"; const logger = createLogger("ingame/core"); @@ -53,16 +54,6 @@ export class GameCore { /** @type {GameRoot} */ this.root = null; - /** - * Time budget (seconds) for logic updates - */ - this.logicTimeBudget = 0; - - /** - * Time budget (seconds) for user interface updates - */ - this.uiTimeBudget = 0; - /** * 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 @@ -97,6 +88,9 @@ export class GameCore { // This isn't nice, but we need it right here root.gameState.keyActionMapper = new KeyActionMapper(root, this.root.gameState.inputReciever); + // Needs to come first + root.dynamicTickrate = new DynamicTickrate(root); + // Init classes root.camera = new Camera(root); root.map = new MapView(root); @@ -250,17 +244,6 @@ export class GameCore { // Perform logic ticks this.root.time.performTicks(deltaMs, this.boundInternalTick); - // Update UI particles - this.uiTimeBudget += deltaMs; - const maxUiSteps = 3; - if (this.uiTimeBudget > globalConfig.physicsDeltaMs * maxUiSteps) { - this.uiTimeBudget = globalConfig.physicsDeltaMs; - } - while (this.uiTimeBudget >= globalConfig.physicsDeltaMs) { - this.uiTimeBudget -= globalConfig.physicsDeltaMs; - // root.uiParticleMgr.update(); - } - // Update analytics root.productionAnalytics.update(); @@ -288,6 +271,9 @@ export class GameCore { updateLogic() { const root = this.root; + + root.dynamicTickrate.beginTick(); + this.duringLogicUpdate = true; // Update entities, this removes destroyed entities @@ -296,6 +282,8 @@ export class GameCore { // 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; } @@ -303,7 +291,7 @@ export class GameCore { // root.particleMgr.update(); this.duringLogicUpdate = false; - + root.dynamicTickrate.endTick(); return true; } diff --git a/src/js/game/dynamic_tickrate.js b/src/js/game/dynamic_tickrate.js new file mode 100644 index 00000000..bfa5d42d --- /dev/null +++ b/src/js/game/dynamic_tickrate.js @@ -0,0 +1,104 @@ +import { GameRoot } from "./root"; +import { createLogger } from "../core/logging"; +import { globalConfig } from "../core/config"; +import { performanceNow, Math_min, Math_round, Math_max } from "../core/builtins"; +import { round3Digits } from "../core/utils"; + +const logger = createLogger("dynamic_tickrate"); + +export class DynamicTickrate { + /** + * + * @param {GameRoot} root + */ + constructor(root) { + this.root = root; + + this.setTickRate(120); + + this.currentTickStart = null; + this.capturedTicks = []; + this.averageTickDuration = 0; + + // Exposed + this.deltaSeconds = 0; + this.deltaMs = 0; + } + + /** + * Sets the tick rate to N updates per second + * @param {number} rate + */ + setTickRate(rate) { + logger.log("Applying tick-rate of", rate); + this.currentTickRate = rate; + this.deltaMs = 1000.0 / this.currentTickRate; + this.deltaSeconds = 1.0 / this.currentTickRate; + } + + /** + * Increases the tick rate marginally + */ + increaseTickRate() { + this.setTickRate(Math_round(Math_min(globalConfig.maximumTickRate, this.currentTickRate * 1.1))); + } + + /** + * Decreases the tick rate marginally + */ + decreaseTickRate() { + this.setTickRate(Math_round(Math_min(globalConfig.maximumTickRate, this.currentTickRate * 0.9))); + } + + /** + * Call whenever a tick began + */ + beginTick() { + assert(this.currentTickStart === null, "BeginTick called twice"); + this.currentTickStart = performanceNow(); + + if (this.capturedTicks.length > this.currentTickRate * 4) { + // Take only a portion of the ticks + this.capturedTicks.sort(); + this.capturedTicks.splice(0, 10); + this.capturedTicks.splice(this.capturedTicks.length - 11, 10); + + let average = 0; + for (let i = 0; i < this.capturedTicks.length; ++i) { + average += this.capturedTicks[i]; + } + average /= this.capturedTicks.length; + + // Calculate tick duration to cover X% of the frame + const ticksPerFrame = this.currentTickRate / 60; + const maxFrameDurationMs = 8; + const maxTickDuration = maxFrameDurationMs / ticksPerFrame; + // const maxTickDuration = (1000 / this.currentTickRate) * 0.75; + logger.log( + "Average time per tick:", + round3Digits(average) + "ms", + "allowed are", + maxTickDuration + ); + this.averageTickDuration = average; + + if (average < maxTickDuration) { + this.increaseTickRate(); + } else { + this.decreaseTickRate(); + } + + this.capturedTicks = []; + } + } + + /** + * Call whenever a tick ended + */ + endTick() { + assert(this.currentTickStart !== null, "EndTick called without BeginTick"); + const duration = performanceNow() - this.currentTickStart; + this.capturedTicks.push(duration); + this.currentTickStart = null; + } +} diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index 9b2e7993..73091457 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -21,6 +21,7 @@ import { HUDPinnedShapes } from "./parts/pinned_shapes"; import { ShapeDefinition } from "../shape_definition"; import { HUDNotifications, enumNotificationType } from "./parts/notifications"; import { HUDSettingsMenu } from "./parts/settings_menu"; +import { HUDDebugInfo } from "./parts/debug_info"; export class GameHUD { /** @@ -57,6 +58,7 @@ export class GameHUD { settingsMenu: new HUDSettingsMenu(this.root), // betaOverlay: new HUDBetaOverlay(this.root), + debugInfo: new HUDDebugInfo(this.root), }; this.signals = { diff --git a/src/js/game/hud/parts/debug_info.js b/src/js/game/hud/parts/debug_info.js new file mode 100644 index 00000000..74ce7fe6 --- /dev/null +++ b/src/js/game/hud/parts/debug_info.js @@ -0,0 +1,19 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv, round3Digits } from "../../../core/utils"; + +export class HUDDebugInfo extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_DebugInfo", []); + + this.tickRateElement = makeDiv(this.element, null, ["tickRate"], "Ticks /s: 120"); + this.tickDurationElement = makeDiv(this.element, null, ["tickDuration"], "Update time: 0.5ms"); + } + + initialize() {} + + update() { + this.tickRateElement.innerText = "Tickrate: " + this.root.dynamicTickrate.currentTickRate; + this.tickDurationElement.innerText = + "Avg. Dur: " + round3Digits(this.root.dynamicTickrate.averageTickDuration) + "ms"; + } +} diff --git a/src/js/game/root.js b/src/js/game/root.js index 72658fc5..5e979729 100644 --- a/src/js/game/root.js +++ b/src/js/game/root.js @@ -26,6 +26,7 @@ import { ProductionAnalytics } from "./production_analytics"; import { Entity } from "./entity"; import { ShapeDefinition } from "./shape_definition"; import { BaseItem } from "./base_item"; +import { DynamicTickrate } from "./dynamic_tickrate"; /* typehints:end */ const logger = createLogger("game/root"); @@ -115,6 +116,9 @@ export class GameRoot { /** @type {ProductionAnalytics} */ this.productionAnalytics = null; + /** @type {DynamicTickrate} */ + this.dynamicTickrate = null; + this.signals = { // Entities entityAdded: /** @type {TypedSignal<[Entity]>} */ (new Signal()), diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index f3166d76..9ecf8ba1 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -109,81 +109,101 @@ export class BeltSystem extends GameSystemWithFilter { this.forEachMatchingEntityOnScreen(parameters, this.drawEntityItems.bind(this)); } - update() { + /** + * Updates a given entity + * @param {Entity} entity + * @param {Set} processedEntities + */ + updateBelt(entity, processedEntities) { + if (processedEntities.has(entity.uid)) { + return; + } + + processedEntities.add(entity.uid); + // Divide by item spacing on belts since we use throughput and not speed const beltSpeed = this.root.hubGoals.getBeltBaseSpeed() * - globalConfig.physicsDeltaSeconds * + this.root.dynamicTickrate.deltaSeconds * globalConfig.itemSpacingOnBelts; + const beltComp = entity.components.Belt; + const staticComp = entity.components.StaticMapEntity; + const items = beltComp.sortedItems; - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - const beltComp = entity.components.Belt; - const staticComp = entity.components.StaticMapEntity; - const items = beltComp.sortedItems; - if (items.length === 0) { - // Fast out for performance - continue; - } + if (items.length === 0) { + // Fast out for performance + return; + } - const ejectorComp = entity.components.ItemEjector; - let maxProgress = 1; + const ejectorComp = entity.components.ItemEjector; + let maxProgress = 1; - // When ejecting, we can not go further than the item spacing since it - // will be on the corner - if (ejectorComp.isAnySlotEjecting()) { - maxProgress = 1 - globalConfig.itemSpacingOnBelts; - } else { - // Find follow up belt to make sure we don't clash items - const followUpDirection = staticComp.localDirectionToWorld(beltComp.direction); - const followUpVector = enumDirectionToVector[followUpDirection]; - - const followUpTile = staticComp.origin.add(followUpVector); - const followUpEntity = this.root.map.getTileContent(followUpTile); - - if (followUpEntity) { - const followUpBeltComp = followUpEntity.components.Belt; - if (followUpBeltComp) { - const spacingOnBelt = followUpBeltComp.getDistanceToFirstItemCenter(); - maxProgress = Math_min(1, 1 - globalConfig.itemSpacingOnBelts + spacingOnBelt); - } + // When ejecting, we can not go further than the item spacing since it + // will be on the corner + if (ejectorComp.isAnySlotEjecting()) { + maxProgress = 1 - globalConfig.itemSpacingOnBelts; + } else { + // Find follow up belt to make sure we don't clash items + const followUpDirection = staticComp.localDirectionToWorld(beltComp.direction); + const followUpVector = enumDirectionToVector[followUpDirection]; + + const followUpTile = staticComp.origin.add(followUpVector); + const followUpEntity = this.root.map.getTileContent(followUpTile); + + if (followUpEntity) { + const followUpBeltComp = followUpEntity.components.Belt; + if (followUpBeltComp) { + // Update follow up belt first + this.updateBelt(followUpEntity, processedEntities); + + const spacingOnBelt = followUpBeltComp.getDistanceToFirstItemCenter(); + maxProgress = Math_min(1, 1 - globalConfig.itemSpacingOnBelts + spacingOnBelt); } } + } - let speedMultiplier = 1; - if (beltComp.direction !== enumDirection.top) { - // Shaped belts are longer, thus being quicker - speedMultiplier = 1.41; - } + let speedMultiplier = 1; + if (beltComp.direction !== enumDirection.top) { + // Shaped belts are longer, thus being quicker + speedMultiplier = 1.41; + } - for (let itemIndex = items.length - 1; itemIndex >= 0; --itemIndex) { - const itemAndProgress = items[itemIndex]; - - const newProgress = itemAndProgress[0] + speedMultiplier * beltSpeed; - if (newProgress >= 1.0) { - // Try to give this item to a new belt - const freeSlot = ejectorComp.getFirstFreeSlot(); - - if (freeSlot === null) { - // So, we don't have a free slot - damned! - itemAndProgress[0] = 1.0; - maxProgress = 1 - globalConfig.itemSpacingOnBelts; - } else { - // We got a free slot, remove this item and keep it on the ejector slot - if (!ejectorComp.tryEject(freeSlot, itemAndProgress[1])) { - assert(false, "Ejection failed"); - } - items.splice(itemIndex, 1); - maxProgress = 1; - } + for (let itemIndex = items.length - 1; itemIndex >= 0; --itemIndex) { + const itemAndProgress = items[itemIndex]; + + const newProgress = itemAndProgress[0] + speedMultiplier * beltSpeed; + if (newProgress >= 1.0) { + // Try to give this item to a new belt + const freeSlot = ejectorComp.getFirstFreeSlot(); + + if (freeSlot === null) { + // So, we don't have a free slot - damned! + itemAndProgress[0] = 1.0; + maxProgress = 1 - globalConfig.itemSpacingOnBelts; } else { - itemAndProgress[0] = Math_min(newProgress, maxProgress); - maxProgress = itemAndProgress[0] - globalConfig.itemSpacingOnBelts; + // We got a free slot, remove this item and keep it on the ejector slot + if (!ejectorComp.tryEject(freeSlot, itemAndProgress[1])) { + assert(false, "Ejection failed"); + } + items.splice(itemIndex, 1); + maxProgress = 1; } + } else { + itemAndProgress[0] = Math_min(newProgress, maxProgress); + maxProgress = itemAndProgress[0] - globalConfig.itemSpacingOnBelts; } } } + update() { + const processedEntities = new Set(); + + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + this.updateBelt(entity, processedEntities); + } + } + /** * * @param {DrawParameters} parameters diff --git a/src/js/game/systems/item_acceptor.js b/src/js/game/systems/item_acceptor.js index 51ce07d6..6a3bec0f 100644 --- a/src/js/game/systems/item_acceptor.js +++ b/src/js/game/systems/item_acceptor.js @@ -31,7 +31,7 @@ export class ItemAcceptorSystem extends GameSystemWithFilter { for (let animIndex = 0; animIndex < aceptorComp.itemConsumptionAnimations.length; ++animIndex) { const anim = aceptorComp.itemConsumptionAnimations[animIndex]; anim.animProgress += - globalConfig.physicsDeltaSeconds * + this.root.dynamicTickrate.deltaSeconds * this.root.hubGoals.getBeltBaseSpeed() * 2 * globalConfig.itemSpacingOnBelts; diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index 890b44bc..da3e5c7c 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -14,7 +14,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter { update() { const effectiveBeltSpeed = this.root.hubGoals.getBeltBaseSpeed() * globalConfig.itemSpacingOnBelts; - const progressGrowth = (effectiveBeltSpeed / 0.5) * globalConfig.physicsDeltaSeconds; + const progressGrowth = (effectiveBeltSpeed / 0.5) * this.root.dynamicTickrate.deltaSeconds; // Try to find acceptors for every ejector for (let i = 0; i < this.allEntities.length; ++i) { diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index ddd02be0..185e4c41 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -23,7 +23,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { // First of all, process the current recipe processorComp.secondsUntilEject = Math_max( 0, - processorComp.secondsUntilEject - globalConfig.physicsDeltaSeconds + processorComp.secondsUntilEject - this.root.dynamicTickrate.deltaSeconds ); // Check if we have any finished items we can eject diff --git a/src/js/game/systems/underground_belt.js b/src/js/game/systems/underground_belt.js index b3c72cf3..538fab36 100644 --- a/src/js/game/systems/underground_belt.js +++ b/src/js/game/systems/underground_belt.js @@ -31,7 +31,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { // Decrease remaining time of all items in belt for (let k = 0; k < undergroundComp.pendingItems.length; ++k) { const item = undergroundComp.pendingItems[k]; - item[1] = Math_max(0, item[1] - globalConfig.physicsDeltaSeconds); + item[1] = Math_max(0, item[1] - this.root.dynamicTickrate.deltaSeconds); } if (undergroundComp.mode === enumUndergroundBeltMode.sender) { diff --git a/src/js/game/time/game_time.js b/src/js/game/time/game_time.js index 3fe48453..f83cbe79 100644 --- a/src/js/game/time/game_time.js +++ b/src/js/game/time/game_time.js @@ -6,7 +6,7 @@ import { types, BasicSerializableObject } from "../../savegame/serialization"; import { RegularGameSpeed } from "./regular_game_speed"; import { BaseGameSpeed } from "./base_game_speed"; import { PausedGameSpeed } from "./paused_game_speed"; -import { performanceNow } from "../../core/builtins"; +import { performanceNow, Math_max } from "../../core/builtins"; import { FastForwardGameSpeed } from "./fast_forward_game_speed"; import { gGameSpeedRegistry } from "../../core/global_registries"; import { globalConfig } from "../../core/config"; @@ -102,7 +102,7 @@ export class GameTime extends BasicSerializableObject { * Internal method to generate new logic time budget * @param {number} deltaMs */ - înternalAddDeltaToBudget(deltaMs) { + internalAddDeltaToBudget(deltaMs) { // Only update if game is supposed to update if (this.root.hud.shouldPauseGame()) { this.logicTimeBudget = 0; @@ -112,9 +112,13 @@ export class GameTime extends BasicSerializableObject { } // Check for too big pile of updates -> reduce it to 1 - const maxLogicSteps = this.speed.getMaxLogicStepsInQueue(); - if (this.logicTimeBudget > globalConfig.physicsDeltaMs * maxLogicSteps) { - this.logicTimeBudget = globalConfig.physicsDeltaMs * maxLogicSteps; + const maxLogicSteps = Math_max( + 3, + (this.speed.getMaxLogicStepsInQueue() * this.root.dynamicTickrate.currentTickRate) / 60 + ); + if (this.logicTimeBudget > this.root.dynamicTickrate.deltaMs * maxLogicSteps) { + // logger.warn("Skipping logic time steps since more than", maxLogicSteps, "are in queue"); + this.logicTimeBudget = this.root.dynamicTickrate.deltaMs * maxLogicSteps; } } @@ -124,13 +128,13 @@ export class GameTime extends BasicSerializableObject { * @param {function():boolean} updateMethod */ performTicks(deltaMs, updateMethod) { - this.înternalAddDeltaToBudget(deltaMs); + this.internalAddDeltaToBudget(deltaMs); const speedAtStart = this.root.time.getSpeed(); // Update physics & logic - while (this.logicTimeBudget >= globalConfig.physicsDeltaMs) { - this.logicTimeBudget -= globalConfig.physicsDeltaMs; + while (this.logicTimeBudget >= this.root.dynamicTickrate.deltaMs) { + this.logicTimeBudget -= this.root.dynamicTickrate.deltaMs; if (!updateMethod()) { // Gameover happened or so, do not update anymore @@ -138,7 +142,7 @@ export class GameTime extends BasicSerializableObject { } // Step game time - this.timeSeconds = quantizeFloat(this.timeSeconds + globalConfig.physicsDeltaSeconds); + this.timeSeconds = quantizeFloat(this.timeSeconds + this.root.dynamicTickrate.deltaSeconds); // Game time speed changed, need to abort since our logic steps are no longer valid if (speedAtStart.getId() !== this.speed.getId()) { diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 8335e350..b0853a8c 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -179,7 +179,12 @@ export class MainMenuState extends GameState { this.trackClicks(qs(".mainContainer .importButton"), this.requestImportSavegame); if (G_IS_DEV && globalConfig.debug.fastGameEnter) { - this.onPlayButtonClicked(); + const games = this.app.savegameMgr.getSavegamesMetaData(); + if (games.length > 0) { + this.resumeGame(games[0]); + } else { + this.onPlayButtonClicked(); + } } // Initialize video