Add backup save modal and loading functionality

This commit is contained in:
SpectralFlame 2023-06-24 00:42:41 -05:00 committed by cyip92
parent b076c38504
commit 3a50050f79
5 changed files with 210 additions and 7 deletions

View File

@ -0,0 +1,105 @@
<script>
import PrimaryButton from "@/components/PrimaryButton";
import { BACKUP_SLOT_TYPE } from "@/core/storage";
export default {
name: "BackupEntry",
components: {
PrimaryButton
},
props: {
slotData: {
type: Object,
required: true
}
},
data() {
return {
currTime: 0,
};
},
computed: {
save() {
return GameStorage.loadFromBackup(this.slotData.id);
},
progressStr() {
if (!this.save) return "(Empty)";
const rm = new Decimal(this.save.reality.realityMachines);
if (rm.gt(0)) return `Reality Machines: ${format(new Decimal(rm), 2)}`;
const ep = new Decimal(this.save.eternityPoints);
if (ep.gt(0)) return `Eternity Points: ${format(new Decimal(ep), 2)}`;
const ip = new Decimal(this.save.infinityPoints);
if (ip.gt(0)) return `Infinity Points: ${format(new Decimal(ip), 2)}`;
return `Antimatter: ${formatPostBreak(new Decimal(this.save.antimatter), 2, 1)}`;
},
slotType() {
const formattedTime = this.slotData.intervalStr?.();
switch (this.slotData.type) {
case BACKUP_SLOT_TYPE.ONLINE:
return `Saves every ${formattedTime} online`;
case BACKUP_SLOT_TYPE.OFFLINE:
return `Saves after ${formattedTime} offline`;
case BACKUP_SLOT_TYPE.RESERVE:
return "Pre-loading save";
default:
throw new Error("Unrecognized backup save type");
}
},
lastSaved() {
const lastSave = GameStorage.backupTimeData[this.slotData.id].last;
return lastSave
? `Last saved: ${TimeSpan.fromMilliseconds(this.currTime - lastSave)} ago`
: "Slot not currently in use";
}
},
methods: {
update() {
this.currTime = Date.now();
},
load() {
if (!this.save) return;
// This seems to be the only way to properly hide the modal after the save is properly loaded,
// since the offline progress modal appears nearly immediately after clicking the button
Modal.hide();
if (this.slotData.type !== BACKUP_SLOT_TYPE.RESERVE) GameStorage.saveToReserveSlot();
GameStorage.loadPlayerObject(this.save);
GameUI.notify.info(`Game loaded from backup slot #${this.slotData.id}`);
GameStorage.processLocalBackups();
},
},
};
</script>
<template>
<div class="c-bordered-entry">
<h3>Slot #{{ slotData.id }}:</h3>
<span>{{ progressStr }}</span>
<span>{{ slotType }}</span>
<span class="c-fixed-height">{{ lastSaved }}</span>
<PrimaryButton
class="o-primary-btn--width-medium"
:class="{ 'o-primary-btn--disabled' : !save }"
@click="load()"
>
Load
</PrimaryButton>
</div>
</template>
<style scoped>
.c-bordered-entry {
display: flex;
flex-direction: column;
align-items: center;
font-size: 1.1rem;
border: var(--var-border-width, 0.2rem) solid;
border-radius: var(--var-border-radius, 0.4rem);
padding: 0.5rem 0.3rem;
margin: 0.3rem;
}
.c-fixed-height {
height: 4rem;
}
</style>

View File

@ -0,0 +1,65 @@
<script>
import BackupEntry from "@/components/modals/options/BackupEntry";
import ModalWrapper from "@/components/modals/ModalWrapper";
import { AutoBackupSlots } from "@/core/storage";
export default {
name: "BackupWindowModal",
components: {
ModalWrapper,
BackupEntry
},
data() {
return {
// Used to force a key-swap whenever a save happens, to make unused slots immediately update
nextSave: 0,
};
},
computed: {
backupSlots: () => AutoBackupSlots,
},
methods: {
update() {
this.nextSave = GameStorage.nextBackup;
}
}
};
</script>
<template>
<ModalWrapper>
<template #header>
Automatic Backup Saves
</template>
<div class="c-info">
The game makes automatic backups based on time you have spent online or offline.
Additionally, your current save is saved into the last slot any time a backup from here is loaded.
<div class="c-entry-container">
<BackupEntry
v-for="slot in backupSlots"
:key="nextSave + slot.id"
class="l-backup-entry"
:slot-data="slot"
/>
</div>
</div>
</ModalWrapper>
</template>
<style scoped>
.c-info {
width: 60rem;
}
.c-entry-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.l-backup-entry {
width: calc(50% - 0.6rem);
height: calc(25% - 0.6rem);
}
</style>

View File

@ -189,7 +189,15 @@ export default {
/>
</div>
<div class="l-options-grid__row">
<OptionsButton
:class="{ 'o-pelle-disabled-pointer': creditsClosed }"
onclick="Modal.backupWindows.show()"
>
Open Automatic Save Backup Menu
</OptionsButton>
<SaveFileName />
</div>
<div class="l-options-grid__row">
<OptionsButton
v-if="canSpeedrun"
class="o-primary-btn--option_font-x-large"

View File

@ -11,7 +11,6 @@ import NormalChallengeStartModal from "@/components/modals/challenges/NormalChal
import AntimatterGalaxyModal from "@/components/modals/prestige/AntimatterGalaxyModal";
import ArmageddonModal from "@/components/modals/prestige/ArmageddonModal";
import BigCrunchModal from "@/components/modals/prestige/BigCrunchModal";
import ChangeNameModal from "@/components/modals/ChangeNameModal";
import DimensionBoostModal from "@/components/modals/prestige/DimensionBoostModal";
import EnterCelestialsModal from "@/components/modals/prestige/EnterCelestialsModal";
import EnterDilationModal from "@/components/modals/prestige/EnterDilationModal";
@ -19,14 +18,13 @@ import EternityModal from "@/components/modals/prestige/EternityModal";
import ExitChallengeModal from "@/components/modals/prestige/ExitChallengeModal";
import ExitDilationModal from "@/components/modals/prestige/ExitDilationModal";
import HardResetModal from "@/components/modals/prestige/HardResetModal";
import ModifySeedModal from "@/components/modals/ModifySeedModal";
import RealityModal from "@/components/modals/prestige/RealityModal";
import ReplicantiGalaxyModal from "@/components/modals/prestige/ReplicantiGalaxyModal";
import ResetRealityModal from "@/components/modals/prestige/ResetRealityModal";
import SpeedrunModeModal from "@/components/modals/SpeedrunModeModal";
import AnimationOptionsModal from "@/components/modals/options/AnimationOptionsModal";
import AwayProgressOptionsModal from "@/components/modals/options/AwayProgressOptionsModal";
import BackupWindowModal from "@/components/modals/options/BackupWindowModal";
import ConfirmationOptionsModal from "@/components/modals/options/ConfirmationOptionsModal";
import CosmeticSetChoiceModal from "@/components/modals/options/glyph-appearance/CosmeticSetChoiceModal";
import GlyphDisplayOptionsModal from "@/components/modals/options/glyph-appearance/GlyphDisplayOptionsModal";
@ -51,6 +49,7 @@ import AwayProgressModal from "@/components/modals/AwayProgressModal";
import BreakInfinityModal from "@/components/modals/BreakInfinityModal";
import CatchupModal from "@/components/modals/catchup/CatchupModal";
import ChangelogModal from "@/components/modals/ChangelogModal";
import ChangeNameModal from "@/components/modals/ChangeNameModal";
import ClearConstantsModal from "@/components/modals/ClearConstantsModal";
import CreditsModal from "@/components/modals/CreditsModal";
import DeleteAutomatorScriptModal from "@/components/modals/DeleteAutomatorScriptModal";
@ -64,12 +63,14 @@ import ImportSaveModal from "@/components/modals/ImportSaveModal";
import ImportTimeStudyConstants from "@/components/modals/ImportTimeStudyConstants";
import InformationModal from "@/components/modals/InformationModal";
import LoadGameModal from "@/components/modals/LoadGameModal";
import ModifySeedModal from "@/components/modals/ModifySeedModal";
import PelleEffectsModal from "@/components/modals/PelleEffectsModal";
import RealityGlyphCreationModal from "@/components/modals/RealityGlyphCreationModal";
import ReplaceGlyphModal from "@/components/modals/ReplaceGlyphModal";
import RespecIAPModal from "@/components/modals/RespecIAPModal";
import SacrificeModal from "@/components/modals/SacrificeModal";
import SingularityMilestonesModal from "@/components/modals/SingularityMilestonesModal";
import SpeedrunModeModal from "@/components/modals/SpeedrunModeModal";
import StdStoreModal from "@/components/modals/StdStoreModal";
import StudyStringModal from "@/components/modals/StudyStringModal";
import SwitchAutomatorEditorModal from "@/components/modals/SwitchAutomatorEditorModal";
@ -211,6 +212,7 @@ Modal.reality = new Modal(RealityModal, 1, GAME_EVENT.REALITY_RESET_AFTER);
Modal.resetReality = new Modal(ResetRealityModal, 1, GAME_EVENT.REALITY_RESET_AFTER);
Modal.celestials = new Modal(EnterCelestialsModal, 1);
Modal.hardReset = new Modal(HardResetModal, 1);
Modal.backupWindows = new Modal(BackupWindowModal, 1);
Modal.enterSpeedrun = new Modal(SpeedrunModeModal);
Modal.modifySeed = new Modal(ModifySeedModal);
Modal.changeName = new Modal(ChangeNameModal);

View File

@ -6,47 +6,55 @@ import { migrations } from "./migrations";
import { deepmergeAll } from "@/utility/deepmerge";
const BACKUP_SLOT_TYPE = {
export const BACKUP_SLOT_TYPE = {
ONLINE: 0,
OFFLINE: 1,
RESERVE: 2,
};
// Note: interval is in seconds, and only the first RESERVE slot is ever used
// Note: interval is in seconds, and only the first RESERVE slot is ever used. Having intervalStr as a redundant
// prop is necessary because using our TimeSpan formatting functions produces undesirable strings like "1.00 minutes"
export const AutoBackupSlots = [
{
id: 1,
type: BACKUP_SLOT_TYPE.ONLINE,
intervalStr: () => `${formatInt(1)} minute`,
interval: 60,
},
{
id: 2,
type: BACKUP_SLOT_TYPE.ONLINE,
intervalStr: () => `${formatInt(5)} minutes`,
interval: 5 * 60,
},
{
id: 3,
type: BACKUP_SLOT_TYPE.ONLINE,
intervalStr: () => `${formatInt(20)} minutes`,
interval: 20 * 60,
},
{
id: 4,
type: BACKUP_SLOT_TYPE.ONLINE,
intervalStr: () => `${formatInt(1)} hour`,
interval: 3600,
},
{
id: 5,
type: BACKUP_SLOT_TYPE.OFFLINE,
intervalStr: () => `${formatInt(10)} minutes`,
interval: 10 * 60,
},
{
id: 6,
type: BACKUP_SLOT_TYPE.OFFLINE,
intervalStr: () => `${formatInt(1)} hour`,
interval: 3600,
},
{
id: 7,
type: BACKUP_SLOT_TYPE.OFFLINE,
intervalStr: () => `${formatInt(5)} hours`,
interval: 5 * 3600,
},
{
@ -115,6 +123,7 @@ export const GameStorage = {
};
this.currentSlot = 0;
this.loadPlayerObject(root);
this.processLocalBackups();
this.save(true);
return;
}
@ -130,6 +139,7 @@ export const GameStorage = {
// Save current slot to make sure no changes are lost
this.save(true);
this.loadPlayerObject(this.saves[slot] ?? Player.defaultStart);
this.processLocalBackups();
Tabs.all.find(t => t.id === player.options.lastOpenTab).show(false);
Modal.hideAll();
Cloud.resetTempState();
@ -250,6 +260,12 @@ export const GameStorage = {
localStorage.setItem(this.backupTimeKey(this.currentSlot), GameSaveSerializer.serialize(this.backupTimeData));
},
// Does not actually load, but returns an object which is meant to be passed on to loadPlayerObject()
loadFromBackup(backupSlot) {
const data = localStorage.getItem(this.backupDataKey(this.currentSlot, backupSlot));
return GameSaveSerializer.deserialize(data);
},
// This is called after the player object is loaded
processLocalBackups() {
// Set the next backup timer to whatever the next multiple of the shortest online interval is
@ -258,11 +274,12 @@ export const GameStorage = {
// Check for the amount of time spent offline and perform immediate backups for any slots
// which have had more than their timers elapse since the last time the game was open and saved
const offlineTimeMs = Date.now() - this.lastUpdateOnLoad;
const currentTime = Date.now();
const offlineTimeMs = currentTime - this.lastUpdateOnLoad;
for (const backupInfo of AutoBackupSlots.filter(slot => slot.type === BACKUP_SLOT_TYPE.OFFLINE)) {
if (offlineTimeMs < 1000 * backupInfo.interval) continue;
const id = backupInfo.id;
this.saveToBackup(id);
this.saveToBackup(id, currentTime);
}
// Load in all the data from previous backup times
@ -291,6 +308,12 @@ export const GameStorage = {
GameIntervals.backup.restart();
},
// Saves the current game state to the first reserve slot it finds
saveToReserveSlot() {
const targetSlot = AutoBackupSlots.find(slot => slot.type === 2).id;
this.saveToBackup(targetSlot, Date.now());
},
export() {
copyToClipboard(this.exportModifiedSave());
GameUI.notify.info("Exported current savefile to your clipboard");