diff --git a/src/data/custom-pokemon-data.ts b/src/data/custom-pokemon-data.ts index 5b3cf370e54..f6f9016c147 100644 --- a/src/data/custom-pokemon-data.ts +++ b/src/data/custom-pokemon-data.ts @@ -7,17 +7,21 @@ import type { Nature } from "#enums/nature"; * Includes abilities, nature, changed types, etc. */ export class CustomPokemonData { - public spriteScale: number; + public spriteScale = 1; public ability: Abilities | -1; public passive: Abilities | -1; public nature: Nature | -1; public types: PokemonType[]; + /** Deprecated but needed for session save migration */ + // TODO: Remove this once pre-session migration is implemented + public hitsRecCount: number | null = null; constructor(data?: CustomPokemonData | Partial) { - this.spriteScale = data?.spriteScale ?? -1; + this.spriteScale = data?.spriteScale ?? 1; this.ability = data?.ability ?? -1; this.passive = data?.passive ?? -1; this.nature = data?.nature ?? -1; this.types = data?.types ?? []; + this.hitsRecCount = data?.hitsRecCount ?? null; } } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 662ecfca2cf..124625455f2 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1183,12 +1183,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { formKey === "ruchbah-starmobile" || formKey === "caph-starmobile" ) { + // G-Max and starmobiles have flat 1.5x scale return 1.5; } - if (this.customPokemonData.spriteScale > 0) { - return this.customPokemonData.spriteScale; - } - return 1; + return this.customPokemonData.spriteScale; } /** Resets the pokemon's field sprite properties, including position, alpha, and scale */ diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 5899196b55a..65f10747307 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1194,13 +1194,18 @@ export class GameData { } } + // load modifier data + + if (globalScene.modifiers.length) { + console.warn("Existing modifiers not cleared on session load, deleting..."); + globalScene.modifiers = []; + } for (const modifierData of sessionData.modifiers) { const modifier = modifierData.toModifier(Modifier[modifierData.className]); if (modifier) { globalScene.addModifier(modifier, true); } } - globalScene.updateModifiers(true); for (const enemyModifierData of sessionData.enemyModifiers) { @@ -1338,12 +1343,12 @@ export class GameData { } parseSessionData(dataStr: string): SessionSaveData { - // TODO: Add add `null`/`undefined` to the corresponding type signatures for this + // TODO: Add `null`/`undefined` to the corresponding type signatures for this // (or prevent them from being null) // If the value is able to *not exist*, it should say so in the code const sessionData = JSON.parse(dataStr, (k: string, v: any) => { - // TODO: Move this into migrate script + // TODO: Add pre-parse migrate scripts switch (k) { case "party": case "enemyParty": { @@ -1452,7 +1457,7 @@ export class GameData { encrypt(JSON.stringify(sessionData), bypassLogin), ); - console.debug("Session data saved"); + console.debug("Session data saved!"); if (!bypassLogin && sync) { pokerogueApi.savedata.updateAll(request).then(error => { diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 1643245ac4b..7e71dffde5e 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -69,6 +69,12 @@ export default class PokemonData { public customPokemonData: CustomPokemonData; public fusionCustomPokemonData: CustomPokemonData; + // Deprecated attributes, needed for now to allow SessionData migration (see PR#4619 comments) + // TODO: Remove these once pre-session migration is implemented + public natureOverride: Nature | -1; + public mysteryEncounterPokemonData: CustomPokemonData | null; + public fusionMysteryEncounterPokemonData: CustomPokemonData | null; + /** * Construct a new {@linkcode PokemonData} instance out of a {@linkcode Pokemon} * or JSON representation thereof. @@ -114,6 +120,15 @@ export default class PokemonData { this.isTerastallized = !!source.isTerastallized; this.stellarTypesBoosted = source.stellarTypesBoosted ?? []; + // Deprecated, but needed for session data migration + this.natureOverride = source.natureOverride; + this.mysteryEncounterPokemonData = source.mysteryEncounterPokemonData + ? new CustomPokemonData(source.mysteryEncounterPokemonData) + : null; + this.fusionMysteryEncounterPokemonData = source.fusionMysteryEncounterPokemonData + ? new CustomPokemonData(source.fusionMysteryEncounterPokemonData) + : null; + this.fusionSpecies = sourcePokemon?.fusionSpecies?.speciesId ?? source.fusionSpecies; this.fusionFormIndex = source.fusionFormIndex; this.fusionAbilityIndex = source.fusionAbilityIndex; diff --git a/src/system/version_migration/versions/v1_9_0.ts b/src/system/version_migration/versions/v1_9_0.ts index 1d39676bf7b..ed90c28112e 100644 --- a/src/system/version_migration/versions/v1_9_0.ts +++ b/src/system/version_migration/versions/v1_9_0.ts @@ -27,14 +27,17 @@ const migratePartyData: SessionSaveMigrator = { // only edit summondata moveset if exists pkmnData.summonData.moveset &&= pkmnData.summonData.moveset.filter(m => !!m); - if ( - pkmnData.customPokemonData && - "hitsRecCount" in pkmnData.customPokemonData && - typeof pkmnData.customPokemonData["hitsRecCount"] === "number" - ) { - // transfer old hit count stat to battleData. - // No need to reset it as new Pokemon - pkmnData.battleData.hitCount = pkmnData.customPokemonData["hitsRecCount"]; + if (pkmnData.customPokemonData) { + // revert all "-1" sprite scales to a minimum value of 1 + pkmnData.customPokemonData.spriteScale = Math.max(1, pkmnData.customPokemonData.spriteScale); + if ( + "hitsRecCount" in pkmnData.customPokemonData && + typeof pkmnData.customPokemonData["hitsRecCount"] === "number" + ) { + // transfer old hit count stat to battleData. + pkmnData.battleData.hitCount = pkmnData.customPokemonData["hitsRecCount"]; + pkmnData.customPokemonData["hitsRecCount"] = null; + } } return pkmnData; }; diff --git a/test/abilities/harvest.test.ts b/test/abilities/harvest.test.ts index de2318569b8..1536b85d79b 100644 --- a/test/abilities/harvest.test.ts +++ b/test/abilities/harvest.test.ts @@ -82,14 +82,15 @@ describe("Abilities - Harvest", () => { .weather(WeatherType.NONE); // clear weather so we can control when harvest rolls succeed await game.classicMode.startBattle([Species.MILOTIC]); - const player = game.scene.getPlayerPokemon(); + const milotic = game.scene.getPlayerPokemon()!; + expect(milotic).toBeDefined(); // Chug a few berries without harvest (should get tracked) game.move.select(Moves.SPLASH); await game.forceEnemyMove(Moves.NUZZLE); await game.toNextTurn(); - expect(player?.battleData.berriesEaten).toEqual(expect.arrayContaining([BerryType.ENIGMA, BerryType.LUM])); + expect(milotic.battleData.berriesEaten).toEqual(expect.arrayContaining([BerryType.ENIGMA, BerryType.LUM])); expect(getPlayerBerries()).toHaveLength(2); // Give ourselves harvest and disable enemy neut gas, @@ -97,10 +98,12 @@ describe("Abilities - Harvest", () => { game.override.ability(Abilities.HARVEST); game.move.select(Moves.GASTRO_ACID); await game.forceEnemyMove(Moves.NUZZLE); + await game.phaseInterceptor.to("TurnEndPhase", false); vi.spyOn(Phaser.Math.RND, "realInRange").mockReturnValue(0); + await game.phaseInterceptor.to("TurnEndPhase"); - expect(player?.battleData.berriesEaten).toEqual( + expect(milotic.battleData.berriesEaten).toEqual( expect.arrayContaining([BerryType.ENIGMA, BerryType.LUM, BerryType.ENIGMA, BerryType.LUM]), ); expect(getPlayerBerries()).toHaveLength(0); @@ -112,11 +115,11 @@ describe("Abilities - Harvest", () => { vi.spyOn(Phaser.Math.RND, "realInRange").mockReturnValue(1); await game.toNextTurn(); - expect(player?.battleData.berriesEaten).toHaveLength(3); + expect(milotic?.battleData.berriesEaten).toHaveLength(3); expect(getPlayerBerries()).toHaveLength(1); }); - it("remembers berries eaten array across waves and save/reload", async () => { + it("remembers berries eaten array across waves", async () => { game.override .startingHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 2 }]) .ability(Abilities.BALL_FETCH); // don't actually need harvest for this test @@ -132,20 +135,17 @@ describe("Abilities - Harvest", () => { // ate 1 berry without recovering (no harvest) expect(regieleki.battleData.berriesEaten).toEqual([BerryType.PETAYA]); - expect(getPlayerBerries()).toEqual([expect.objectContaining({ berryType: BerryType.PETAYA, stackCount: 1 })]); - expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.SPATK)).toBe(1); + expectBerriesContaining({ name: "BERRY", count: 1, type: BerryType.PETAYA }); + expect(regieleki.getStatStage(Stat.SPATK)).toBe(1); await game.toNextWave(); expect(regieleki.battleData.berriesEaten).toEqual([BerryType.PETAYA]); - - await game.reload.reloadSession(); - - const regielekiReloaded = game.scene.getPlayerPokemon()!; - expect(regielekiReloaded.battleData.berriesEaten).toEqual([BerryType.PETAYA]); + expectBerriesContaining({ name: "BERRY", count: 1, type: BerryType.PETAYA }); + expect(regieleki.getStatStage(Stat.SPATK)).toBe(1); }); - it("keeps berries eaten across reloads", async () => { + it("keeps harvested berries across reloads", async () => { game.override .startingHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 1 }]) .moveset([Moves.SPLASH, Moves.EARTHQUAKE]) @@ -172,12 +172,13 @@ describe("Abilities - Harvest", () => { await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextWave(); - expect(getPlayerBerries()).toEqual([expect.objectContaining({ berryType: BerryType.PETAYA, stackCount: 1 })]); + expectBerriesContaining({ name: "BERRY", count: 1, type: BerryType.PETAYA }); expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.SPATK)).toBe(1); await game.reload.reloadSession(); - expect(getPlayerBerries()).toEqual([expect.objectContaining({ berryType: BerryType.PETAYA, stackCount: 1 })]); + expect(regieleki.battleData.berriesEaten).toEqual([]); + expectBerriesContaining({ name: "BERRY", count: 1, type: BerryType.PETAYA }); expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.SPATK)).toBe(1); }); diff --git a/test/testUtils/helpers/reloadHelper.ts b/test/testUtils/helpers/reloadHelper.ts index 33d9afecefc..4a9e5356968 100644 --- a/test/testUtils/helpers/reloadHelper.ts +++ b/test/testUtils/helpers/reloadHelper.ts @@ -46,6 +46,16 @@ export class ReloadHelper extends GameManagerHelper { scene.unshiftPhase(titlePhase); this.game.endPhase(); // End the currently ongoing battle + // remove all persistent mods before loading + // TODO: Look into why these aren't removed before load + if (this.game.scene.modifiers.length) { + console.log( + "Removing %d modifiers from scene on load...", + this.game.scene.modifiers.length, + this.game.scene.modifiers, + ); + this.game.scene.modifiers = []; + } titlePhase.loadSaveSlot(-1); // Load the desired session data this.game.phaseInterceptor.shift(); // Loading the save slot also ended TitlePhase, clean it up