const Memory = { /** * @type {State} */ state: new State(), /** * @returns {(Object|State)} */ saveToObject () { const prepareSaveData = (saveData) => { const prepareMonster = (monster) => { if (monster.statusEffect && monster.statusEffect.slug === 'lifeleech') { monster.statusEffect = null; } return monster; }; for (const idx in saveData.monsters) { saveData.monsters[idx] = prepareMonster(saveData.monsters[idx]); } for (const idx in saveData.player.monsters) { saveData.player.monsters[idx] = prepareMonster(saveData.player.monsters[idx]); } for (const idx in saveData.opponent.monsters) { saveData.opponent.monsters[idx] = prepareMonster(saveData.opponent.monsters[idx]); } return JSON.parse(JSON.stringify(saveData)); }; const saveMonster = (monsterData, monsterState) => { monsterData.level = monsterState.level; monsterData.hp = monsterState.hp; return monsterData; }; /** * @type {State} */ const saveData = prepareSaveData(Object.assign({}, Memory.state)); // monsters for (const idx in saveData.monsters) { saveData.monsters[idx] = saveMonster(saveData.monsters[idx], Memory.state.monsters[idx]); } // player - monsters for (const idx in saveData.player.monsters) { saveData.player.monsters[idx] = saveMonster(saveData.player.monsters[idx], Memory.state.player.monsters[idx]); } saveData.player.activeMonsterIdx = Memory.state.player.monsters.indexOf(Memory.state.player.activeMonster); // opponent - monsters for (const idx in saveData.opponent.monsters) { saveData.opponent.monsters[idx] = saveMonster(saveData.opponent.monsters[idx], Memory.state.opponent.monsters[idx]); } saveData.opponent.activeMonsterIdx = Memory.state.opponent.monsters.indexOf(Memory.state.opponent.activeMonster); return saveData; }, /** * @returns {string} */ saveToString () { // hash return btoa(JSON.stringify(Memory.saveToObject())); }, saveToLocalStorage () { const lastWrite = new Date(localStorage.getItem('lastWrite')); if (Math.abs(new Date() - lastWrite) / (1000 * 60 * 60 * 24) >= 1) { localStorage.setItem(`state_${(new Date()).toLocaleDateString()}`, localStorage.getItem('state')); } localStorage.setItem('state', Memory.saveToString()); localStorage.setItem('lastWrite', new Date()); }, /** * @param {(Object|State)} saveData */ async loadFromObject (saveData) { /** * @param {Area} areaData */ const loadArea = async (areaData) => { const area = await fetchArea(areaData.slug); area.monsterProgress = areaData.monsterProgress; area.trainerProgress = areaData.trainerProgress; return area; }; /** * @param {Monster} monsterData */ const loadMonster = async (monsterData) => { const monster = await fetchMonster(monsterData.slug); monster.level = monsterData.level; monster.hp = monsterData.hp; monster.exp = monsterData.exp; monster.tasteWarm = monsterData.tasteWarm; monster.tasteCold = monsterData.tasteCold; monster.gender = monsterData.gender; monster.heldItem = await loadItem(monsterData.heldItem); monster.statusEffect = await loadStatusEffect(monsterData.statusEffect); monster.statModifiers = monsterData.statModifiers; monster.experienceModifier = monsterData.experienceModifier; monster.moneyModifier = monsterData.moneyModifier; monster.activeTechniques = await Promise.all(monsterData.activeTechniques.map(async (technique) => { return loadTechnique(technique); })); return monster; }; /** * @param {InventoryItem} inventoryItemData */ const loadInventoryItem = async (inventoryItemData) => { const inventoryItem = new InventoryItem(await fetchItem(inventoryItemData.item.slug)); inventoryItem.quantity = inventoryItemData.quantity; return inventoryItem; }; /** * @param {Item} itemData */ const loadItem = async (itemData) => { if (!itemData) { return null; } const item = await fetchItem(itemData.slug); return item; }; /** * @param {StatusEffect} statusEffectData */ const loadStatusEffect = async (statusEffectData) => { if (!statusEffectData) { return null; } const statusEffect = await fetchStatusEffect(statusEffectData.slug); statusEffect.turnsLeft = statusEffectData.turnsLeft; return statusEffect; }; /** * @param {Trainer} trainerData */ const loadTrainer = async (trainerData) => { const trainer = new Trainer(trainerData); return trainer; }; /** * @param {Technique} techniqueData */ const loadTechnique = async (techniqueData) => { const technique = await fetchTechnique(techniqueData.slug); technique.turnLastUse = techniqueData.turnLastUse; return technique; }; /** * @type {State} */ const loadedState = saveData; const loadActions = [ async () => Memory.state.Settings.name = loadedState.Settings.name, async () => Memory.state.Settings.language = loadedState.Settings.language, async () => { await fetchTranslation(Memory.state.Settings.language); applyTranslation(); }, async () => Memory.state.Settings.currency = loadedState.Settings.currency, async () => Memory.state.Settings.logMaxLength = loadedState.Settings.logMaxLength, async () => { if (loadedState.Game) { // backwards compat: TODO: remove check later Memory.state.Game.isInBattle = loadedState.Game.isInBattle; } }, ...Object.keys(loadedState.areaProgress).map((areaSlug) => async () => { const areaData = loadedState.areaProgress[areaSlug]; Memory.state.areaProgress[areaSlug] = await loadArea(areaData); }), async () => Memory.state.currentArea = await loadArea(loadedState.currentArea), async () => Memory.state.lastVisitedTown = loadedState.lastVisitedTown, async () => Memory.state.storyProgress = loadedState.storyProgress || {}, // backwards compat: TODO: remove check later async () => Memory.state.currentStory = loadedState.currentStory || '', // backwards compat: TODO: remove check later async () => Memory.state.turn = loadedState.turn, async () => Memory.state.money = loadedState.money, async () => Memory.state.monsters = [], ...loadedState.monsters.map((monsterData) => async () => Memory.state.monsters.push(await loadMonster(monsterData))), async () => Memory.state.player = await loadTrainer(loadedState.player), ...loadedState.player.monsters.map((monsterData) => async () => Memory.state.player.monsters.push(await loadMonster(monsterData))), ...loadedState.player.inventory.map((itemData) => async () => Memory.state.player.inventory.push(await loadInventoryItem(itemData))), async () => Memory.state.player.activeMonster = Memory.state.player.monsters[loadedState.player.activeMonsterIdx], async () => Memory.state.opponent = await loadTrainer(loadedState.opponent), ...loadedState.opponent.monsters.map((monsterData) => async () => Memory.state.opponent.monsters.push(await loadMonster(monsterData))), ...loadedState.opponent.inventory.map((itemData) => async () => Memory.state.opponent.inventory.push(await loadInventoryItem(itemData))), async () => Memory.state.opponent.activeMonster = Memory.state.opponent.monsters[loadedState.opponent.activeMonsterIdx], async () => Memory.state.rivalMonster = loadedState.rivalMonster, async () => Memory.state.activeTechnique = await loadTechnique(loadedState.activeTechnique), async () => { if (typeof loadedState.activeBall === 'string') { // backwards compat: TODO: remove check later Memory.state.activeBall = loadedState.activeBall; } else { Memory.state.activeBall = loadedState.activeBall && loadedState.activeBall.slug; } }, // draw game async () => { if (!Game.isTown(Memory.state.currentArea)) { UI.drawOpponentMonster(); UI.drawActiveMonster(); UI.drawActiveTechniques(); } UI.drawArea(); UI.drawStatus(); }, async () => Story.progress(Memory.state.currentStory), ]; await UI.showLoading(loadActions); }, /** * @param {string} saveData */ loadFromString (saveData) { Memory.loadFromObject(JSON.parse(atob(saveData))); }, loadFromLocalStorage () { Memory.loadFromString(localStorage.getItem('state')); }, }; async function initializeState () { Memory.state.currentArea = await fetchArea('paper-town'); Memory.state.player = new Trainer({ monsters: [] }); Memory.state.opponent = new Trainer({ monsters: [] }); Memory.state.activeTechnique = await fetchTechnique('all_in'); }