From df499e2f71ebd70bf3bbef6849f9e02d847b293f Mon Sep 17 00:00:00 2001 From: Nexllon <75459029+Nexllon@users.noreply.github.com> Date: Tue, 14 May 2024 16:42:30 -0300 Subject: [PATCH 01/15] offline mode - adds daily run and fixes clear freezes (#834) * offline mode - adds daily run and fixes clear freezes * removed unused import --- src/data/daily-run.ts | 5 +++-- src/phases.ts | 29 ++++++++++++++++++++++++----- src/system/game-data.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index 5371b87a6..c1a1206f3 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -13,14 +13,15 @@ export interface DailyRunConfig { } export function fetchDailyRunSeed(): Promise { - return new Promise(resolve => { + return new Promise((resolve, reject) => { Utils.apiFetch('daily/seed').then(response => { if (!response.ok) { resolve(null); return; } return response.text(); - }).then(seed => resolve(seed)); + }).then(seed => resolve(seed)) + .catch(err => reject(err)); }); } diff --git a/src/phases.ts b/src/phases.ts index 8aa179d35..123169021 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -289,7 +289,7 @@ export class TitlePhase extends Phase { } this.scene.sessionSlotId = slotId; - fetchDailyRunSeed().then(seed => { + const generateDaily = (seed: string) => { this.scene.gameMode = gameModes[GameModes.DAILY]; this.scene.setSeed(seed); @@ -332,9 +332,18 @@ export class TitlePhase extends Phase { this.scene.sessionPlayTime = 0; this.end(); }); - }).catch(err => { - console.error("Failed to load daily run:\n", err); - }); + }; + + // If Online, calls seed fetch from db to generate daily run. If Offline, generates a daily run based on current date. + if (!Utils.isLocal) { + fetchDailyRunSeed().then(seed => { + generateDaily(seed); + }).catch(err => { + console.error("Failed to load daily run:\n", err); + }); + } else { + generateDaily(btoa(new Date().toISOString().substring(0, 10))); + } }); } @@ -3612,10 +3621,20 @@ export class GameOverPhase extends BattlePhase { }); }); }; + + /* Added a local check to see if the game is running offline on victory + If Online, execute apiFetch as intended + If Offline, execute offlineNewClear(), a localStorage implementation of newClear daily run checks */ if (this.victory) { - Utils.apiFetch(`savedata/newclear?slot=${this.scene.sessionSlotId}`, true) + if (!Utils.isLocal) { + Utils.apiFetch(`savedata/newclear?slot=${this.scene.sessionSlotId}`, true) .then(response => response.json()) .then(newClear => doGameOver(newClear)); + } else { + this.scene.gameData.offlineNewClear(this.scene).then(result => { + doGameOver(result); + }); + } } else doGameOver(false); } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 7f6e1475b..5e942b4fa 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -759,6 +759,36 @@ export class GameData { }); } + /* Defines a localStorage item 'daily' to check on clears, offline implementation of savedata/newclear API + If a GameModes clear other than Daily is checked, newClear = true as usual + If a Daily mode is cleared, checks if it was already cleared before, based on seed, and returns true only to new daily clear runs */ + offlineNewClear(scene: BattleScene): Promise { + return new Promise(resolve => { + const sessionData = this.getSessionSaveData(scene); + const seed = sessionData.seed; + let daily: string[] = []; + + if (sessionData.gameMode == GameModes.DAILY) { + if (localStorage.hasOwnProperty('daily')) { + daily = JSON.parse(atob(localStorage.getItem('daily'))); + if (daily.includes(seed)) { + return resolve(false); + } else { + daily.push(seed); + localStorage.setItem('daily', btoa(JSON.stringify(daily))); + return resolve(true); + } + } else { + daily.push(seed); + localStorage.setItem('daily', btoa(JSON.stringify(daily))); + return resolve(true); + } + } else { + return resolve(true); + } + }); + } + tryClearSession(scene: BattleScene, slotId: integer): Promise<[success: boolean, newClear: boolean]> { return new Promise<[boolean, boolean]>(resolve => { if (bypassLogin) { From 06ec265bcd4f94157741eef24a86e76c9d3d24b1 Mon Sep 17 00:00:00 2001 From: karl-police Date: Tue, 14 May 2024 21:42:11 +0200 Subject: [PATCH 02/15] battle.ts Translation Consistency and fixes --- src/locales/de/battle.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/locales/de/battle.ts b/src/locales/de/battle.ts index bff8c688d..ca0e60bf9 100644 --- a/src/locales/de/battle.ts +++ b/src/locales/de/battle.ts @@ -9,11 +9,11 @@ export const battle: SimpleTranslationEntries = { "trainerComeBack": "{{trainerName}} ruft {{pokemonName}} zurück!", "playerGo": "Los! {{pokemonName}}!", "trainerGo": "{{trainerName}} sendet {{pokemonName}} raus!", - "switchQuestion": "Willst du\n{{pokemonName}} auswechseln?", + "switchQuestion": "Möchtest du\n{{pokemonName}} auswechseln?", "trainerDefeated": `{{trainerName}}\nwurde besiegt!`, "pokemonCaught": "{{pokemonName}} wurde gefangen!", "pokemon": "Pokémon", - "sendOutPokemon": "Los! {{pokemonName}}!", + "sendOutPokemon": "Los, {{pokemonName}}!", "hitResultCriticalHit": "Ein Volltreffer!", "hitResultSuperEffective": "Das ist sehr effektiv!", "hitResultNotVeryEffective": "Das ist nicht sehr effektiv…", @@ -26,28 +26,28 @@ export const battle: SimpleTranslationEntries = { "learnMove": "{{pokemonName}} erlernt\n{{moveName}}!", "learnMovePrompt": "{{pokemonName}} versucht, {{moveName}} zu erlernen.", "learnMoveLimitReached": "Aber {{pokemonName}} kann nur\nmaximal vier Attacken erlernen.", - "learnMoveReplaceQuestion": "Soll eine andere Attacke durch\n{{moveName}} ersetzt werden?", + "learnMoveReplaceQuestion": "Soll eine bekannte Attacke durch\n{{moveName}} ersetzt werden?", "learnMoveStopTeaching": "{{moveName}} nicht\nerlernen?", "learnMoveNotLearned": "{{pokemonName}} hat\n{{moveName}} nicht erlernt.", "learnMoveForgetQuestion": "Welche Attacke soll vergessen werden?", "learnMoveForgetSuccess": "{{pokemonName}} hat\n{{moveName}} vergessen.", "levelCapUp": "Das Levellimit\nhat sich zu {{levelCap}} erhöht!", "moveNotImplemented": "{{moveName}} ist noch nicht implementiert und kann nicht ausgewählt werden.", - "moveNoPP": "Du hast keine AP für\ndiese Attacke mehr übrig!", + "moveNoPP": "Es sind keine AP für\ndiese Attacke mehr übrig!", "moveDisabled": "{{moveName}} ist deaktiviert!", "noPokeballForce": "Eine unsichtbare Kraft\nverhindert die Nutzung von Pokébällen.", "noPokeballTrainer": "Du kannst das Pokémon\neines anderen Trainers nicht fangen!", - "noPokeballMulti": "Du kannst erst einen Pokéball werden,\nwenn nur noch ein Pokémon übrig ist!", + "noPokeballMulti": "Du kannst erst einen Pokéball werfen,\nwenn nur noch ein Pokémon übrig ist!", "noPokeballStrong": "Das Ziel-Pokémon ist zu stark, um gefangen zu werden!\nDu musst es zuerst schwächen!", "noEscapeForce": "Eine unsichtbare Kraft\nverhindert die Flucht.", "noEscapeTrainer": "Du kannst nicht\naus einem Trainerkampf fliehen!", "noEscapePokemon": "{{pokemonName}}'s {{moveName}}\nverhindert {{escapeVerb}}!", "runAwaySuccess": "Du bist entkommen!", - "runAwayCannotEscape": 'Du kannst nicht fliehen!', + "runAwayCannotEscape": 'Flucht unmöglich!', "escapeVerbSwitch": "auswechseln", "escapeVerbFlee": "flucht", "skipItemQuestion": "Bist du sicher, dass du kein Item nehmen willst?", "notDisabled": "{{pokemonName}}'s {{moveName}} ist\nnicht mehr deaktiviert!", "eggHatching": "Oh?", "ivScannerUseQuestion": "IV-Scanner auf {{pokemonName}} benutzen?" -} as const; \ No newline at end of file +} as const; From f505c7f5fd62629d3e0219ad8415f5d90b523c5d Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Tue, 14 May 2024 16:12:31 -0400 Subject: [PATCH 03/15] Fix issues with formatLargeNumber --- src/utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 90dfe0a21..142de91c1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -192,7 +192,9 @@ export function formatLargeNumber(count: integer, threshold: integer): string { return '?'; } const digits = ((ret.length + 2) % 3) + 1; - const decimalNumber = parseInt(ret.slice(digits, digits + (3 - digits))); + let decimalNumber = ret.slice(digits, digits + 2); + while (decimalNumber.endsWith('0')) + decimalNumber = decimalNumber.slice(0, -1); return `${ret.slice(0, digits)}${decimalNumber ? `.${decimalNumber}` : ''}${suffix}`; } From aeeebcbf383f69d6db952480658a263142f44df2 Mon Sep 17 00:00:00 2001 From: cimds <34217078+cimds@users.noreply.github.com> Date: Tue, 14 May 2024 18:41:26 -0400 Subject: [PATCH 04/15] Update biomes.ts: Cave's Beach link changed to Lake Replacing Cave's Beach link with Lake would weaken the "water loop", a situation where players get continually routed back to Beach, leading to frustration. Lake is a crossroads biome that connects to much of the rest of the game, while still connecting to Beach, making it not a drastic detour. Infernal Vulpix simulated a hypothetical 10k wave endless run for me to see the difference this change would have on biome frequencies. Apologies for the broken formatting from me copying this from Discord: Odds Alt Abyss 1.98% 2.40% Badlands 3.57% 3.46% Beach 6.30% 4.06% Cave 5.87% 5.91% Construction Site 3.90% 4.83% Desert 1.70% 1.70% Dojo 2.05% 2.58% Factory 2.03% 2.57% Fairy Cave 0.86% 0.59% Forest 5.79% 5.64% Grass 2.12% 2.48% Graveyard 2.03% 2.39% Ice Cave 4.00% 2.68% Island 0.79% 0.53% Jungle 2.80% 2.72% Laboratory 0.13% 0.13% Lake 3.37% 6.26% Meadow 2.93% 2.74% Metropolis 2.33% 2.55% Mountain 2.86% 2.40% Plains 5.96% 6.94% Power Plant 2.06% 2.63% Ruins 2.51% 2.59% Sea 6.16% 4.07% Seabed 2.90% 2.04% Slum 2.58% 2.67% Snowy Forest 3.79% 2.60% Space 0.37% 0.41% Swamp 3.79% 4.64% Tall Grass 4.18% 4.73% Temple 2.98% 3.06% Town 0.00% 0.00% Volcano 2.58% 2.29% Wasteland 0.72% 0.72% End 2.00% 2.00% --- src/data/biomes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/biomes.ts b/src/data/biomes.ts index 61da0410d..db13158fa 100644 --- a/src/data/biomes.ts +++ b/src/data/biomes.ts @@ -48,7 +48,7 @@ export const biomeLinks: BiomeLinks = { [Biome.SEABED]: [ Biome.CAVE, [ Biome.VOLCANO, 4 ] ], [Biome.MOUNTAIN]: [ Biome.VOLCANO, [ Biome.WASTELAND, 3 ] ], [Biome.BADLANDS]: [ Biome.DESERT, Biome.MOUNTAIN ], - [Biome.CAVE]: [ Biome.BADLANDS, Biome.BEACH ], + [Biome.CAVE]: [ Biome.BADLANDS, Biome.LAKE ], [Biome.DESERT]: Biome.RUINS, [Biome.ICE_CAVE]: Biome.SNOWY_FOREST, [Biome.MEADOW]: [ Biome.PLAINS, [ Biome.FAIRY_CAVE, 2 ] ], @@ -7871,4 +7871,4 @@ export const biomeTrainerPools: BiomeTrainerPools = { } console.log(JSON.stringify(pokemonBiomes, null, ' '));*/ -} \ No newline at end of file +} From 6bed21770d0aac1943c98a5705f32710aecd38eb Mon Sep 17 00:00:00 2001 From: karl-police Date: Wed, 15 May 2024 03:36:49 +0200 Subject: [PATCH 05/15] Recoil (#863) --- src/locales/de/ability-trigger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/locales/de/ability-trigger.ts b/src/locales/de/ability-trigger.ts index 889007412..27d2053b6 100644 --- a/src/locales/de/ability-trigger.ts +++ b/src/locales/de/ability-trigger.ts @@ -1,5 +1,5 @@ import { SimpleTranslationEntries } from "#app/plugins/i18n"; export const abilityTriggers: SimpleTranslationEntries = { - 'blockRecoilDamage' : `{{pokemonName}}'s {{abilityName}}\nprotected it from recoil!`, -} as const; \ No newline at end of file + 'blockRecoilDamage' : `{{pokemonName}} wurde durch {{abilityName}}\nvor Rückstoß geschützt!`, +} as const; From 835b00d457ef4b160d46cf67fd407ae9635d1433 Mon Sep 17 00:00:00 2001 From: Benjamin Odom Date: Tue, 14 May 2024 23:19:12 -0500 Subject: [PATCH 06/15] Added Comments to Base Classes (#860) --- src/data/move.ts | 88 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 14 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 3c99ad8c1..c89277c55 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -434,29 +434,66 @@ export class SelfStatusMove extends Move { } } +/** + * Base class defining all {@link Move} Attributes + * @abstract + */ export abstract class MoveAttr { + /** Should this {@link Move} target the user? */ public selfTarget: boolean; constructor(selfTarget: boolean = false) { this.selfTarget = selfTarget; } + /** + * Applies move attributes + * @see {@link applyMoveAttrsInternal} + * @virtual + * @param user The {@link Pokemon} using the move + * @param target The target {@link Pokemon} of the move + * @param move The {@link Move} being used + * @param args Set of unique arguments needed by this attribute + * @returns true if the application succeeds + */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise { return true; } + /** + * @virtual + * @returns the {@link MoveCondition} or {@link MoveConditionFunc} for this {@link Move} + */ getCondition(): MoveCondition | MoveConditionFunc { return null; } + /** + * @virtual + * @param user The {@link Pokemon} using the move + * @param target The target {@link Pokemon} of the move + * @param move The {@link Move} being used + * @param cancelled A {@link Utils.BooleanHolder} which stores if the move should fail + * @returns the string representing failure of this {@link Move} + */ getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { return null; } + /** + * Used by the Enemy AI to rank an attack based on a given user + * @see {@link EnemyPokemon.getNextMove} + * @virtual + */ getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { return 0; } + /** + * Used by the Enemy AI to rank an attack based on a given target + * @see {@link EnemyPokemon.getNextMove} + * @virtual + */ getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { return 0; } @@ -470,6 +507,9 @@ export enum MoveEffectTrigger { POST_TARGET, } +/** Base class defining all Move Effect Attributes + * @extends MoveAttr + */ export class MoveEffectAttr extends MoveAttr { public trigger: MoveEffectTrigger; public firstHitOnly: boolean; @@ -480,11 +520,21 @@ export class MoveEffectAttr extends MoveAttr { this.firstHitOnly = firstHitOnly; } + /** + * Determines whether the {@link Move}'s effects are valid to {@link apply} + * @virtual + * @param user The {@link Pokemon} using the move + * @param target The target {@link Pokemon} of the move + * @param move The {@link Move} being used + * @param args Set of unique arguments needed by this attribute + * @returns true if the application succeeds + */ canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) { return !!(this.selfTarget ? user.hp && !user.getTag(BattlerTagType.FRENZY) : target.hp) && (this.selfTarget || !target.getTag(BattlerTagType.PROTECTED) || move.hasFlag(MoveFlags.IGNORE_PROTECT)); } + /** Applies move effects so long as they are able based on {@link canApply} */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise { return this.canApply(user, target, move, args); } @@ -806,8 +856,15 @@ export enum MultiHitType { _1_TO_10, } +/** + * Heals the user or target by {@link healRatio} depending on the value of {@link selfTarget} + * @extends MoveEffectAttr + * @see {@link apply} + */ export class HealAttr extends MoveEffectAttr { + /** The percentage of {@link Stat.HP} to heal */ private healRatio: number; + /** Should an animation be shown? */ private showAnim: boolean; constructor(healRatio?: number, showAnim?: boolean, selfTarget?: boolean) { @@ -822,6 +879,10 @@ export class HealAttr extends MoveEffectAttr { return true; } + /** + * Creates a new {@link PokemonHealPhase}. + * This heals the target and shows the appropriate message. + */ addHealPhase(target: Pokemon, healRatio: number) { target.scene.unshiftPhase(new PokemonHealPhase(target.scene, target.getBattlerIndex(), Math.max(Math.floor(target.getMaxHp() * healRatio), 1), getPokemonMessage(target, ' \nhad its HP restored.'), true, !this.showAnim)); @@ -903,7 +964,7 @@ export class IgnoreWeatherTypeDebuffAttr extends MoveAttr { * @param user Pokemon that used the move * @param target N/A * @param move Move with this attribute - * @param args Utils.NumberHolder for arenaAttackTypeMultiplier + * @param args [0] Utils.NumberHolder for arenaAttackTypeMultiplier * @returns true if the function succeeds */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -963,27 +1024,19 @@ export class SandHealAttr extends WeatherHealAttr { } /** - * Heals the target by either {@link normalHealRatio} or {@link boostedHealRatio} + * Heals the target or the user by either {@link normalHealRatio} or {@link boostedHealRatio} * depending on the evaluation of {@link condition} + * @extends HealAttr * @see {@link apply} - * @param user The Pokemon using this move - * @param target The target Pokemon of this move - * @param move This move - * @param args N/A - * @returns if the move was successful */ export class BoostHealAttr extends HealAttr { + /** Healing received when {@link condition} is false */ private normalHealRatio?: number; + /** Healing received when {@link condition} is true */ private boostedHealRatio?: number; + /** The lambda expression to check against when boosting the healing value */ private condition?: MoveConditionFunc; - /** - * @param normalHealRatio Healing received when {@link condition} is false - * @param boostedHealRatio Healing received when {@link condition} is true - * @param showAnim Should a healing animation be showed? - * @param selfTarget Should the move target the user? - * @param condition The condition to check against when boosting the healing value - */ constructor(normalHealRatio?: number, boostedHealRatio?: number, showAnim?: boolean, selfTarget?: boolean, condition?: MoveConditionFunc) { super(normalHealRatio, showAnim, selfTarget); this.normalHealRatio = normalHealRatio; @@ -991,6 +1044,13 @@ export class BoostHealAttr extends HealAttr { this.condition = condition; } + /** + * @param user The Pokemon using this move + * @param target The target Pokemon of this move + * @param move This move + * @param args N/A + * @returns true if the move was successful + */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const healRatio = this.condition(user, target, move) ? this.boostedHealRatio : this.normalHealRatio; this.addHealPhase(target, healRatio); From 67c18a15e2c0f9bec28646d8dadb01ac2a478c21 Mon Sep 17 00:00:00 2001 From: Samuel H Date: Wed, 15 May 2024 00:52:06 -0400 Subject: [PATCH 07/15] Implement client session and re-implement hybrid saving (#888) * Implement client session and re-implement hybrid saving (WiP) * Fixes for hybrid saving * Add local data clears where applicable * Include client session ID in system update * Change save threshold from 5 waves to 10 waves or 5 minutes --- src/account.ts | 3 +- src/battle-scene.ts | 5 + src/phases.ts | 16 +- src/system/game-data.ts | 399 +++++++++++++++++---------------- src/ui/egg-gacha-ui-handler.ts | 2 +- 5 files changed, 231 insertions(+), 194 deletions(-) diff --git a/src/account.ts b/src/account.ts index afb9aca35..0d79f4d55 100644 --- a/src/account.ts +++ b/src/account.ts @@ -7,13 +7,14 @@ export interface UserInfo { } export let loggedInUser: UserInfo = null; +export const clientSessionId = Utils.randomString(32); export function updateUserInfo(): Promise<[boolean, integer]> { return new Promise<[boolean, integer]>(resolve => { if (bypassLogin) { let lastSessionSlot = -1; for (let s = 0; s < 2; s++) { - if (localStorage.getItem(`sessionData${s ? s : ''}`)) { + if (localStorage.getItem(`sessionData${s ? s : ''}_${loggedInUser.username}`)) { lastSessionSlot = s; break; } diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 111e5439b..77c3b7ad4 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -88,6 +88,7 @@ export default class BattleScene extends SceneBase { public uiInputs: UiInputs; public sessionPlayTime: integer = null; + public lastSavePlayTime: integer = null; public masterVolume: number = 0.5; public bgmVolume: number = 1; public seVolume: number = 1; @@ -452,6 +453,8 @@ export default class BattleScene extends SceneBase { initSession(): void { if (this.sessionPlayTime === null) this.sessionPlayTime = 0; + if (this.lastSavePlayTime === null) + this.lastSavePlayTime = 0; if (this.playTimeTimer) this.playTimeTimer.destroy(); @@ -464,6 +467,8 @@ export default class BattleScene extends SceneBase { this.gameData.gameStats.playTime++; if (this.sessionPlayTime !== null) this.sessionPlayTime++; + if (this.lastSavePlayTime !== null) + this.lastSavePlayTime++; } }); diff --git a/src/phases.ts b/src/phases.ts index 123169021..a5439c228 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -330,6 +330,7 @@ export class TitlePhase extends Phase { this.scene.newBattle(); this.scene.arena.init(); this.scene.sessionPlayTime = 0; + this.scene.lastSavePlayTime = 0; this.end(); }); }; @@ -393,8 +394,12 @@ export class UnavailablePhase extends Phase { } export class ReloadSessionPhase extends Phase { - constructor(scene: BattleScene) { + private systemDataStr: string; + + constructor(scene: BattleScene, systemDataStr?: string) { super(scene); + + this.systemDataStr = systemDataStr; } start(): void { @@ -410,7 +415,9 @@ export class ReloadSessionPhase extends Phase { delayElapsed = true; }); - this.scene.gameData.loadSystem().then(() => { + this.scene.gameData.clearLocalData(); + + (this.systemDataStr ? this.scene.gameData.initSystem(this.systemDataStr) : this.scene.gameData.loadSystem()).then(() => { if (delayElapsed) this.end(); else @@ -531,6 +538,7 @@ export class SelectStarterPhase extends Phase { this.scene.newBattle(); this.scene.arena.init(); this.scene.sessionPlayTime = 0; + this.scene.lastSavePlayTime = 0; this.end(); }); }); @@ -784,7 +792,7 @@ export class EncounterPhase extends BattlePhase { this.scene.ui.setMode(Mode.MESSAGE).then(() => { if (!this.loaded) { - this.scene.gameData.saveAll(this.scene, true).then(success => { + this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || this.scene.lastSavePlayTime >= 10).then(success => { this.scene.disableMenu = false; if (!success) return this.scene.reset(true); @@ -3692,7 +3700,7 @@ export class PostGameOverPhase extends Phase { start() { super.start(); - this.scene.gameData.saveSystem().then(success => { + this.scene.gameData.saveAll(this.scene, true, true, true).then(success => { if (!success) return this.scene.reset(true); this.scene.gameData.tryClearSession(this.scene, this.scene.sessionSlotId).then((success: boolean | [boolean, boolean]) => { diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 5e942b4fa..e7537fc52 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -20,7 +20,7 @@ import { Egg } from "../data/egg"; import { VoucherType, vouchers } from "./voucher"; import { AES, enc } from "crypto-js"; import { Mode } from "../ui/ui"; -import { loggedInUser, updateUserInfo } from "../account"; +import { clientSessionId, loggedInUser, updateUserInfo } from "../account"; import { Nature } from "../data/nature"; import { GameStats } from "./game-stats"; import { Tutorial } from "../tutorial"; @@ -67,6 +67,18 @@ export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0): str } } +function encrypt(data: string, bypassLogin: boolean): string { + return (bypassLogin + ? (data: string) => btoa(data) + : (data: string) => AES.encrypt(data, saveKey))(data); +} + +function decrypt(data: string, bypassLogin: boolean): string { + return (bypassLogin + ? (data: string) => atob(data) + : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8))(data); +} + interface SystemSaveData { trainerId: integer; secretId: integer; @@ -277,8 +289,10 @@ export class GameData { const maxIntAttrValue = Math.pow(2, 31); const systemData = JSON.stringify(data, (k: any, v: any) => typeof v === 'bigint' ? v <= maxIntAttrValue ? Number(v) : v.toString() : v); + localStorage.setItem(`data_${loggedInUser.username}`, encrypt(systemData, bypassLogin)); + if (!bypassLogin) { - Utils.apiPost(`savedata/update?datatype=${GameDataType.SYSTEM}`, systemData, undefined, true) + Utils.apiPost(`savedata/update?datatype=${GameDataType.SYSTEM}&clientSessionId=${clientSessionId}`, systemData, undefined, true) .then(response => response.text()) .then(error => { this.scene.ui.savingIcon.hide(); @@ -296,10 +310,6 @@ export class GameData { resolve(true); }); } else { - localStorage.setItem('data_bak', localStorage.getItem('data')); - - localStorage.setItem('data', btoa(systemData)); - this.scene.ui.savingIcon.hide(); resolve(true); @@ -309,120 +319,13 @@ export class GameData { public loadSystem(): Promise { return new Promise(resolve => { - if (bypassLogin && !localStorage.hasOwnProperty('data')) + console.log('Client Session:', clientSessionId); + + if (bypassLogin && !localStorage.getItem(`data_${loggedInUser.username}`)) return resolve(false); - const handleSystemData = (systemDataStr: string) => { - try { - const systemData = this.parseSystemData(systemDataStr); - - console.debug(systemData); - - /*const versions = [ this.scene.game.config.gameVersion, data.gameVersion || '0.0.0' ]; - - if (versions[0] !== versions[1]) { - const [ versionNumbers, oldVersionNumbers ] = versions.map(ver => ver.split('.').map(v => parseInt(v))); - }*/ - - this.trainerId = systemData.trainerId; - this.secretId = systemData.secretId; - - this.gender = systemData.gender; - - this.saveSetting(Setting.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); - - const initStarterData = !systemData.starterData; - - if (initStarterData) { - this.initStarterData(); - - if (systemData['starterMoveData']) { - const starterMoveData = systemData['starterMoveData']; - for (let s of Object.keys(starterMoveData)) - this.starterData[s].moveset = starterMoveData[s]; - } - - if (systemData['starterEggMoveData']) { - const starterEggMoveData = systemData['starterEggMoveData']; - for (let s of Object.keys(starterEggMoveData)) - this.starterData[s].eggMoves = starterEggMoveData[s]; - } - - this.migrateStarterAbilities(systemData, this.starterData); - } else { - if ([ '1.0.0', '1.0.1' ].includes(systemData.gameVersion)) - this.migrateStarterAbilities(systemData); - //this.fixVariantData(systemData); - this.fixStarterData(systemData); - // Migrate ability starter data if empty for caught species - Object.keys(systemData.starterData).forEach(sd => { - if (systemData.dexData[sd].caughtAttr && !systemData.starterData[sd].abilityAttr) - systemData.starterData[sd].abilityAttr = 1; - }); - this.starterData = systemData.starterData; - } - - if (systemData.gameStats) { - if (systemData.gameStats.legendaryPokemonCaught !== undefined && systemData.gameStats.subLegendaryPokemonCaught === undefined) - this.fixLegendaryStats(systemData); - this.gameStats = systemData.gameStats; - } - - if (systemData.unlocks) { - for (let key of Object.keys(systemData.unlocks)) { - if (this.unlocks.hasOwnProperty(key)) - this.unlocks[key] = systemData.unlocks[key]; - } - } - - if (systemData.achvUnlocks) { - for (let a of Object.keys(systemData.achvUnlocks)) { - if (achvs.hasOwnProperty(a)) - this.achvUnlocks[a] = systemData.achvUnlocks[a]; - } - } - - if (systemData.voucherUnlocks) { - for (let v of Object.keys(systemData.voucherUnlocks)) { - if (vouchers.hasOwnProperty(v)) - this.voucherUnlocks[v] = systemData.voucherUnlocks[v]; - } - } - - if (systemData.voucherCounts) { - Utils.getEnumKeys(VoucherType).forEach(key => { - const index = VoucherType[key]; - this.voucherCounts[index] = systemData.voucherCounts[index] || 0; - }); - } - - this.eggs = systemData.eggs - ? systemData.eggs.map(e => e.toEgg()) - : []; - - this.dexData = Object.assign(this.dexData, systemData.dexData); - this.consolidateDexData(this.dexData); - this.defaultDexData = null; - - if (initStarterData) { - const starterIds = Object.keys(this.starterData).map(s => parseInt(s) as Species); - for (let s of starterIds) { - this.starterData[s].candyCount += this.dexData[s].caughtCount; - this.starterData[s].candyCount += this.dexData[s].hatchedCount * 2; - if (this.dexData[s].caughtAttr & DexAttr.SHINY) - this.starterData[s].candyCount += 4; - } - } - - resolve(true); - } catch (err) { - console.error(err); - resolve(false); - } - } - if (!bypassLogin) { - Utils.apiFetch(`savedata/get?datatype=${GameDataType.SYSTEM}`, true) + Utils.apiFetch(`savedata/system?clientSessionId=${clientSessionId}`, true) .then(response => response.text()) .then(response => { if (!response.length || response[0] !== '{') { @@ -437,10 +340,132 @@ export class GameData { return resolve(false); } - handleSystemData(response); + const cachedSystem = localStorage.getItem(`data_${loggedInUser.username}`); + this.initSystem(response, cachedSystem ? AES.decrypt(cachedSystem, saveKey).toString(enc.Utf8) : null).then(resolve); }); } else - handleSystemData(atob(localStorage.getItem('data'))); + this.initSystem(decrypt(localStorage.getItem(`data_${loggedInUser.username}`), bypassLogin)).then(resolve); + }); + } + + public initSystem(systemDataStr: string, cachedSystemDataStr?: string): Promise { + return new Promise(resolve => { + try { + let systemData = this.parseSystemData(systemDataStr); + + if (cachedSystemDataStr) { + let cachedSystemData = this.parseSystemData(cachedSystemDataStr); + console.log(cachedSystemData.timestamp, systemData.timestamp) + if (cachedSystemData.timestamp > systemData.timestamp) { + console.debug('Use cached system'); + systemData = cachedSystemData; + } else + this.clearLocalData(); + } + + console.debug(systemData); + + /*const versions = [ this.scene.game.config.gameVersion, data.gameVersion || '0.0.0' ]; + + if (versions[0] !== versions[1]) { + const [ versionNumbers, oldVersionNumbers ] = versions.map(ver => ver.split('.').map(v => parseInt(v))); + }*/ + + this.trainerId = systemData.trainerId; + this.secretId = systemData.secretId; + + this.gender = systemData.gender; + + this.saveSetting(Setting.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); + + const initStarterData = !systemData.starterData; + + if (initStarterData) { + this.initStarterData(); + + if (systemData['starterMoveData']) { + const starterMoveData = systemData['starterMoveData']; + for (let s of Object.keys(starterMoveData)) + this.starterData[s].moveset = starterMoveData[s]; + } + + if (systemData['starterEggMoveData']) { + const starterEggMoveData = systemData['starterEggMoveData']; + for (let s of Object.keys(starterEggMoveData)) + this.starterData[s].eggMoves = starterEggMoveData[s]; + } + + this.migrateStarterAbilities(systemData, this.starterData); + } else { + if ([ '1.0.0', '1.0.1' ].includes(systemData.gameVersion)) + this.migrateStarterAbilities(systemData); + //this.fixVariantData(systemData); + this.fixStarterData(systemData); + // Migrate ability starter data if empty for caught species + Object.keys(systemData.starterData).forEach(sd => { + if (systemData.dexData[sd].caughtAttr && !systemData.starterData[sd].abilityAttr) + systemData.starterData[sd].abilityAttr = 1; + }); + this.starterData = systemData.starterData; + } + + if (systemData.gameStats) { + if (systemData.gameStats.legendaryPokemonCaught !== undefined && systemData.gameStats.subLegendaryPokemonCaught === undefined) + this.fixLegendaryStats(systemData); + this.gameStats = systemData.gameStats; + } + + if (systemData.unlocks) { + for (let key of Object.keys(systemData.unlocks)) { + if (this.unlocks.hasOwnProperty(key)) + this.unlocks[key] = systemData.unlocks[key]; + } + } + + if (systemData.achvUnlocks) { + for (let a of Object.keys(systemData.achvUnlocks)) { + if (achvs.hasOwnProperty(a)) + this.achvUnlocks[a] = systemData.achvUnlocks[a]; + } + } + + if (systemData.voucherUnlocks) { + for (let v of Object.keys(systemData.voucherUnlocks)) { + if (vouchers.hasOwnProperty(v)) + this.voucherUnlocks[v] = systemData.voucherUnlocks[v]; + } + } + + if (systemData.voucherCounts) { + Utils.getEnumKeys(VoucherType).forEach(key => { + const index = VoucherType[key]; + this.voucherCounts[index] = systemData.voucherCounts[index] || 0; + }); + } + + this.eggs = systemData.eggs + ? systemData.eggs.map(e => e.toEgg()) + : []; + + this.dexData = Object.assign(this.dexData, systemData.dexData); + this.consolidateDexData(this.dexData); + this.defaultDexData = null; + + if (initStarterData) { + const starterIds = Object.keys(this.starterData).map(s => parseInt(s) as Species); + for (let s of starterIds) { + this.starterData[s].candyCount += this.dexData[s].caughtCount; + this.starterData[s].candyCount += this.dexData[s].hatchedCount * 2; + if (this.dexData[s].caughtAttr & DexAttr.SHINY) + this.starterData[s].candyCount += 4; + } + } + + resolve(true); + } catch (err) { + console.error(err); + resolve(false); + } }); } @@ -474,6 +499,29 @@ export class GameData { return dataStr; } + public async verify(): Promise { + if (bypassLogin) + return true; + + const response = await Utils.apiPost(`savedata/system/verify`, JSON.stringify({ clientSessionId: clientSessionId }), undefined, true) + .then(response => response.json()); + + if (!response.valid) { + this.scene.clearPhaseQueue(); + this.scene.unshiftPhase(new ReloadSessionPhase(this.scene, JSON.stringify(response.systemData))); + this.clearLocalData(); + return false; + } + + return true; + } + + public clearLocalData(): void { + localStorage.removeItem(`data_${loggedInUser.username}`); + for (let s = 0; s < 5; s++) + localStorage.removeItem(`sessionData${s ? s : ''}_${loggedInUser.username}`); + } + public saveSetting(setting: Setting, valueIndex: integer): boolean { let settings: object = {}; if (localStorage.hasOwnProperty('settings')) @@ -557,40 +605,6 @@ export class GameData { } as SessionSaveData; } - saveSession(scene: BattleScene, skipVerification?: boolean): Promise { - return new Promise(resolve => { - Utils.executeIf(!skipVerification, updateUserInfo).then(success => { - if (success !== null && !success) - return resolve(false); - - const sessionData = this.getSessionSaveData(scene); - - if (!bypassLogin) { - Utils.apiPost(`savedata/update?datatype=${GameDataType.SESSION}&slot=${scene.sessionSlotId}&trainerId=${this.trainerId}&secretId=${this.secretId}`, JSON.stringify(sessionData), undefined, true) - .then(response => response.text()) - .then(error => { - if (error) { - if (error.startsWith('session out of date')) { - this.scene.clearPhaseQueue(); - this.scene.unshiftPhase(new ReloadSessionPhase(this.scene)); - } - console.error(error); - return resolve(false); - } - console.debug('Session data saved'); - resolve(true); - }); - } else { - localStorage.setItem(`sessionData${scene.sessionSlotId ? scene.sessionSlotId : ''}`, btoa(JSON.stringify(sessionData))); - - console.debug('Session data saved'); - - resolve(true); - } - }); - }); - } - getSession(slotId: integer): Promise { return new Promise(async (resolve, reject) => { if (slotId < 0) @@ -605,8 +619,8 @@ export class GameData { } }; - if (!bypassLogin) { - Utils.apiFetch(`savedata/get?datatype=${GameDataType.SESSION}&slot=${slotId}`, true) + if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ''}_${loggedInUser.username}`)) { + Utils.apiFetch(`savedata/session?slot=${slotId}&clientSessionId=${clientSessionId}`, true) .then(response => response.text()) .then(async response => { if (!response.length || response[0] !== '{') { @@ -614,12 +628,14 @@ export class GameData { return resolve(null); } + localStorage.setItem(`sessionData${slotId ? slotId : ''}_${loggedInUser.username}`, encrypt(response, bypassLogin)); + await handleSessionData(response); }); } else { - const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ''}`); + const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ''}_${loggedInUser.username}`); if (sessionData) - await handleSessionData(atob(sessionData)); + await handleSessionData(decrypt(sessionData, bypassLogin)); else return resolve(null); } @@ -640,6 +656,7 @@ export class GameData { console.log('Seed:', scene.seed); scene.sessionPlayTime = sessionData.playTime || 0; + scene.lastSavePlayTime = 0; const loadPokemonAssets: Promise[] = []; @@ -731,16 +748,17 @@ export class GameData { deleteSession(slotId: integer): Promise { return new Promise(resolve => { if (bypassLogin) { - localStorage.removeItem('sessionData'); + localStorage.removeItem(`sessionData${this.scene.sessionSlotId ? this.scene.sessionSlotId : ''}_${loggedInUser.username}`); return resolve(true); } updateUserInfo().then(success => { if (success !== null && !success) return resolve(false); - Utils.apiFetch(`savedata/delete?datatype=${GameDataType.SESSION}&slot=${slotId}`, true).then(response => { + Utils.apiFetch(`savedata/delete?datatype=${GameDataType.SESSION}&slot=${slotId}&clientSessionId=${clientSessionId}`, true).then(response => { if (response.ok) { loggedInUser.lastSessionSlot = -1; + localStorage.removeItem(`sessionData${this.scene.sessionSlotId ? this.scene.sessionSlotId : ''}_${loggedInUser.username}`); resolve(true); } return response.text(); @@ -792,7 +810,7 @@ export class GameData { tryClearSession(scene: BattleScene, slotId: integer): Promise<[success: boolean, newClear: boolean]> { return new Promise<[boolean, boolean]>(resolve => { if (bypassLogin) { - localStorage.removeItem(`sessionData${slotId ? slotId : ''}`); + localStorage.removeItem(`sessionData${slotId ? slotId : ''}_${loggedInUser.username}`); return resolve([true, true]); } @@ -800,9 +818,11 @@ export class GameData { if (success !== null && !success) return resolve([false, false]); const sessionData = this.getSessionSaveData(scene); - Utils.apiPost(`savedata/clear?slot=${slotId}&trainerId=${this.trainerId}&secretId=${this.secretId}`, JSON.stringify(sessionData), undefined, true).then(response => { - if (response.ok) + Utils.apiPost(`savedata/clear?slot=${slotId}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`, JSON.stringify(sessionData), undefined, true).then(response => { + if (response.ok) { loggedInUser.lastSessionSlot = -1; + localStorage.removeItem(`sessionData${this.scene.sessionSlotId ? this.scene.sessionSlotId : ''}_${loggedInUser.username}`); + } return response.json(); }).then(jsonResponse => { if (!jsonResponse.error) @@ -855,14 +875,14 @@ export class GameData { }) as SessionSaveData; } - saveAll(scene: BattleScene, skipVerification?: boolean): Promise { + saveAll(scene: BattleScene, skipVerification: boolean = false, sync: boolean = false, useCachedSession: boolean = false): Promise { return new Promise(resolve => { Utils.executeIf(!skipVerification, updateUserInfo).then(success => { if (success !== null && !success) return resolve(false); - this.scene.ui.savingIcon.show(); - const data = this.getSystemSaveData(); - const sessionData = this.getSessionSaveData(scene); + if (sync) + this.scene.ui.savingIcon.show(); + const sessionData = useCachedSession ? this.parseSessionData(decrypt(localStorage.getItem(`sessionData${scene.sessionSlotId ? scene.sessionSlotId : ''}_${loggedInUser.username}`), bypassLogin)) : this.getSessionSaveData(scene); const maxIntAttrValue = Math.pow(2, 31); const systemData = this.getSystemSaveData(); @@ -870,14 +890,24 @@ export class GameData { const request = { system: systemData, session: sessionData, - sessionSlotId: scene.sessionSlotId + sessionSlotId: scene.sessionSlotId, + clientSessionId: clientSessionId }; - if (!bypassLogin) { + localStorage.setItem(`data_${loggedInUser.username}`, encrypt(JSON.stringify(systemData, (k: any, v: any) => typeof v === 'bigint' ? v <= maxIntAttrValue ? Number(v) : v.toString() : v), bypassLogin)); + + localStorage.setItem(`sessionData${scene.sessionSlotId ? scene.sessionSlotId : ''}_${loggedInUser.username}`, encrypt(JSON.stringify(sessionData), bypassLogin)); + + console.debug('Session data saved'); + + if (!bypassLogin && sync) { Utils.apiPost('savedata/updateall', JSON.stringify(request, (k: any, v: any) => typeof v === 'bigint' ? v <= maxIntAttrValue ? Number(v) : v.toString() : v), undefined, true) .then(response => response.text()) .then(error => { - this.scene.ui.savingIcon.hide(); + if (sync) { + this.scene.lastSavePlayTime = 0; + this.scene.ui.savingIcon.hide(); + } if (error) { if (error.startsWith('client version out of date')) { this.scene.clearPhaseQueue(); @@ -892,17 +922,10 @@ export class GameData { resolve(true); }); } else { - localStorage.setItem('data_bak', localStorage.getItem('data')); - - localStorage.setItem('data', btoa(JSON.stringify(systemData, (k: any, v: any) => typeof v === 'bigint' ? v <= maxIntAttrValue ? Number(v) : v.toString() : v))); - - localStorage.setItem(`sessionData${scene.sessionSlotId ? scene.sessionSlotId : ''}`, btoa(JSON.stringify(sessionData))); - - console.debug('Session data saved'); - - this.scene.ui.savingIcon.hide(); - - resolve(true); + this.verify().then(success => { + this.scene.ui.savingIcon.hide(); + resolve(success); + }); } }); }); @@ -910,7 +933,7 @@ export class GameData { public tryExportData(dataType: GameDataType, slotId: integer = 0): Promise { return new Promise(resolve => { - const dataKey: string = getDataTypeKey(dataType, slotId); + const dataKey: string = `${getDataTypeKey(dataType, slotId)}_${loggedInUser.username}`; const handleData = (dataStr: string) => { switch (dataType) { case GameDataType.SYSTEM: @@ -926,7 +949,7 @@ export class GameData { link.remove(); }; if (!bypassLogin && dataType < GameDataType.SETTINGS) { - Utils.apiFetch(`savedata/get?datatype=${dataType}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ''}`, true) + Utils.apiFetch(`savedata/${dataType === GameDataType.SYSTEM ? 'system' : 'session'}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ''}&clientSessionId=${clientSessionId}`, true) .then(response => response.text()) .then(response => { if (!response.length || response[0] !== '{') { @@ -941,14 +964,14 @@ export class GameData { } else { const data = localStorage.getItem(dataKey); if (data) - handleData(atob(data)); + handleData(decrypt(data, bypassLogin)); resolve(!!data); } }); } public importData(dataType: GameDataType, slotId: integer = 0): void { - const dataKey = getDataTypeKey(dataType, slotId); + const dataKey = `${getDataTypeKey(dataType, slotId)}_${loggedInUser.username}`; let saveFile: any = document.getElementById('saveFile'); if (saveFile) @@ -1009,11 +1032,13 @@ export class GameData { return this.scene.ui.showText(`Your ${dataName} data could not be loaded. It may be corrupted.`, null, () => this.scene.ui.showText(null, 0), Utils.fixedInt(1500)); this.scene.ui.showText(`Your ${dataName} data will be overridden and the page will reload. Proceed?`, null, () => { this.scene.ui.setOverlayMode(Mode.CONFIRM, () => { + localStorage.setItem(dataKey, encrypt(dataStr, bypassLogin)); + if (!bypassLogin && dataType < GameDataType.SETTINGS) { updateUserInfo().then(success => { if (!success) return displayError(`Could not contact the server. Your ${dataName} data could not be imported.`); - Utils.apiPost(`savedata/update?datatype=${dataType}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ''}&trainerId=${this.trainerId}&secretId=${this.secretId}`, dataStr, undefined, true) + Utils.apiPost(`savedata/update?datatype=${dataType}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ''}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`, dataStr, undefined, true) .then(response => response.text()) .then(error => { if (error) { @@ -1023,10 +1048,8 @@ export class GameData { window.location = window.location; }); }); - } else { - localStorage.setItem(dataKey, btoa(dataStr)); + } else window.location = window.location; - } }, () => { this.scene.ui.revertMode(); this.scene.ui.showText(null, 0); diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index 315dfdbd3..5626405da 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -372,7 +372,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.scene.gameData.gameStats.eggsPulled++; } - this.scene.gameData.saveSystem().then(success => { + this.scene.gameData.saveAll(this.scene, true, true, true).then(success => { if (!success) return this.scene.reset(true); doPull(); From c6973365cbb73e48dcb988cbfdd40d6f39a099bf Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Wed, 15 May 2024 00:56:17 -0400 Subject: [PATCH 08/15] Change last save interval to correct value --- src/phases.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/phases.ts b/src/phases.ts index a5439c228..322f93870 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -792,7 +792,7 @@ export class EncounterPhase extends BattlePhase { this.scene.ui.setMode(Mode.MESSAGE).then(() => { if (!this.loaded) { - this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || this.scene.lastSavePlayTime >= 10).then(success => { + this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || this.scene.lastSavePlayTime >= 300).then(success => { this.scene.disableMenu = false; if (!success) return this.scene.reset(true); From 7e0e4ecd6db57a02742373298ece3e4a17868c5a Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Wed, 15 May 2024 01:29:07 -0400 Subject: [PATCH 09/15] Call saveSystem on gacha pull outside of current battle --- src/ui/egg-gacha-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index 5626405da..a7fd61b30 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -372,7 +372,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.scene.gameData.gameStats.eggsPulled++; } - this.scene.gameData.saveAll(this.scene, true, true, true).then(success => { + (this.scene.currentBattle ? this.scene.gameData.saveAll(this.scene, true, true, true) : this.scene.gameData.saveSystem()).then(success => { if (!success) return this.scene.reset(true); doPull(); From 751120e77e88021d74ce6759640e9c825a752878 Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Wed, 15 May 2024 01:42:36 -0400 Subject: [PATCH 10/15] Fix crashing in offline mode --- src/account.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/account.ts b/src/account.ts index 0d79f4d55..357cd5af4 100644 --- a/src/account.ts +++ b/src/account.ts @@ -12,6 +12,7 @@ export const clientSessionId = Utils.randomString(32); export function updateUserInfo(): Promise<[boolean, integer]> { return new Promise<[boolean, integer]>(resolve => { if (bypassLogin) { + loggedInUser = { username: 'Guest', lastSessionSlot: -1 }; let lastSessionSlot = -1; for (let s = 0; s < 2; s++) { if (localStorage.getItem(`sessionData${s ? s : ''}_${loggedInUser.username}`)) { @@ -19,7 +20,7 @@ export function updateUserInfo(): Promise<[boolean, integer]> { break; } } - loggedInUser = { username: 'Guest', lastSessionSlot: lastSessionSlot }; + loggedInUser.lastSessionSlot = lastSessionSlot; return resolve([ true, 200 ]); } Utils.apiFetch('account/info', true).then(response => { From 70ecc51f505135aa1340e2997a19b490ece0854c Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Wed, 15 May 2024 01:54:15 -0400 Subject: [PATCH 11/15] Don't clear local data in offline mode --- src/system/game-data.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index e7537fc52..45d050263 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -517,6 +517,8 @@ export class GameData { } public clearLocalData(): void { + if (bypassLogin) + return; localStorage.removeItem(`data_${loggedInUser.username}`); for (let s = 0; s < 5; s++) localStorage.removeItem(`sessionData${s ? s : ''}_${loggedInUser.username}`); From 4a7da3e5f68dd99afdc898d83ae02010d9a85f5a Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Wed, 15 May 2024 02:01:30 -0400 Subject: [PATCH 12/15] Fix export not working --- src/system/game-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 45d050263..e7ffd281b 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -951,7 +951,7 @@ export class GameData { link.remove(); }; if (!bypassLogin && dataType < GameDataType.SETTINGS) { - Utils.apiFetch(`savedata/${dataType === GameDataType.SYSTEM ? 'system' : 'session'}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ''}&clientSessionId=${clientSessionId}`, true) + Utils.apiFetch(`savedata/${dataType === GameDataType.SYSTEM ? 'system' : 'session'}?clientSessionId=${clientSessionId}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ''}`, true) .then(response => response.text()) .then(response => { if (!response.length || response[0] !== '{') { From 1e224a6ac48ace2e4b64a2da341307c304289bca Mon Sep 17 00:00:00 2001 From: Madmadness65 Date: Wed, 15 May 2024 01:46:24 -0500 Subject: [PATCH 13/15] Allow Original Color Magearna to be obtained MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Another completely cosmetic Pokémon form (and a nice looking one at that!). --- src/battle-scene.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 77c3b7ad4..f15cf1a52 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1012,6 +1012,7 @@ export default class BattleScene extends SceneBase { case Species.FLORGES: case Species.FURFROU: case Species.ORICORIO: + case Species.MAGEARNA: case Species.SQUAWKABILLY: case Species.TATSUGIRI: case Species.PALDEA_TAUROS: From e89dbad5f1b0ac284a83fbe077f9e708ea850575 Mon Sep 17 00:00:00 2001 From: notpatchmaybe <104580041+notpatchmaybe@users.noreply.github.com> Date: Wed, 15 May 2024 09:56:06 +0100 Subject: [PATCH 14/15] Readded removed args, inverted 'simulated' instead of removing (#874) --- src/data/ability.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 0ff03ed67..4128f22e2 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1755,7 +1755,7 @@ export class StatusEffectImmunityAbAttr extends PreSetStatusAbAttr { } getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { - return getPokemonMessage(pokemon, `'s ${abilityName}\nprevents ${this.immuneEffects.length ? getStatusEffectDescriptor(args[0] as StatusEffect) : 'status problems'}!`); + return getPokemonMessage(pokemon, `'s ${abilityName}\nprevents ${this.immuneEffects.length ? getStatusEffectDescriptor(this.immuneEffects[0]) : 'status problems'}!`); } } @@ -2825,7 +2825,7 @@ export function applyPostStatChangeAbAttrs(attrType: { new(...args: any[]): Post export function applyPreSetStatusAbAttrs(attrType: { new(...args: any[]): PreSetStatusAbAttr }, pokemon: Pokemon, effect: StatusEffect, cancelled: Utils.BooleanHolder, ...args: any[]): Promise { const simulated = args.length > 1 && args[1]; - return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPreSetStatus(pokemon, passive, effect, cancelled, args), args, false, false, !simulated); + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPreSetStatus(pokemon, passive, effect, cancelled, args), args, false, false, simulated); } export function applyPreApplyBattlerTagAbAttrs(attrType: { new(...args: any[]): PreApplyBattlerTagAbAttr }, From 512016faef74ab77f821d3b22863be8bcee1c520 Mon Sep 17 00:00:00 2001 From: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Date: Wed, 15 May 2024 11:10:20 +0200 Subject: [PATCH 15/15] =?UTF-8?q?Sacrifical=20Moves=20(that=20dont=20requi?= =?UTF-8?q?re=20a=20target=20like=20explosion=20or=20self=20d=E2=80=A6=20(?= =?UTF-8?q?#691)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sacrifical Moves (that dont require a target like explosion or self destruct) now also work if the target is flying, diving etc. There is also a new catagorie of moves. "SacrificalMovesOnHit" for all moves that need to hit for them to be sacrifical like MEMENTO * Added comments, added (what i think is TSDoc) to functions and classes. Removed empty lines i introduced * . * Added fixes for the Review by TempsRay. * Added missing * * Remove Target Requirement of SacrificialAttr * Update move.ts --------- Co-authored-by: Benjamin Odom --- src/data/move.ts | 58 +++++++++++++++++++++++++++++++++++++++++++----- src/phases.ts | 16 ++++++++----- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index c89277c55..5f5765c1d 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -788,17 +788,65 @@ export class RecoilAttr extends MoveEffectAttr { } } + +/** + * Attribute used for moves which self KO the user regardless if the move hits a target + * @extends MoveEffectAttr + * @see {@link apply} + **/ export class SacrificialAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, MoveEffectTrigger.POST_TARGET); } + /** + * Deals damage to the user equal to their current hp + * @param user Pokemon that used the move + * @param target The target of the move + * @param move Move with this attribute + * @param args N/A + * @returns true if the function succeeds + **/ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + user.damageAndUpdate(user.hp, HitResult.OTHER, false, true, true); + user.turnData.damageTaken += user.hp; + + return true; + } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + if (user.isBoss()) + return -20; + return Math.ceil(((1 - user.getHpRatio()) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, user) - 0.5)); + } +} + +/** + * Attribute used for moves which self KO the user but only if the move hits a target + * @extends MoveEffectAttr + * @see {@link apply} + **/ +export class SacrificialAttrOnHit extends MoveEffectAttr { + constructor() { + super(true, MoveEffectTrigger.POST_TARGET); + } + + /** + * Deals damage to the user equal to their current hp if the move lands + * @param user Pokemon that used the move + * @param target The target of the move + * @param move Move with this attribute + * @param args N/A + * @returns true if the function succeeds + **/ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + + // If the move fails to hit a target, then the user does not faint and the function returns false if (!super.apply(user, target, move, args)) return false; user.damageAndUpdate(user.hp, HitResult.OTHER, false, true, true); - user.turnData.damageTaken += user.hp; + user.turnData.damageTaken += user.hp; return true; } @@ -5020,7 +5068,7 @@ export function initMoves() { new StatusMove(Moves.WILL_O_WISP, Type.FIRE, 85, 15, -1, 0, 3) .attr(StatusEffectAttr, StatusEffect.BURN), new StatusMove(Moves.MEMENTO, Type.DARK, 100, 10, -1, 0, 3) - .attr(SacrificialAttr) + .attr(SacrificialAttrOnHit) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -2), new AttackMove(Moves.FACADE, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 3) .attr(MovePowerMultiplierAttr, (user, target, move) => user.status @@ -5556,7 +5604,7 @@ export function initMoves() { new AttackMove(Moves.SPACIAL_REND, Type.DRAGON, MoveCategory.SPECIAL, 100, 95, 5, -1, 0, 4) .attr(HighCritAttr), new SelfStatusMove(Moves.LUNAR_DANCE, Type.PSYCHIC, -1, 10, -1, 0, 4) - .attr(SacrificialAttr) + .attr(SacrificialAttrOnHit) .danceMove() .triageMove() .unimplemented(), @@ -5705,7 +5753,7 @@ export function initMoves() { .partial(), new AttackMove(Moves.FINAL_GAMBIT, Type.FIGHTING, MoveCategory.SPECIAL, -1, 100, 5, -1, 0, 5) .attr(UserHpDamageAttr) - .attr(SacrificialAttr), + .attr(SacrificialAttrOnHit), new StatusMove(Moves.BESTOW, Type.NORMAL, -1, 15, -1, 0, 5) .ignoresProtect() .unimplemented(), diff --git a/src/phases.ts b/src/phases.ts index 322f93870..a802582cb 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2,7 +2,7 @@ import BattleScene, { AnySound, bypassLogin, startingWave } from "./battle-scene import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult, TurnMove } from "./field/pokemon"; import * as Utils from './utils'; import { Moves } from "./data/enums/moves"; -import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, OneHitKOAttr, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, DelayedAttackAttr, RechargeAttr, PreMoveMessageAttr, HealStatusEffectAttr, IgnoreOpponentStatChangesAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, OneHitKOAccuracyAttr, ForceSwitchOutAttr, VariableTargetAttr } from "./data/move"; +import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, OneHitKOAttr, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, DelayedAttackAttr, RechargeAttr, PreMoveMessageAttr, HealStatusEffectAttr, IgnoreOpponentStatChangesAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, OneHitKOAccuracyAttr, ForceSwitchOutAttr, VariableTargetAttr, SacrificialAttr } from "./data/move"; import { Mode } from './ui/ui'; import { Command } from "./ui/command-ui-handler"; import { Stat } from "./data/pokemon-stat"; @@ -2304,8 +2304,8 @@ export class MovePhase extends BattlePhase { if (this.move.moveId) this.showMoveText(); - - if ((moveQueue.length && moveQueue[0].move === Moves.NONE) || !targets.length) { + + if ((moveQueue.length && moveQueue[0].move === Moves.NONE) || (!targets.length && !this.move.getMove().getAttrs(SacrificialAttr).length)) { moveQueue.shift(); this.cancel(); this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL }); @@ -2537,8 +2537,14 @@ export class MoveEffectPhase extends PokemonPhase { })); } // Trigger effect which should only apply one time after all targeted effects have already applied - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_TARGET, - user, null, this.move.getMove()) + const postTarget = applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_TARGET, + user, null, this.move.getMove()); + + if (applyAttrs.length) // If there is a pending asynchronous move effect, do this after + applyAttrs[applyAttrs.length - 1]?.then(() => postTarget); + else // Otherwise, push a new asynchronous move effect + applyAttrs.push(postTarget); + Promise.allSettled(applyAttrs).then(() => this.end()); }); });