From c2a7afc5ba8b078533ef6075f96129be18c7d4b0 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 1 Feb 2025 00:54:04 -0800 Subject: [PATCH 01/18] [Test] Add eslint rule enforcing proper `await` usage in tests --- eslint.config.js | 18 ++++++++++++++++++ src/test/evolution.test.ts | 12 ++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 1cea5563a78..b4cd536917c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,6 +5,7 @@ import importX from 'eslint-plugin-import-x'; export default [ { + name: "eslint-config", files: ["src/**/*.{ts,tsx,js,jsx}"], ignores: ["dist/*", "build/*", "coverage/*", "public/*", ".github/*", "node_modules/*", ".vscode/*"], languageOptions: { @@ -48,5 +49,22 @@ export default [ "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], // Disallows multiple empty lines "@typescript-eslint/consistent-type-imports": "error", // Enforces type-only imports wherever possible } + }, + { + name: "eslint-tests", + files: ["src/test/**.test.ts"], + languageOptions: { + parser: parser, + parserOptions: { + "project": ["./tsconfig.json"] + } + }, + plugins: { + "@typescript-eslint": tseslint + }, + rules: { + "@typescript-eslint/no-floating-promises": "error", // Require Promise-like statements to be handled appropriately. - https://typescript-eslint.io/rules/no-floating-promises/ + "@typescript-eslint/no-misused-promises": "error", // Disallow Promises in places not designed to handle them. - https://typescript-eslint.io/rules/no-misused-promises/ + } } ] diff --git a/src/test/evolution.test.ts b/src/test/evolution.test.ts index 10748899d59..29412e9e425 100644 --- a/src/test/evolution.test.ts +++ b/src/test/evolution.test.ts @@ -40,10 +40,10 @@ describe("Evolution", () => { eevee.abilityIndex = 2; trapinch.abilityIndex = 2; - eevee.evolve(pokemonEvolutions[Species.EEVEE][6], eevee.getSpeciesForm()); + await eevee.evolve(pokemonEvolutions[Species.EEVEE][6], eevee.getSpeciesForm()); expect(eevee.abilityIndex).toBe(2); - trapinch.evolve(pokemonEvolutions[Species.TRAPINCH][0], trapinch.getSpeciesForm()); + await trapinch.evolve(pokemonEvolutions[Species.TRAPINCH][0], trapinch.getSpeciesForm()); expect(trapinch.abilityIndex).toBe(1); }); @@ -55,10 +55,10 @@ describe("Evolution", () => { bulbasaur.abilityIndex = 0; charmander.abilityIndex = 1; - bulbasaur.evolve(pokemonEvolutions[Species.BULBASAUR][0], bulbasaur.getSpeciesForm()); + await bulbasaur.evolve(pokemonEvolutions[Species.BULBASAUR][0], bulbasaur.getSpeciesForm()); expect(bulbasaur.abilityIndex).toBe(0); - charmander.evolve(pokemonEvolutions[Species.CHARMANDER][0], charmander.getSpeciesForm()); + await charmander.evolve(pokemonEvolutions[Species.CHARMANDER][0], charmander.getSpeciesForm()); expect(charmander.abilityIndex).toBe(1); }); @@ -68,7 +68,7 @@ describe("Evolution", () => { const squirtle = game.scene.getPlayerPokemon()!; squirtle.abilityIndex = 5; - squirtle.evolve(pokemonEvolutions[Species.SQUIRTLE][0], squirtle.getSpeciesForm()); + await squirtle.evolve(pokemonEvolutions[Species.SQUIRTLE][0], squirtle.getSpeciesForm()); expect(squirtle.abilityIndex).toBe(0); }); @@ -80,7 +80,7 @@ describe("Evolution", () => { nincada.metBiome = -1; nincada.gender = 1; - nincada.evolve(pokemonEvolutions[Species.NINCADA][0], nincada.getSpeciesForm()); + await nincada.evolve(pokemonEvolutions[Species.NINCADA][0], nincada.getSpeciesForm()); const ninjask = game.scene.getPlayerParty()[0]; const shedinja = game.scene.getPlayerParty()[1]; expect(ninjask.abilityIndex).toBe(2); From 7d6036df98b16e690c78a19dc39e798922dba31b Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 1 Feb 2025 01:29:25 -0800 Subject: [PATCH 02/18] Fix files glob --- eslint.config.js | 2 +- src/test/abilities/shield_dust.test.ts | 6 +-- src/test/abilities/unseen_fist.test.ts | 8 ++-- src/test/data/status_effect.test.ts | 4 +- src/test/items/light_ball.test.ts | 14 +++---- src/test/items/metal_powder.test.ts | 10 ++--- src/test/items/quick_powder.test.ts | 26 ++++++------- src/test/items/thick_club.test.ts | 38 +++++++++---------- src/test/moves/dragon_rage.test.ts | 6 +-- src/test/moves/fissure.test.ts | 6 +-- src/test/moves/toxic_spikes.test.ts | 2 +- .../mystery-encounter-utils.test.ts | 24 ++++++------ src/test/ui/transfer-item.test.ts | 13 +++---- 13 files changed, 74 insertions(+), 85 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index b4cd536917c..0da9cc604bf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -52,7 +52,7 @@ export default [ }, { name: "eslint-tests", - files: ["src/test/**.test.ts"], + files: ["src/test/**/**.test.ts"], languageOptions: { parser: parser, parserOptions: { diff --git a/src/test/abilities/shield_dust.test.ts b/src/test/abilities/shield_dust.test.ts index 9f1e6aeb11d..329f52cc4c6 100644 --- a/src/test/abilities/shield_dust.test.ts +++ b/src/test/abilities/shield_dust.test.ts @@ -53,11 +53,11 @@ describe("Abilities - Shield Dust", () => { expect(move.id).toBe(Moves.AIR_SLASH); const chance = new NumberHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); - applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance); + await applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + await applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance); expect(chance.value).toBe(0); - }, 20000); + }); //TODO King's Rock Interaction Unit Test }); diff --git a/src/test/abilities/unseen_fist.test.ts b/src/test/abilities/unseen_fist.test.ts index f8fa8a723fe..584f997aa55 100644 --- a/src/test/abilities/unseen_fist.test.ts +++ b/src/test/abilities/unseen_fist.test.ts @@ -45,9 +45,9 @@ describe("Abilities - Unseen Fist", () => { it( "should not apply if the source has Long Reach", - () => { + async () => { game.override.passiveAbility(Abilities.LONG_REACH); - testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false); + await testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false); } ); @@ -67,7 +67,7 @@ describe("Abilities - Unseen Fist", () => { game.override.enemyLevel(1); game.override.moveset([ Moves.TACKLE ]); - await game.startBattle(); + await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; enemyPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, enemyPokemon.id); @@ -86,7 +86,7 @@ async function testUnseenFistHitResult(game: GameManager, attackMove: Moves, pro game.override.moveset([ attackMove ]); game.override.enemyMoveset([ protectMove, protectMove, protectMove, protectMove ]); - await game.startBattle(); + await game.classicMode.startBattle(); const leadPokemon = game.scene.getPlayerPokemon()!; expect(leadPokemon).not.toBe(undefined); diff --git a/src/test/data/status_effect.test.ts b/src/test/data/status_effect.test.ts index 7948549b8e8..071dea989a9 100644 --- a/src/test/data/status_effect.test.ts +++ b/src/test/data/status_effect.test.ts @@ -20,8 +20,8 @@ const pokemonName = "PKM"; const sourceText = "SOURCE"; describe("Status Effect Messages", () => { - beforeAll(() => { - i18next.init(); + beforeAll(async () => { + await i18next.init(); }); describe("NONE", () => { diff --git a/src/test/items/light_ball.test.ts b/src/test/items/light_ball.test.ts index 987a5ab8b0c..aae1d806a28 100644 --- a/src/test/items/light_ball.test.ts +++ b/src/test/items/light_ball.test.ts @@ -31,7 +31,7 @@ describe("Items - Light Ball", () => { it("LIGHT_BALL activates in battle correctly", async() => { game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "LIGHT_BALL" }]); const consoleSpy = vi.spyOn(console, "log"); - await game.startBattle([ + await game.classicMode.startBattle([ Species.PIKACHU ]); @@ -64,7 +64,7 @@ describe("Items - Light Ball", () => { }); it("LIGHT_BALL held by PIKACHU", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.PIKACHU ]); @@ -83,7 +83,7 @@ describe("Items - Light Ball", () => { expect(spAtkValue.value / spAtkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); @@ -92,7 +92,7 @@ describe("Items - Light Ball", () => { }, 20000); it("LIGHT_BALL held by fused PIKACHU (base)", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.PIKACHU, Species.MAROWAK ]); @@ -122,7 +122,7 @@ describe("Items - Light Ball", () => { expect(spAtkValue.value / spAtkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); @@ -161,7 +161,7 @@ describe("Items - Light Ball", () => { expect(spAtkValue.value / spAtkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); @@ -189,7 +189,7 @@ describe("Items - Light Ball", () => { expect(spAtkValue.value / spAtkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); diff --git a/src/test/items/metal_powder.test.ts b/src/test/items/metal_powder.test.ts index 42ef9c1bb16..68c3107af08 100644 --- a/src/test/items/metal_powder.test.ts +++ b/src/test/items/metal_powder.test.ts @@ -31,7 +31,7 @@ describe("Items - Metal Powder", () => { it("METAL_POWDER activates in battle correctly", async() => { game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "METAL_POWDER" }]); const consoleSpy = vi.spyOn(console, "log"); - await game.startBattle([ + await game.classicMode.startBattle([ Species.DITTO ]); @@ -79,7 +79,7 @@ describe("Items - Metal Powder", () => { expect(defValue.value / defStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); expect(defValue.value / defStat).toBe(2); @@ -112,7 +112,7 @@ describe("Items - Metal Powder", () => { expect(defValue.value / defStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); expect(defValue.value / defStat).toBe(2); @@ -145,7 +145,7 @@ describe("Items - Metal Powder", () => { expect(defValue.value / defStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); expect(defValue.value / defStat).toBe(2); @@ -167,7 +167,7 @@ describe("Items - Metal Powder", () => { expect(defValue.value / defStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); expect(defValue.value / defStat).toBe(1); diff --git a/src/test/items/quick_powder.test.ts b/src/test/items/quick_powder.test.ts index d30111cbd6a..ae16daf17ff 100644 --- a/src/test/items/quick_powder.test.ts +++ b/src/test/items/quick_powder.test.ts @@ -31,7 +31,7 @@ describe("Items - Quick Powder", () => { it("QUICK_POWDER activates in battle correctly", async() => { game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "QUICK_POWDER" }]); const consoleSpy = vi.spyOn(console, "log"); - await game.startBattle([ + await game.classicMode.startBattle([ Species.DITTO ]); @@ -64,7 +64,7 @@ describe("Items - Quick Powder", () => { }); it("QUICK_POWDER held by DITTO", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.DITTO ]); @@ -79,14 +79,14 @@ describe("Items - Quick Powder", () => { expect(spdValue.value / spdStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); expect(spdValue.value / spdStat).toBe(2); - }, 20000); + }); it("QUICK_POWDER held by fused DITTO (base)", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.DITTO, Species.MAROWAK ]); @@ -112,14 +112,14 @@ describe("Items - Quick Powder", () => { expect(spdValue.value / spdStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); expect(spdValue.value / spdStat).toBe(2); - }, 20000); + }); it("QUICK_POWDER held by fused DITTO (part)", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.MAROWAK, Species.DITTO ]); @@ -145,14 +145,14 @@ describe("Items - Quick Powder", () => { expect(spdValue.value / spdStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); expect(spdValue.value / spdStat).toBe(2); - }, 20000); + }); it("QUICK_POWDER not held by DITTO", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.MAROWAK ]); @@ -167,9 +167,9 @@ describe("Items - Quick Powder", () => { expect(spdValue.value / spdStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); expect(spdValue.value / spdStat).toBe(1); - }, 20000); + }); }); diff --git a/src/test/items/thick_club.test.ts b/src/test/items/thick_club.test.ts index 08b19250ea7..d32c213e506 100644 --- a/src/test/items/thick_club.test.ts +++ b/src/test/items/thick_club.test.ts @@ -31,7 +31,7 @@ describe("Items - Thick Club", () => { it("THICK_CLUB activates in battle correctly", async() => { game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }]); const consoleSpy = vi.spyOn(console, "log"); - await game.startBattle([ + await game.classicMode.startBattle([ Species.CUBONE ]); @@ -64,7 +64,7 @@ describe("Items - Thick Club", () => { }); it("THICK_CLUB held by CUBONE", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.CUBONE ]); @@ -79,14 +79,14 @@ describe("Items - Thick Club", () => { expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); expect(atkValue.value / atkStat).toBe(2); - }, 20000); + }); it("THICK_CLUB held by MAROWAK", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.MAROWAK ]); @@ -101,14 +101,14 @@ describe("Items - Thick Club", () => { expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); expect(atkValue.value / atkStat).toBe(2); - }, 20000); + }); it("THICK_CLUB held by ALOLA_MAROWAK", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.ALOLA_MAROWAK ]); @@ -123,18 +123,18 @@ describe("Items - Thick Club", () => { expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); expect(atkValue.value / atkStat).toBe(2); - }, 20000); + }); it("THICK_CLUB held by fused CUBONE line (base)", async() => { // Randomly choose from the Cubone line const species = [ Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK ]; const randSpecies = Utils.randInt(species.length); - await game.startBattle([ + await game.classicMode.startBattle([ species[randSpecies], Species.PIKACHU ]); @@ -160,18 +160,18 @@ describe("Items - Thick Club", () => { expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); expect(atkValue.value / atkStat).toBe(2); - }, 20000); + }); it("THICK_CLUB held by fused CUBONE line (part)", async() => { // Randomly choose from the Cubone line const species = [ Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK ]; const randSpecies = Utils.randInt(species.length); - await game.startBattle([ + await game.classicMode.startBattle([ Species.PIKACHU, species[randSpecies] ]); @@ -197,14 +197,14 @@ describe("Items - Thick Club", () => { expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); expect(atkValue.value / atkStat).toBe(2); - }, 20000); + }); it("THICK_CLUB not held by CUBONE", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.PIKACHU ]); @@ -219,9 +219,9 @@ describe("Items - Thick Club", () => { expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); expect(atkValue.value / atkStat).toBe(1); - }, 20000); + }); }); diff --git a/src/test/moves/dragon_rage.test.ts b/src/test/moves/dragon_rage.test.ts index a2350960546..61630ede326 100644 --- a/src/test/moves/dragon_rage.test.ts +++ b/src/test/moves/dragon_rage.test.ts @@ -45,14 +45,10 @@ describe("Moves - Dragon Rage", () => { game.override.enemyPassiveAbility(Abilities.BALL_FETCH); game.override.enemyLevel(100); - await game.startBattle(); + await game.classicMode.startBattle(); partyPokemon = game.scene.getPlayerParty()[0]; enemyPokemon = game.scene.getEnemyPokemon()!; - - // remove berries - game.scene.removePartyMemberModifiers(0); - game.scene.clearEnemyHeldItemModifiers(); }); it("ignores weaknesses", async () => { diff --git a/src/test/moves/fissure.test.ts b/src/test/moves/fissure.test.ts index 0975a87b2b1..65719df0205 100644 --- a/src/test/moves/fissure.test.ts +++ b/src/test/moves/fissure.test.ts @@ -41,14 +41,10 @@ describe("Moves - Fissure", () => { game.override.enemyPassiveAbility(Abilities.BALL_FETCH); game.override.enemyLevel(100); - await game.startBattle(); + await game.classicMode.startBattle(); partyPokemon = game.scene.getPlayerParty()[0]; enemyPokemon = game.scene.getEnemyPokemon()!; - - // remove berries - game.scene.removePartyMemberModifiers(0); - game.scene.clearEnemyHeldItemModifiers(); }); it("ignores damage modification from abilities, for example FUR_COAT", async () => { diff --git a/src/test/moves/toxic_spikes.test.ts b/src/test/moves/toxic_spikes.test.ts index c2d1c5aaee8..8969289c2f2 100644 --- a/src/test/moves/toxic_spikes.test.ts +++ b/src/test/moves/toxic_spikes.test.ts @@ -132,7 +132,7 @@ describe("Moves - Toxic Spikes", () => { const sessionData : SessionSaveData = gameData["getSessionSaveData"](); localStorage.setItem("sessionTestData", encrypt(JSON.stringify(sessionData), true)); const recoveredData : SessionSaveData = gameData.parseSessionData(decrypt(localStorage.getItem("sessionTestData")!, true)); - gameData.loadSession(0, recoveredData); + await gameData.loadSession(0, recoveredData); expect(sessionData.arena.tags).toEqual(recoveredData.arena.tags); localStorage.removeItem("sessionTestData"); diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index f0057fea7f0..7c924b86e0d 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -48,12 +48,12 @@ describe("Mystery Encounter Utils", () => { expect(result.species.speciesId).toBe(Species.ARCEUS); }); - it("gets a fainted pokemon from player party if isAllowedInBattle is false", () => { + it("gets a fainted pokemon from player party if isAllowedInBattle is false", async () => { // Both pokemon fainted scene.getPlayerParty().forEach(p => { p.hp = 0; p.trySetStatus(StatusEffect.FAINT); - p.updateInfo(); + void p.updateInfo(); }); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) @@ -68,12 +68,12 @@ describe("Mystery Encounter Utils", () => { expect(result.species.speciesId).toBe(Species.ARCEUS); }); - it("gets an unfainted legal pokemon from player party if isAllowed is true and isFainted is false", () => { + it("gets an unfainted legal pokemon from player party if isAllowed is true and isFainted is false", async () => { // Only faint 1st pokemon const party = scene.getPlayerParty(); party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); - party[0].updateInfo(); + await party[0].updateInfo(); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) game.override.seed("random"); @@ -87,12 +87,12 @@ describe("Mystery Encounter Utils", () => { expect(result.species.speciesId).toBe(Species.MANAPHY); }); - it("returns last unfainted pokemon if doNotReturnLastAbleMon is false", () => { + it("returns last unfainted pokemon if doNotReturnLastAbleMon is false", async () => { // Only faint 1st pokemon const party = scene.getPlayerParty(); party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); - party[0].updateInfo(); + await party[0].updateInfo(); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) game.override.seed("random"); @@ -106,12 +106,12 @@ describe("Mystery Encounter Utils", () => { expect(result.species.speciesId).toBe(Species.MANAPHY); }); - it("never returns last unfainted pokemon if doNotReturnLastAbleMon is true", () => { + it("never returns last unfainted pokemon if doNotReturnLastAbleMon is true", async () => { // Only faint 1st pokemon const party = scene.getPlayerParty(); party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); - party[0].updateInfo(); + await party[0].updateInfo(); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) game.override.seed("random"); @@ -152,12 +152,12 @@ describe("Mystery Encounter Utils", () => { expect(result.species.speciesId).toBe(Species.ARCEUS); }); - it("returns highest level unfainted if unfainted is true", () => { + it("returns highest level unfainted if unfainted is true", async () => { const party = scene.getPlayerParty(); party[0].level = 100; party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); - party[0].updateInfo(); + await party[0].updateInfo(); party[1].level = 10; const result = getHighestLevelPlayerPokemon(true); @@ -191,12 +191,12 @@ describe("Mystery Encounter Utils", () => { expect(result.species.speciesId).toBe(Species.ARCEUS); }); - it("returns lowest level unfainted if unfainted is true", () => { + it("returns lowest level unfainted if unfainted is true", async () => { const party = scene.getPlayerParty(); party[0].level = 10; party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); - party[0].updateInfo(); + await party[0].updateInfo(); party[1].level = 100; const result = getLowestLevelPlayerPokemon(true); diff --git a/src/test/ui/transfer-item.test.ts b/src/test/ui/transfer-item.test.ts index 762db7fc7ce..b08b056f60e 100644 --- a/src/test/ui/transfer-item.test.ts +++ b/src/test/ui/transfer-item.test.ts @@ -2,8 +2,6 @@ import { BerryType } from "#app/enums/berry-type"; import { Button } from "#app/enums/buttons"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; -import { BattleEndPhase } from "#app/phases/battle-end-phase"; -import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import PartyUiHandler, { PartyUiMode } from "#app/ui/party-ui-handler"; import { Mode } from "#app/ui/ui"; @@ -12,7 +10,6 @@ import Phaser from "phaser"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - describe("UI - Transfer Items", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -41,7 +38,7 @@ describe("UI - Transfer Items", () => { game.override.enemySpecies(Species.MAGIKARP); game.override.enemyMoveset([ Moves.SPLASH ]); - await game.startBattle([ Species.RAYQUAZA, Species.RAYQUAZA, Species.RAYQUAZA ]); + await game.classicMode.startBattle([ Species.RAYQUAZA, Species.RAYQUAZA, Species.RAYQUAZA ]); game.move.select(Moves.DRAGON_CLAW); @@ -52,10 +49,10 @@ describe("UI - Transfer Items", () => { handler.setCursor(1); handler.processInput(Button.ACTION); - game.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER); + void game.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER); }); - await game.phaseInterceptor.to(BattleEndPhase); + await game.phaseInterceptor.to("BattleEndPhase"); }); it("check red tint for held item limit in transfer menu", async () => { @@ -72,7 +69,7 @@ describe("UI - Transfer Items", () => { game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.to(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); }, 20000); it("check transfer option for pokemon to transfer to", async () => { @@ -91,6 +88,6 @@ describe("UI - Transfer Items", () => { game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.to(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); }, 20000); }); From 7cd3217114d209a92f113ea290f83bd541c8fba5 Mon Sep 17 00:00:00 2001 From: PrabbyDD <147005742+PrabbyDD@users.noreply.github.com> Date: Sun, 9 Feb 2025 00:14:56 -0800 Subject: [PATCH 03/18] [Ability] Implement Mirror Armor (#4769) * beginnings of implementation of mirror armor * logging some new changes * fixing edge cases * adding changes for sticky web and other features of mirror armor * adding changes for sticky web and other features of mirror armor * adding more unit tests and cleaning up notes * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * using arena tags source id variable * updating submodule pointer for locales * small change * Update src/data/move.ts commit Kev fix (minor flip for consistency) Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * fix import * Use global scene * Update tests --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> Co-authored-by: Madmadness65 --- src/battle.ts | 3 + src/data/ability.ts | 42 +++- src/data/arena-tag.ts | 2 +- src/phases/move-effect-phase.ts | 7 + src/phases/show-ability-phase.ts | 8 + src/phases/stat-stage-change-phase.ts | 50 +++- src/test/abilities/mirror_armor.test.ts | 315 ++++++++++++++++++++++++ 7 files changed, 421 insertions(+), 6 deletions(-) create mode 100644 src/test/abilities/mirror_armor.test.ts diff --git a/src/battle.ts b/src/battle.ts index fa333040c22..3f36865c74b 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -101,6 +101,9 @@ export default class Battle { public battleSeed: string = Utils.randomString(16, true); private battleSeedState: string | null = null; public moneyScattered: number = 0; + /** Primarily for double battles, keeps track of last enemy and player pokemon that triggered its ability or used a move */ + public lastEnemyInvolved: number; + public lastPlayerInvolved: number; public lastUsedPokeball: PokeballType | null = null; /** The number of times a Pokemon on the player's side has fainted this battle */ public playerFaints: number = 0; diff --git a/src/data/ability.ts b/src/data/ability.ts index c19b6fe9ba4..21ec5667426 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2756,6 +2756,44 @@ export class PreStatStageChangeAbAttr extends AbAttr { } } +/** + * Reflect all {@linkcode BattleStat} reductions caused by other Pokémon's moves and Abilities. + * Currently only applies to Mirror Armor. + */ +export class ReflectStatStageChangeAbAttr extends PreStatStageChangeAbAttr { + /** {@linkcode BattleStat} to reflect */ + private reflectedStat? : BattleStat; + + /** + * Apply the {@linkcode ReflectStatStageChangeAbAttr} to an interaction + * @param _pokemon The user pokemon + * @param _passive N/A + * @param simulated `true` if the ability is being simulated by the AI + * @param stat the {@linkcode BattleStat} being affected + * @param cancelled The {@linkcode Utils.BooleanHolder} that will be set to true due to reflection + * @param args + * @returns true because it reflects any stat being lowered + */ + applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean { + const attacker: Pokemon = args[0]; + const stages = args[1]; + this.reflectedStat = stat; + if (!simulated) { + globalScene.unshiftPhase(new StatStageChangePhase(attacker.getBattlerIndex(), false, [ stat ], stages, true, false, true, null, true)); + } + cancelled.value = true; + return true; + } + + getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string { + return i18next.t("abilityTriggers:protectStat", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + abilityName, + statName: this.reflectedStat ? i18next.t(getStatKey(this.reflectedStat)) : i18next.t("battle:stats") + }); + } +} + /** * Protect one or all {@linkcode BattleStat} from reductions caused by other Pokémon's moves and Abilities */ @@ -6065,8 +6103,8 @@ export function initAbilities() { new Ability(Abilities.PROPELLER_TAIL, 8) .attr(BlockRedirectAbAttr), new Ability(Abilities.MIRROR_ARMOR, 8) - .ignorable() - .unimplemented(), + .attr(ReflectStatStageChangeAbAttr) + .ignorable(), /** * Right now, the logic is attached to Surf and Dive moves. Ideally, the post-defend/hit should be an * ability attribute but the current implementation of move effects for BattlerTag does not support this- in the case diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 816de3e824c..2fa4593fd6c 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -910,7 +910,7 @@ class StickyWebTag extends ArenaTrapTag { if (!cancelled.value) { globalScene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() })); const stages = new NumberHolder(-1); - globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value)); + globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value, true, false, true, null, false, true)); return true; } } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index fff8caf38b5..be9a36940ea 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -95,6 +95,13 @@ export class MoveEffectPhase extends PokemonPhase { return super.end(); } + /** If an enemy used this move, set this as last enemy that used move or ability */ + if (!user.isPlayer()) { + globalScene.currentBattle.lastEnemyInvolved = this.fieldIndex; + } else { + globalScene.currentBattle.lastPlayerInvolved = this.fieldIndex; + } + const isDelayedAttack = this.move.getMove().hasAttr(DelayedAttackAttr); /** If the user was somehow removed from the field and it's not a delayed attack, end this phase */ if (!user.isOnField()) { diff --git a/src/phases/show-ability-phase.ts b/src/phases/show-ability-phase.ts index a0db660ded5..d759ad833a1 100644 --- a/src/phases/show-ability-phase.ts +++ b/src/phases/show-ability-phase.ts @@ -17,6 +17,14 @@ export class ShowAbilityPhase extends PokemonPhase { const pokemon = this.getPokemon(); if (pokemon) { + + if (!pokemon.isPlayer()) { + /** If its an enemy pokemon, list it as last enemy to use ability or move */ + globalScene.currentBattle.lastEnemyInvolved = pokemon.getBattlerIndex() % 2; + } else { + globalScene.currentBattle.lastPlayerInvolved = pokemon.getBattlerIndex() % 2; + } + globalScene.abilityBar.showAbility(pokemon, this.passive); if (pokemon?.battleData) { diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 359610b320c..753d1f7cede 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -1,7 +1,8 @@ import { globalScene } from "#app/global-scene"; import type { BattlerIndex } from "#app/battle"; -import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, PostStatStageChangeAbAttr, ProtectStatAbAttr, StatStageChangeCopyAbAttr, StatStageChangeMultiplierAbAttr } from "#app/data/ability"; +import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, PostStatStageChangeAbAttr, ProtectStatAbAttr, ReflectStatStageChangeAbAttr, StatStageChangeCopyAbAttr, StatStageChangeMultiplierAbAttr } from "#app/data/ability"; import { ArenaTagSide, MistTag } from "#app/data/arena-tag"; +import type { ArenaTag } from "#app/data/arena-tag"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { ResetNegativeStatStageModifier } from "#app/modifier/modifier"; @@ -10,6 +11,8 @@ import { NumberHolder, BooleanHolder } from "#app/utils"; import i18next from "i18next"; import { PokemonPhase } from "./pokemon-phase"; import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat"; +import { OctolockTag } from "#app/data/battler-tags"; +import { ArenaTagType } from "#app/enums/arena-tag-type"; export type StatStageChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void; @@ -21,9 +24,11 @@ export class StatStageChangePhase extends PokemonPhase { private ignoreAbilities: boolean; private canBeCopied: boolean; private onChange: StatStageChangeCallback | null; + private comingFromMirrorArmorUser: boolean; + private comingFromStickyWeb: boolean; - constructor(battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: number, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatStageChangeCallback | null = null) { + constructor(battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: number, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatStageChangeCallback | null = null, comingFromMirrorArmorUser: boolean = false, comingFromStickyWeb: boolean = false) { super(battlerIndex); this.selfTarget = selfTarget; @@ -33,6 +38,8 @@ export class StatStageChangePhase extends PokemonPhase { this.ignoreAbilities = ignoreAbilities; this.canBeCopied = canBeCopied; this.onChange = onChange; + this.comingFromMirrorArmorUser = comingFromMirrorArmorUser; + this.comingFromStickyWeb = comingFromStickyWeb; } start() { @@ -41,12 +48,44 @@ export class StatStageChangePhase extends PokemonPhase { if (this.stats.length > 1) { for (let i = 0; i < this.stats.length; i++) { const stat = [ this.stats[i] ]; - globalScene.unshiftPhase(new StatStageChangePhase(this.battlerIndex, this.selfTarget, stat, this.stages, this.showMessage, this.ignoreAbilities, this.canBeCopied, this.onChange)); + globalScene.unshiftPhase(new StatStageChangePhase(this.battlerIndex, this.selfTarget, stat, this.stages, this.showMessage, this.ignoreAbilities, this.canBeCopied, this.onChange, this.comingFromMirrorArmorUser)); } return this.end(); } const pokemon = this.getPokemon(); + let opponentPokemon: Pokemon | undefined; + + /** Gets the position of last enemy or player pokemon that used ability or move, primarily for double battles involving Mirror Armor */ + if (pokemon.isPlayer()) { + /** If this SSCP is not from sticky web, then we find the opponent pokemon that last did something */ + if (!this.comingFromStickyWeb) { + opponentPokemon = globalScene.getEnemyField()[globalScene.currentBattle.lastEnemyInvolved]; + } else { + /** If this SSCP is from sticky web, then check if pokemon that last sucessfully used sticky web is on field */ + const stickyTagID = globalScene.arena.findTagsOnSide( + (t: ArenaTag) => t.tagType === ArenaTagType.STICKY_WEB, + ArenaTagSide.PLAYER)[0].sourceId; + globalScene.getEnemyField().forEach((e) => { + if (e.id === stickyTagID) { + opponentPokemon = e; + } + }); + } + } else { + if (!this.comingFromStickyWeb) { + opponentPokemon = globalScene.getPlayerField()[globalScene.currentBattle.lastPlayerInvolved]; + } else { + const stickyTagID = globalScene.arena.findTagsOnSide( + (t: ArenaTag) => t.tagType === ArenaTagType.STICKY_WEB, + ArenaTagSide.ENEMY)[0].sourceId; + globalScene.getPlayerField().forEach((e) => { + if (e.id === stickyTagID) { + opponentPokemon = e; + } + }); + } + } if (!pokemon.isActive(true)) { return this.end(); @@ -70,6 +109,11 @@ export class StatStageChangePhase extends PokemonPhase { if (!cancelled.value && !this.selfTarget && stages.value < 0) { applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate); + + /** Potential stat reflection due to Mirror Armor, does not apply to Octolock end of turn effect */ + if (opponentPokemon !== undefined && !pokemon.findTag(t => t instanceof OctolockTag) && !this.comingFromMirrorArmorUser) { + applyPreStatStageChangeAbAttrs(ReflectStatStageChangeAbAttr, pokemon, stat, cancelled, simulate, opponentPokemon, this.stages); + } } // If one stat stage decrease is cancelled, simulate the rest of the applications diff --git a/src/test/abilities/mirror_armor.test.ts b/src/test/abilities/mirror_armor.test.ts new file mode 100644 index 00000000000..070428a8ee7 --- /dev/null +++ b/src/test/abilities/mirror_armor.test.ts @@ -0,0 +1,315 @@ +import { Stat } from "#enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { BattlerIndex } from "#app/battle"; + +// TODO: When Magic Bounce is implemented, make a test for its interaction with mirror guard, use screech + +describe("Ability - Mirror Armor", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override.battleType("single") + .enemySpecies(Species.RATTATA) + .enemyMoveset([ Moves.SPLASH, Moves.STICKY_WEB, Moves.TICKLE, Moves.OCTOLOCK ]) + .enemyAbility(Abilities.BALL_FETCH) + .startingLevel(2000) + .moveset([ Moves.SPLASH, Moves.STICKY_WEB, Moves.TICKLE, Moves.OCTOLOCK ]) + .ability(Abilities.BALL_FETCH); + }); + + it("Player side + single battle Intimidate - opponent loses stats", async () => { + game.override.ability(Abilities.MIRROR_ARMOR); + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy has intimidate, enemy should lose -1 atk + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(userPokemon.getStatStage(Stat.ATK)).toBe(0); + }); + + it("Enemy side + single battle Intimidate - player loses stats", async () => { + game.override.enemyAbility(Abilities.MIRROR_ARMOR); + game.override.ability(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy has intimidate, enemy should lose -1 atk + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(userPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + }); + + it("Player side + double battle Intimidate - opponents each lose -2 atk", async () => { + game.override.battleType("double"); + game.override.ability(Abilities.MIRROR_ARMOR); + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + + const [ enemy1, enemy2 ] = game.scene.getEnemyField(); + const [ player1, player2 ] = game.scene.getPlayerField(); + + // Enemy has intimidate, enemy should lose -2 atk each + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.toNextTurn(); + + expect(enemy1.getStatStage(Stat.ATK)).toBe(-2); + expect(enemy2.getStatStage(Stat.ATK)).toBe(-2); + expect(player1.getStatStage(Stat.ATK)).toBe(0); + expect(player2.getStatStage(Stat.ATK)).toBe(0); + }); + + it("Enemy side + double battle Intimidate - players each lose -2 atk", async () => { + game.override.battleType("double"); + game.override.enemyAbility(Abilities.MIRROR_ARMOR); + game.override.ability(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + + const [ enemy1, enemy2 ] = game.scene.getEnemyField(); + const [ player1, player2 ] = game.scene.getPlayerField(); + + // Enemy has intimidate, enemy should lose -1 atk + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.toNextTurn(); + + expect(enemy1.getStatStage(Stat.ATK)).toBe(0); + expect(enemy2.getStatStage(Stat.ATK)).toBe(0); + expect(player1.getStatStage(Stat.ATK)).toBe(-2); + expect(player2.getStatStage(Stat.ATK)).toBe(-2); + }); + + it("Player side + single battle Intimidate + Tickle - opponent loses stats", async () => { + game.override.ability(Abilities.MIRROR_ARMOR); + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy has intimidate and uses tickle, enemy receives -2 atk and -1 defense + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2); + expect(userPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(userPokemon.getStatStage(Stat.DEF)).toBe(0); + }); + + it("Player side + double battle Intimidate + Tickle - opponents each lose -3 atk, -1 def", async () => { + game.override.battleType("double"); + game.override.ability(Abilities.MIRROR_ARMOR); + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + + const [ enemy1, enemy2 ] = game.scene.getEnemyField(); + const [ player1, player2 ] = game.scene.getPlayerField(); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER_2); + await game.toNextTurn(); + + expect(player1.getStatStage(Stat.ATK)).toBe(0); + expect(player1.getStatStage(Stat.DEF)).toBe(0); + expect(player2.getStatStage(Stat.ATK)).toBe(0); + expect(player2.getStatStage(Stat.DEF)).toBe(0); + expect(enemy1.getStatStage(Stat.ATK)).toBe(-3); + expect(enemy1.getStatStage(Stat.DEF)).toBe(-1); + expect(enemy2.getStatStage(Stat.ATK)).toBe(-3); + expect(enemy2.getStatStage(Stat.DEF)).toBe(-1); + + }); + + it("Enemy side + single battle Intimidate + Tickle - player loses stats", async () => { + game.override.enemyAbility(Abilities.MIRROR_ARMOR); + game.override.ability(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy has intimidate and uses tickle, enemy receives -2 atk and -1 defense + game.move.select(Moves.TICKLE); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(userPokemon.getStatStage(Stat.DEF)).toBe(-1); + expect(userPokemon.getStatStage(Stat.ATK)).toBe(-2); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); + }); + + it("Player side + single battle Intimidate + oppoenent has white smoke - no one loses stats", async () => { + game.override.enemyAbility(Abilities.WHITE_SMOKE); + game.override.ability(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy has intimidate and uses tickle, enemy has white smoke, no one loses stats + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(userPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(userPokemon.getStatStage(Stat.DEF)).toBe(0); + }); + + it("Enemy side + single battle Intimidate + player has white smoke - no one loses stats", async () => { + game.override.ability(Abilities.WHITE_SMOKE); + game.override.enemyAbility(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy has intimidate and uses tickle, enemy has white smoke, no one loses stats + game.move.select(Moves.TICKLE); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(userPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(userPokemon.getStatStage(Stat.DEF)).toBe(0); + }); + + it("Player side + single battle + opponent uses octolock - does not interact with mirror armor, player loses stats", async () => { + game.override.ability(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy uses octolock, player loses stats at end of turn + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.OCTOLOCK, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0); + expect(userPokemon.getStatStage(Stat.DEF)).toBe(-1); + expect(userPokemon.getStatStage(Stat.SPDEF)).toBe(-1); + }); + + it("Enemy side + single battle + player uses octolock - does not interact with mirror armor, opponent loses stats", async () => { + game.override.enemyAbility(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Player uses octolock, enemy loses stats at end of turn + game.move.select(Moves.OCTOLOCK); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(userPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(userPokemon.getStatStage(Stat.SPDEF)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1); + expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1); + }); + + it("Both sides have mirror armor - does not loop, player loses attack", async () => { + game.override.enemyAbility(Abilities.MIRROR_ARMOR); + game.override.ability(Abilities.MIRROR_ARMOR); + game.override.ability(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(userPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + }); + + it("Single battle + sticky web applied player side - player switches out and enemy should lose -1 speed", async () => { + game.override.ability(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.STICKY_WEB, BattlerIndex.PLAYER); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(userPokemon.getStatStage(Stat.SPD)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(-1); + }); + + it("Double battle + sticky web applied player side - player switches out and enemy 1 should lose -1 speed", async () => { + game.override.battleType("double"); + game.override.ability(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + const [ enemy1, enemy2 ] = game.scene.getEnemyField(); + const [ player1, player2 ] = game.scene.getPlayerField(); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.STICKY_WEB, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.toNextTurn(); + + game.doSwitchPokemon(2); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.toNextTurn(); + + expect(enemy1.getStatStage(Stat.SPD)).toBe(-1); + expect(enemy2.getStatStage(Stat.SPD)).toBe(0); + expect(player1.getStatStage(Stat.SPD)).toBe(0); + expect(player2.getStatStage(Stat.SPD)).toBe(0); + }); +}); From 366c88517c36ae46fd00f540f3ae7f7b860e6234 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Sun, 9 Feb 2025 03:41:59 -0500 Subject: [PATCH 04/18] [UI/UX] Show IVs on stats page of summary (#5172) * Show IVs on stats screen * Clearer text and gold perfect IVs --- src/ui/summary-ui-handler.ts | 50 +++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 3305b3f7aa2..cf5d40bc006 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -96,6 +96,9 @@ export default class SummaryUiHandler extends UiHandler { private friendshipText: Phaser.GameObjects.Text; private friendshipIcon: Phaser.GameObjects.Sprite; private friendshipOverlay: Phaser.GameObjects.Sprite; + private permStatsContainer: Phaser.GameObjects.Container; + private ivContainer: Phaser.GameObjects.Container; + private statsContainer: Phaser.GameObjects.Container; private descriptionScrollTween: Phaser.Tweens.Tween | null; private moveCursorBlinkTimer: Phaser.Time.TimerEvent | null; @@ -534,6 +537,10 @@ export default class SummaryUiHandler extends UiHandler { this.passiveContainer.nameText?.setVisible(!this.passiveContainer.descriptionText?.visible); this.passiveContainer.descriptionText?.setVisible(!this.passiveContainer.descriptionText.visible); this.passiveContainer.labelImage.setVisible(!this.passiveContainer.labelImage.visible); + } else if (this.cursor === Page.STATS) { + //Show IVs + this.permStatsContainer.setVisible(!this.permStatsContainer.visible); + this.ivContainer.setVisible(!this.ivContainer.visible); } } else if (button === Button.CANCEL) { if (this.summaryUiMode === SummaryUiMode.LEARN_MOVE) { @@ -877,8 +884,13 @@ export default class SummaryUiHandler extends UiHandler { profileContainer.add(memoText); break; case Page.STATS: - const statsContainer = globalScene.add.container(0, -pageBg.height); - pageContainer.add(statsContainer); + this.statsContainer = globalScene.add.container(0, -pageBg.height); + pageContainer.add(this.statsContainer); + this.permStatsContainer = globalScene.add.container(27, 56); + this.statsContainer.add(this.permStatsContainer); + this.ivContainer = globalScene.add.container(27, 56); + this.statsContainer.add(this.ivContainer); + this.statsContainer.setVisible(true); PERMANENT_STATS.forEach((stat, s) => { const statName = i18next.t(getStatKey(stat)); @@ -887,18 +899,27 @@ export default class SummaryUiHandler extends UiHandler { const natureStatMultiplier = getNatureStatMultiplier(this.pokemon?.getNature()!, s); // TODO: is this bang correct? - const statLabel = addTextObject(27 + 115 * colIndex + (colIndex === 1 ? 5 : 0), 56 + 16 * rowIndex, statName, natureStatMultiplier === 1 ? TextStyle.SUMMARY : natureStatMultiplier > 1 ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE); + const statLabel = addTextObject(115 * colIndex + (colIndex === 1 ? 5 : 0), 16 * rowIndex, statName, natureStatMultiplier === 1 ? TextStyle.SUMMARY : natureStatMultiplier > 1 ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE); + const ivLabel = addTextObject(115 * colIndex + (colIndex === 1 ? 5 : 0), 16 * rowIndex, statName, this.pokemon?.ivs[stat] === 31 ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY); + statLabel.setOrigin(0.5, 0); - statsContainer.add(statLabel); + ivLabel.setOrigin(0.5, 0); + this.permStatsContainer.add(statLabel); + this.ivContainer.add(ivLabel); const statValueText = stat !== Stat.HP ? Utils.formatStat(this.pokemon?.getStat(stat)!) // TODO: is this bang correct? : `${Utils.formatStat(this.pokemon?.hp!, true)}/${Utils.formatStat(this.pokemon?.getMaxHp()!, true)}`; // TODO: are those bangs correct? + const ivText = `${this.pokemon?.ivs[stat]}/31`; - const statValue = addTextObject(120 + 88 * colIndex, 56 + 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT); + const statValue = addTextObject(93 + 88 * colIndex, 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT); statValue.setOrigin(1, 0); - statsContainer.add(statValue); + this.permStatsContainer.add(statValue); + const ivValue = addTextObject(93 + 88 * colIndex, 16 * rowIndex, ivText, TextStyle.WINDOW_ALT); + ivValue.setOrigin(1, 0); + this.ivContainer.add(ivValue); }); + this.ivContainer.setVisible(false); const itemModifiers = (globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.pokemon?.id, this.playerParty) as PokemonHeldItemModifier[]) @@ -908,7 +929,7 @@ export default class SummaryUiHandler extends UiHandler { const icon = item.getIcon(true); icon.setPosition((i % 17) * 12 + 3, 14 * Math.floor(i / 17) + 15); - statsContainer.add(icon); + this.statsContainer.add(icon); icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 32), Phaser.Geom.Rectangle.Contains); icon.on("pointerover", () => globalScene.ui.showTooltip(item.type.name, item.type.getDescription(), true)); @@ -924,26 +945,26 @@ export default class SummaryUiHandler extends UiHandler { const expLabel = addTextObject(6, 112, i18next.t("pokemonSummary:expPoints"), TextStyle.SUMMARY); expLabel.setOrigin(0, 0); - statsContainer.add(expLabel); + this.statsContainer.add(expLabel); const nextLvExpLabel = addTextObject(6, 128, i18next.t("pokemonSummary:nextLv"), TextStyle.SUMMARY); nextLvExpLabel.setOrigin(0, 0); - statsContainer.add(nextLvExpLabel); + this.statsContainer.add(nextLvExpLabel); const expText = addTextObject(208, 112, pkmExp.toString(), TextStyle.WINDOW_ALT); expText.setOrigin(1, 0); - statsContainer.add(expText); + this.statsContainer.add(expText); const nextLvExp = pkmLvl < globalScene.getMaxExpLevel() ? getLevelTotalExp(pkmLvl + 1, pkmSpeciesGrowthRate) - pkmExp : 0; const nextLvExpText = addTextObject(208, 128, nextLvExp.toString(), TextStyle.WINDOW_ALT); nextLvExpText.setOrigin(1, 0); - statsContainer.add(nextLvExpText); + this.statsContainer.add(nextLvExpText); const expOverlay = globalScene.add.image(140, 145, "summary_stats_overlay_exp"); expOverlay.setOrigin(0, 0); - statsContainer.add(expOverlay); + this.statsContainer.add(expOverlay); const expMaskRect = globalScene.make.graphics({}); expMaskRect.setScale(6); @@ -954,6 +975,11 @@ export default class SummaryUiHandler extends UiHandler { const expMask = expMaskRect.createGeometryMask(); expOverlay.setMask(expMask); + this.abilityPrompt = globalScene.add.image(0, 0, !globalScene.inputController?.gamepadSupport ? "summary_profile_prompt_z" : "summary_profile_prompt_a"); + this.abilityPrompt.setPosition(8, 47); + this.abilityPrompt.setVisible(true); + this.abilityPrompt.setOrigin(0, 0); + this.statsContainer.add(this.abilityPrompt); break; case Page.MOVES: this.movesContainer = globalScene.add.container(5, -pageBg.height + 26); From 612e6a25c124876e1029c6e4160cbbc6866067ce Mon Sep 17 00:00:00 2001 From: damocleas Date: Sun, 9 Feb 2025 04:13:06 -0500 Subject: [PATCH 05/18] Revert "[UI/UX] Show IVs on stats page of summary (#5172)" (#5284) This reverts commit 366c88517c36ae46fd00f540f3ae7f7b860e6234. --- src/ui/summary-ui-handler.ts | 50 +++++++++--------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index cf5d40bc006..3305b3f7aa2 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -96,9 +96,6 @@ export default class SummaryUiHandler extends UiHandler { private friendshipText: Phaser.GameObjects.Text; private friendshipIcon: Phaser.GameObjects.Sprite; private friendshipOverlay: Phaser.GameObjects.Sprite; - private permStatsContainer: Phaser.GameObjects.Container; - private ivContainer: Phaser.GameObjects.Container; - private statsContainer: Phaser.GameObjects.Container; private descriptionScrollTween: Phaser.Tweens.Tween | null; private moveCursorBlinkTimer: Phaser.Time.TimerEvent | null; @@ -537,10 +534,6 @@ export default class SummaryUiHandler extends UiHandler { this.passiveContainer.nameText?.setVisible(!this.passiveContainer.descriptionText?.visible); this.passiveContainer.descriptionText?.setVisible(!this.passiveContainer.descriptionText.visible); this.passiveContainer.labelImage.setVisible(!this.passiveContainer.labelImage.visible); - } else if (this.cursor === Page.STATS) { - //Show IVs - this.permStatsContainer.setVisible(!this.permStatsContainer.visible); - this.ivContainer.setVisible(!this.ivContainer.visible); } } else if (button === Button.CANCEL) { if (this.summaryUiMode === SummaryUiMode.LEARN_MOVE) { @@ -884,13 +877,8 @@ export default class SummaryUiHandler extends UiHandler { profileContainer.add(memoText); break; case Page.STATS: - this.statsContainer = globalScene.add.container(0, -pageBg.height); - pageContainer.add(this.statsContainer); - this.permStatsContainer = globalScene.add.container(27, 56); - this.statsContainer.add(this.permStatsContainer); - this.ivContainer = globalScene.add.container(27, 56); - this.statsContainer.add(this.ivContainer); - this.statsContainer.setVisible(true); + const statsContainer = globalScene.add.container(0, -pageBg.height); + pageContainer.add(statsContainer); PERMANENT_STATS.forEach((stat, s) => { const statName = i18next.t(getStatKey(stat)); @@ -899,27 +887,18 @@ export default class SummaryUiHandler extends UiHandler { const natureStatMultiplier = getNatureStatMultiplier(this.pokemon?.getNature()!, s); // TODO: is this bang correct? - const statLabel = addTextObject(115 * colIndex + (colIndex === 1 ? 5 : 0), 16 * rowIndex, statName, natureStatMultiplier === 1 ? TextStyle.SUMMARY : natureStatMultiplier > 1 ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE); - const ivLabel = addTextObject(115 * colIndex + (colIndex === 1 ? 5 : 0), 16 * rowIndex, statName, this.pokemon?.ivs[stat] === 31 ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY); - + const statLabel = addTextObject(27 + 115 * colIndex + (colIndex === 1 ? 5 : 0), 56 + 16 * rowIndex, statName, natureStatMultiplier === 1 ? TextStyle.SUMMARY : natureStatMultiplier > 1 ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE); statLabel.setOrigin(0.5, 0); - ivLabel.setOrigin(0.5, 0); - this.permStatsContainer.add(statLabel); - this.ivContainer.add(ivLabel); + statsContainer.add(statLabel); const statValueText = stat !== Stat.HP ? Utils.formatStat(this.pokemon?.getStat(stat)!) // TODO: is this bang correct? : `${Utils.formatStat(this.pokemon?.hp!, true)}/${Utils.formatStat(this.pokemon?.getMaxHp()!, true)}`; // TODO: are those bangs correct? - const ivText = `${this.pokemon?.ivs[stat]}/31`; - const statValue = addTextObject(93 + 88 * colIndex, 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT); + const statValue = addTextObject(120 + 88 * colIndex, 56 + 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT); statValue.setOrigin(1, 0); - this.permStatsContainer.add(statValue); - const ivValue = addTextObject(93 + 88 * colIndex, 16 * rowIndex, ivText, TextStyle.WINDOW_ALT); - ivValue.setOrigin(1, 0); - this.ivContainer.add(ivValue); + statsContainer.add(statValue); }); - this.ivContainer.setVisible(false); const itemModifiers = (globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.pokemon?.id, this.playerParty) as PokemonHeldItemModifier[]) @@ -929,7 +908,7 @@ export default class SummaryUiHandler extends UiHandler { const icon = item.getIcon(true); icon.setPosition((i % 17) * 12 + 3, 14 * Math.floor(i / 17) + 15); - this.statsContainer.add(icon); + statsContainer.add(icon); icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 32), Phaser.Geom.Rectangle.Contains); icon.on("pointerover", () => globalScene.ui.showTooltip(item.type.name, item.type.getDescription(), true)); @@ -945,26 +924,26 @@ export default class SummaryUiHandler extends UiHandler { const expLabel = addTextObject(6, 112, i18next.t("pokemonSummary:expPoints"), TextStyle.SUMMARY); expLabel.setOrigin(0, 0); - this.statsContainer.add(expLabel); + statsContainer.add(expLabel); const nextLvExpLabel = addTextObject(6, 128, i18next.t("pokemonSummary:nextLv"), TextStyle.SUMMARY); nextLvExpLabel.setOrigin(0, 0); - this.statsContainer.add(nextLvExpLabel); + statsContainer.add(nextLvExpLabel); const expText = addTextObject(208, 112, pkmExp.toString(), TextStyle.WINDOW_ALT); expText.setOrigin(1, 0); - this.statsContainer.add(expText); + statsContainer.add(expText); const nextLvExp = pkmLvl < globalScene.getMaxExpLevel() ? getLevelTotalExp(pkmLvl + 1, pkmSpeciesGrowthRate) - pkmExp : 0; const nextLvExpText = addTextObject(208, 128, nextLvExp.toString(), TextStyle.WINDOW_ALT); nextLvExpText.setOrigin(1, 0); - this.statsContainer.add(nextLvExpText); + statsContainer.add(nextLvExpText); const expOverlay = globalScene.add.image(140, 145, "summary_stats_overlay_exp"); expOverlay.setOrigin(0, 0); - this.statsContainer.add(expOverlay); + statsContainer.add(expOverlay); const expMaskRect = globalScene.make.graphics({}); expMaskRect.setScale(6); @@ -975,11 +954,6 @@ export default class SummaryUiHandler extends UiHandler { const expMask = expMaskRect.createGeometryMask(); expOverlay.setMask(expMask); - this.abilityPrompt = globalScene.add.image(0, 0, !globalScene.inputController?.gamepadSupport ? "summary_profile_prompt_z" : "summary_profile_prompt_a"); - this.abilityPrompt.setPosition(8, 47); - this.abilityPrompt.setVisible(true); - this.abilityPrompt.setOrigin(0, 0); - this.statsContainer.add(this.abilityPrompt); break; case Page.MOVES: this.movesContainer = globalScene.add.container(5, -pageBg.height + 26); From f77bfc8367b87f28dd336b538156c91e9e94e547 Mon Sep 17 00:00:00 2001 From: damocleas Date: Mon, 10 Feb 2025 00:40:26 -0500 Subject: [PATCH 06/18] Reapply [UI/UX] Show IVs on stats page of summary (#5172) #5291 This reverts commit 612e6a25c124876e1029c6e4160cbbc6866067ce. --- src/ui/summary-ui-handler.ts | 50 +++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 3305b3f7aa2..cf5d40bc006 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -96,6 +96,9 @@ export default class SummaryUiHandler extends UiHandler { private friendshipText: Phaser.GameObjects.Text; private friendshipIcon: Phaser.GameObjects.Sprite; private friendshipOverlay: Phaser.GameObjects.Sprite; + private permStatsContainer: Phaser.GameObjects.Container; + private ivContainer: Phaser.GameObjects.Container; + private statsContainer: Phaser.GameObjects.Container; private descriptionScrollTween: Phaser.Tweens.Tween | null; private moveCursorBlinkTimer: Phaser.Time.TimerEvent | null; @@ -534,6 +537,10 @@ export default class SummaryUiHandler extends UiHandler { this.passiveContainer.nameText?.setVisible(!this.passiveContainer.descriptionText?.visible); this.passiveContainer.descriptionText?.setVisible(!this.passiveContainer.descriptionText.visible); this.passiveContainer.labelImage.setVisible(!this.passiveContainer.labelImage.visible); + } else if (this.cursor === Page.STATS) { + //Show IVs + this.permStatsContainer.setVisible(!this.permStatsContainer.visible); + this.ivContainer.setVisible(!this.ivContainer.visible); } } else if (button === Button.CANCEL) { if (this.summaryUiMode === SummaryUiMode.LEARN_MOVE) { @@ -877,8 +884,13 @@ export default class SummaryUiHandler extends UiHandler { profileContainer.add(memoText); break; case Page.STATS: - const statsContainer = globalScene.add.container(0, -pageBg.height); - pageContainer.add(statsContainer); + this.statsContainer = globalScene.add.container(0, -pageBg.height); + pageContainer.add(this.statsContainer); + this.permStatsContainer = globalScene.add.container(27, 56); + this.statsContainer.add(this.permStatsContainer); + this.ivContainer = globalScene.add.container(27, 56); + this.statsContainer.add(this.ivContainer); + this.statsContainer.setVisible(true); PERMANENT_STATS.forEach((stat, s) => { const statName = i18next.t(getStatKey(stat)); @@ -887,18 +899,27 @@ export default class SummaryUiHandler extends UiHandler { const natureStatMultiplier = getNatureStatMultiplier(this.pokemon?.getNature()!, s); // TODO: is this bang correct? - const statLabel = addTextObject(27 + 115 * colIndex + (colIndex === 1 ? 5 : 0), 56 + 16 * rowIndex, statName, natureStatMultiplier === 1 ? TextStyle.SUMMARY : natureStatMultiplier > 1 ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE); + const statLabel = addTextObject(115 * colIndex + (colIndex === 1 ? 5 : 0), 16 * rowIndex, statName, natureStatMultiplier === 1 ? TextStyle.SUMMARY : natureStatMultiplier > 1 ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE); + const ivLabel = addTextObject(115 * colIndex + (colIndex === 1 ? 5 : 0), 16 * rowIndex, statName, this.pokemon?.ivs[stat] === 31 ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY); + statLabel.setOrigin(0.5, 0); - statsContainer.add(statLabel); + ivLabel.setOrigin(0.5, 0); + this.permStatsContainer.add(statLabel); + this.ivContainer.add(ivLabel); const statValueText = stat !== Stat.HP ? Utils.formatStat(this.pokemon?.getStat(stat)!) // TODO: is this bang correct? : `${Utils.formatStat(this.pokemon?.hp!, true)}/${Utils.formatStat(this.pokemon?.getMaxHp()!, true)}`; // TODO: are those bangs correct? + const ivText = `${this.pokemon?.ivs[stat]}/31`; - const statValue = addTextObject(120 + 88 * colIndex, 56 + 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT); + const statValue = addTextObject(93 + 88 * colIndex, 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT); statValue.setOrigin(1, 0); - statsContainer.add(statValue); + this.permStatsContainer.add(statValue); + const ivValue = addTextObject(93 + 88 * colIndex, 16 * rowIndex, ivText, TextStyle.WINDOW_ALT); + ivValue.setOrigin(1, 0); + this.ivContainer.add(ivValue); }); + this.ivContainer.setVisible(false); const itemModifiers = (globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.pokemon?.id, this.playerParty) as PokemonHeldItemModifier[]) @@ -908,7 +929,7 @@ export default class SummaryUiHandler extends UiHandler { const icon = item.getIcon(true); icon.setPosition((i % 17) * 12 + 3, 14 * Math.floor(i / 17) + 15); - statsContainer.add(icon); + this.statsContainer.add(icon); icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 32), Phaser.Geom.Rectangle.Contains); icon.on("pointerover", () => globalScene.ui.showTooltip(item.type.name, item.type.getDescription(), true)); @@ -924,26 +945,26 @@ export default class SummaryUiHandler extends UiHandler { const expLabel = addTextObject(6, 112, i18next.t("pokemonSummary:expPoints"), TextStyle.SUMMARY); expLabel.setOrigin(0, 0); - statsContainer.add(expLabel); + this.statsContainer.add(expLabel); const nextLvExpLabel = addTextObject(6, 128, i18next.t("pokemonSummary:nextLv"), TextStyle.SUMMARY); nextLvExpLabel.setOrigin(0, 0); - statsContainer.add(nextLvExpLabel); + this.statsContainer.add(nextLvExpLabel); const expText = addTextObject(208, 112, pkmExp.toString(), TextStyle.WINDOW_ALT); expText.setOrigin(1, 0); - statsContainer.add(expText); + this.statsContainer.add(expText); const nextLvExp = pkmLvl < globalScene.getMaxExpLevel() ? getLevelTotalExp(pkmLvl + 1, pkmSpeciesGrowthRate) - pkmExp : 0; const nextLvExpText = addTextObject(208, 128, nextLvExp.toString(), TextStyle.WINDOW_ALT); nextLvExpText.setOrigin(1, 0); - statsContainer.add(nextLvExpText); + this.statsContainer.add(nextLvExpText); const expOverlay = globalScene.add.image(140, 145, "summary_stats_overlay_exp"); expOverlay.setOrigin(0, 0); - statsContainer.add(expOverlay); + this.statsContainer.add(expOverlay); const expMaskRect = globalScene.make.graphics({}); expMaskRect.setScale(6); @@ -954,6 +975,11 @@ export default class SummaryUiHandler extends UiHandler { const expMask = expMaskRect.createGeometryMask(); expOverlay.setMask(expMask); + this.abilityPrompt = globalScene.add.image(0, 0, !globalScene.inputController?.gamepadSupport ? "summary_profile_prompt_z" : "summary_profile_prompt_a"); + this.abilityPrompt.setPosition(8, 47); + this.abilityPrompt.setVisible(true); + this.abilityPrompt.setOrigin(0, 0); + this.statsContainer.add(this.abilityPrompt); break; case Page.MOVES: this.movesContainer = globalScene.add.container(5, -pageBg.height + 26); From 3daa9054f39f26e49c6cc9a3813d9649045a208b Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Mon, 10 Feb 2025 01:30:39 -0500 Subject: [PATCH 07/18] [Misc] Add data for theoretical Valentine's event (#5244) * Add Valentines event data * Event ends Feb 21 not March 21 * Event starts Feb 11 12:00 UTC for testing on beta * Oops I meant February 10 * Add Luvdisc +3 Luck Boost * Added Applin to round out the total pokemon and because I know people are going to be confused why it isn't here if they've actually read in gen 8 (any readers in chat?) --------- Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: damocleas --- .../images/events/valentines2025event-de.png | Bin 0 -> 3267 bytes .../images/events/valentines2025event-en.png | Bin 0 -> 3267 bytes .../events/valentines2025event-es-ES.png | Bin 0 -> 3267 bytes .../images/events/valentines2025event-fr.png | Bin 0 -> 3267 bytes .../images/events/valentines2025event-it.png | Bin 0 -> 3267 bytes .../images/events/valentines2025event-ja.png | Bin 0 -> 3267 bytes .../images/events/valentines2025event-ko.png | Bin 0 -> 3267 bytes .../events/valentines2025event-pt-BR.png | Bin 0 -> 3267 bytes .../events/valentines2025event-zh-CN.png | Bin 0 -> 3267 bytes .../utils/encounter-phase-utils.ts | 5 +++ src/field/pokemon.ts | 8 +++- src/loading-scene.ts | 4 +- src/modifier/modifier-type.ts | 15 +++++-- src/timed-event-manager.ts | 40 ++++++++++++++++++ 14 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 public/images/events/valentines2025event-de.png create mode 100644 public/images/events/valentines2025event-en.png create mode 100644 public/images/events/valentines2025event-es-ES.png create mode 100644 public/images/events/valentines2025event-fr.png create mode 100644 public/images/events/valentines2025event-it.png create mode 100644 public/images/events/valentines2025event-ja.png create mode 100644 public/images/events/valentines2025event-ko.png create mode 100644 public/images/events/valentines2025event-pt-BR.png create mode 100644 public/images/events/valentines2025event-zh-CN.png diff --git a/public/images/events/valentines2025event-de.png b/public/images/events/valentines2025event-de.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec3bfe704ba2a2e3882ae80fbcbaa71d14b1069 GIT binary patch literal 3267 zcmeHKdrVVj6#q(jE1;-xjv{Wmp>aCR630_1A`9M-whI~`j95*`tY}LC6_iI2+?aC$ zxy>Z8Jj$FBhdibgAp(W@U=0!K5Rfc2x9{GuKz7lpa~*$* z*eRr0gVUw}{FW+tVU9sX#A>>vI{|i^J?>RpbxGgH!4Ld^kdIV|BtPctyZB*If;{>R zq49R5H`3lTlNO}U^1;U@*jA7Ui=Pbx09LNrh9W(3^7>?c3ePq&T$UL`&-X!TaS`jL zuP;vYDR(SvKtUAan&+$2FP6+>>V^9gIfs+A>GbKGbPir}Z5%b8BBG`9)00ftOW7U+ zT_D$7gB!tN9-u6Zs9cSb4=Y5H-#+_x^ij_XAN4d=%w^FVDb3LuQ(n4>2u&y73erjW zsb?USv&*-Pg|eI5{-;=)s}ZmhVX$O=+U`AK0Sm2>v9biDA6yeeVZf2HsEct%8#=S-_-E=gCH347AOg@5BjXU-Mlz*tk}b zBLYeT{(A&UJ{&8O{QYgic^;&uV$HJiK5_I$%7O~#XKrwmc!+>w1&N|WW5FK4%B_)D zG)5ESE7Fc6e5{GQ!kUEwfEKOAu*^7exUL2#UR|G5JD?!QxN8ZB+Fr-TTf7cQZF>S8 z=+n8n2N)6^x9BQl*y6psSTCe;Q3dS=LcQ$#HI#ffRwVi5*PFKb_WRt$FOw@|h~7wK z>UN!Wk5ni-U!P_Lsis6@+6B?~O@kJVQM8M@Th!W6#hq>bV^9Flq7~@Z8q|$v6J%Hy zw@2nS1S1tg#yxqA>ZG%LBA-)ha;!>pg&-@HC;_v&6;b^HHa?fQbsz|7+-gKYl?=^G zwo^DtJ{&7E`P>|VxK<>VZRWdoKR5@&MvN#8N&T%QCXKgKs?iDp*W`!?fR)?dKxs^A zpRz#KdLj7uv3Lz^qd2r^6I*9xnfe#r_$_%A+v@?0v==gN-i=eUg_yrI$++@_jzvG< z**wPBz(>`!DjxAlcc?xUX5@>P_nD^*&gLDK9XQu;HQ< z>ZfXp_{L(cyTM=$q1NOdz5rIPJe|^*_lLbe$6JnA1T*gtN6G#Xrh*eDxnV7h s@Q95x4tEN)SPeBoX?cqO|3sM;RJp8EG?=c$nBP(GFL|NmA%`>m0)^P=#{d8T literal 0 HcmV?d00001 diff --git a/public/images/events/valentines2025event-en.png b/public/images/events/valentines2025event-en.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec3bfe704ba2a2e3882ae80fbcbaa71d14b1069 GIT binary patch literal 3267 zcmeHKdrVVj6#q(jE1;-xjv{Wmp>aCR630_1A`9M-whI~`j95*`tY}LC6_iI2+?aC$ zxy>Z8Jj$FBhdibgAp(W@U=0!K5Rfc2x9{GuKz7lpa~*$* z*eRr0gVUw}{FW+tVU9sX#A>>vI{|i^J?>RpbxGgH!4Ld^kdIV|BtPctyZB*If;{>R zq49R5H`3lTlNO}U^1;U@*jA7Ui=Pbx09LNrh9W(3^7>?c3ePq&T$UL`&-X!TaS`jL zuP;vYDR(SvKtUAan&+$2FP6+>>V^9gIfs+A>GbKGbPir}Z5%b8BBG`9)00ftOW7U+ zT_D$7gB!tN9-u6Zs9cSb4=Y5H-#+_x^ij_XAN4d=%w^FVDb3LuQ(n4>2u&y73erjW zsb?USv&*-Pg|eI5{-;=)s}ZmhVX$O=+U`AK0Sm2>v9biDA6yeeVZf2HsEct%8#=S-_-E=gCH347AOg@5BjXU-Mlz*tk}b zBLYeT{(A&UJ{&8O{QYgic^;&uV$HJiK5_I$%7O~#XKrwmc!+>w1&N|WW5FK4%B_)D zG)5ESE7Fc6e5{GQ!kUEwfEKOAu*^7exUL2#UR|G5JD?!QxN8ZB+Fr-TTf7cQZF>S8 z=+n8n2N)6^x9BQl*y6psSTCe;Q3dS=LcQ$#HI#ffRwVi5*PFKb_WRt$FOw@|h~7wK z>UN!Wk5ni-U!P_Lsis6@+6B?~O@kJVQM8M@Th!W6#hq>bV^9Flq7~@Z8q|$v6J%Hy zw@2nS1S1tg#yxqA>ZG%LBA-)ha;!>pg&-@HC;_v&6;b^HHa?fQbsz|7+-gKYl?=^G zwo^DtJ{&7E`P>|VxK<>VZRWdoKR5@&MvN#8N&T%QCXKgKs?iDp*W`!?fR)?dKxs^A zpRz#KdLj7uv3Lz^qd2r^6I*9xnfe#r_$_%A+v@?0v==gN-i=eUg_yrI$++@_jzvG< z**wPBz(>`!DjxAlcc?xUX5@>P_nD^*&gLDK9XQu;HQ< z>ZfXp_{L(cyTM=$q1NOdz5rIPJe|^*_lLbe$6JnA1T*gtN6G#Xrh*eDxnV7h s@Q95x4tEN)SPeBoX?cqO|3sM;RJp8EG?=c$nBP(GFL|NmA%`>m0)^P=#{d8T literal 0 HcmV?d00001 diff --git a/public/images/events/valentines2025event-es-ES.png b/public/images/events/valentines2025event-es-ES.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec3bfe704ba2a2e3882ae80fbcbaa71d14b1069 GIT binary patch literal 3267 zcmeHKdrVVj6#q(jE1;-xjv{Wmp>aCR630_1A`9M-whI~`j95*`tY}LC6_iI2+?aC$ zxy>Z8Jj$FBhdibgAp(W@U=0!K5Rfc2x9{GuKz7lpa~*$* z*eRr0gVUw}{FW+tVU9sX#A>>vI{|i^J?>RpbxGgH!4Ld^kdIV|BtPctyZB*If;{>R zq49R5H`3lTlNO}U^1;U@*jA7Ui=Pbx09LNrh9W(3^7>?c3ePq&T$UL`&-X!TaS`jL zuP;vYDR(SvKtUAan&+$2FP6+>>V^9gIfs+A>GbKGbPir}Z5%b8BBG`9)00ftOW7U+ zT_D$7gB!tN9-u6Zs9cSb4=Y5H-#+_x^ij_XAN4d=%w^FVDb3LuQ(n4>2u&y73erjW zsb?USv&*-Pg|eI5{-;=)s}ZmhVX$O=+U`AK0Sm2>v9biDA6yeeVZf2HsEct%8#=S-_-E=gCH347AOg@5BjXU-Mlz*tk}b zBLYeT{(A&UJ{&8O{QYgic^;&uV$HJiK5_I$%7O~#XKrwmc!+>w1&N|WW5FK4%B_)D zG)5ESE7Fc6e5{GQ!kUEwfEKOAu*^7exUL2#UR|G5JD?!QxN8ZB+Fr-TTf7cQZF>S8 z=+n8n2N)6^x9BQl*y6psSTCe;Q3dS=LcQ$#HI#ffRwVi5*PFKb_WRt$FOw@|h~7wK z>UN!Wk5ni-U!P_Lsis6@+6B?~O@kJVQM8M@Th!W6#hq>bV^9Flq7~@Z8q|$v6J%Hy zw@2nS1S1tg#yxqA>ZG%LBA-)ha;!>pg&-@HC;_v&6;b^HHa?fQbsz|7+-gKYl?=^G zwo^DtJ{&7E`P>|VxK<>VZRWdoKR5@&MvN#8N&T%QCXKgKs?iDp*W`!?fR)?dKxs^A zpRz#KdLj7uv3Lz^qd2r^6I*9xnfe#r_$_%A+v@?0v==gN-i=eUg_yrI$++@_jzvG< z**wPBz(>`!DjxAlcc?xUX5@>P_nD^*&gLDK9XQu;HQ< z>ZfXp_{L(cyTM=$q1NOdz5rIPJe|^*_lLbe$6JnA1T*gtN6G#Xrh*eDxnV7h s@Q95x4tEN)SPeBoX?cqO|3sM;RJp8EG?=c$nBP(GFL|NmA%`>m0)^P=#{d8T literal 0 HcmV?d00001 diff --git a/public/images/events/valentines2025event-fr.png b/public/images/events/valentines2025event-fr.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec3bfe704ba2a2e3882ae80fbcbaa71d14b1069 GIT binary patch literal 3267 zcmeHKdrVVj6#q(jE1;-xjv{Wmp>aCR630_1A`9M-whI~`j95*`tY}LC6_iI2+?aC$ zxy>Z8Jj$FBhdibgAp(W@U=0!K5Rfc2x9{GuKz7lpa~*$* z*eRr0gVUw}{FW+tVU9sX#A>>vI{|i^J?>RpbxGgH!4Ld^kdIV|BtPctyZB*If;{>R zq49R5H`3lTlNO}U^1;U@*jA7Ui=Pbx09LNrh9W(3^7>?c3ePq&T$UL`&-X!TaS`jL zuP;vYDR(SvKtUAan&+$2FP6+>>V^9gIfs+A>GbKGbPir}Z5%b8BBG`9)00ftOW7U+ zT_D$7gB!tN9-u6Zs9cSb4=Y5H-#+_x^ij_XAN4d=%w^FVDb3LuQ(n4>2u&y73erjW zsb?USv&*-Pg|eI5{-;=)s}ZmhVX$O=+U`AK0Sm2>v9biDA6yeeVZf2HsEct%8#=S-_-E=gCH347AOg@5BjXU-Mlz*tk}b zBLYeT{(A&UJ{&8O{QYgic^;&uV$HJiK5_I$%7O~#XKrwmc!+>w1&N|WW5FK4%B_)D zG)5ESE7Fc6e5{GQ!kUEwfEKOAu*^7exUL2#UR|G5JD?!QxN8ZB+Fr-TTf7cQZF>S8 z=+n8n2N)6^x9BQl*y6psSTCe;Q3dS=LcQ$#HI#ffRwVi5*PFKb_WRt$FOw@|h~7wK z>UN!Wk5ni-U!P_Lsis6@+6B?~O@kJVQM8M@Th!W6#hq>bV^9Flq7~@Z8q|$v6J%Hy zw@2nS1S1tg#yxqA>ZG%LBA-)ha;!>pg&-@HC;_v&6;b^HHa?fQbsz|7+-gKYl?=^G zwo^DtJ{&7E`P>|VxK<>VZRWdoKR5@&MvN#8N&T%QCXKgKs?iDp*W`!?fR)?dKxs^A zpRz#KdLj7uv3Lz^qd2r^6I*9xnfe#r_$_%A+v@?0v==gN-i=eUg_yrI$++@_jzvG< z**wPBz(>`!DjxAlcc?xUX5@>P_nD^*&gLDK9XQu;HQ< z>ZfXp_{L(cyTM=$q1NOdz5rIPJe|^*_lLbe$6JnA1T*gtN6G#Xrh*eDxnV7h s@Q95x4tEN)SPeBoX?cqO|3sM;RJp8EG?=c$nBP(GFL|NmA%`>m0)^P=#{d8T literal 0 HcmV?d00001 diff --git a/public/images/events/valentines2025event-it.png b/public/images/events/valentines2025event-it.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec3bfe704ba2a2e3882ae80fbcbaa71d14b1069 GIT binary patch literal 3267 zcmeHKdrVVj6#q(jE1;-xjv{Wmp>aCR630_1A`9M-whI~`j95*`tY}LC6_iI2+?aC$ zxy>Z8Jj$FBhdibgAp(W@U=0!K5Rfc2x9{GuKz7lpa~*$* z*eRr0gVUw}{FW+tVU9sX#A>>vI{|i^J?>RpbxGgH!4Ld^kdIV|BtPctyZB*If;{>R zq49R5H`3lTlNO}U^1;U@*jA7Ui=Pbx09LNrh9W(3^7>?c3ePq&T$UL`&-X!TaS`jL zuP;vYDR(SvKtUAan&+$2FP6+>>V^9gIfs+A>GbKGbPir}Z5%b8BBG`9)00ftOW7U+ zT_D$7gB!tN9-u6Zs9cSb4=Y5H-#+_x^ij_XAN4d=%w^FVDb3LuQ(n4>2u&y73erjW zsb?USv&*-Pg|eI5{-;=)s}ZmhVX$O=+U`AK0Sm2>v9biDA6yeeVZf2HsEct%8#=S-_-E=gCH347AOg@5BjXU-Mlz*tk}b zBLYeT{(A&UJ{&8O{QYgic^;&uV$HJiK5_I$%7O~#XKrwmc!+>w1&N|WW5FK4%B_)D zG)5ESE7Fc6e5{GQ!kUEwfEKOAu*^7exUL2#UR|G5JD?!QxN8ZB+Fr-TTf7cQZF>S8 z=+n8n2N)6^x9BQl*y6psSTCe;Q3dS=LcQ$#HI#ffRwVi5*PFKb_WRt$FOw@|h~7wK z>UN!Wk5ni-U!P_Lsis6@+6B?~O@kJVQM8M@Th!W6#hq>bV^9Flq7~@Z8q|$v6J%Hy zw@2nS1S1tg#yxqA>ZG%LBA-)ha;!>pg&-@HC;_v&6;b^HHa?fQbsz|7+-gKYl?=^G zwo^DtJ{&7E`P>|VxK<>VZRWdoKR5@&MvN#8N&T%QCXKgKs?iDp*W`!?fR)?dKxs^A zpRz#KdLj7uv3Lz^qd2r^6I*9xnfe#r_$_%A+v@?0v==gN-i=eUg_yrI$++@_jzvG< z**wPBz(>`!DjxAlcc?xUX5@>P_nD^*&gLDK9XQu;HQ< z>ZfXp_{L(cyTM=$q1NOdz5rIPJe|^*_lLbe$6JnA1T*gtN6G#Xrh*eDxnV7h s@Q95x4tEN)SPeBoX?cqO|3sM;RJp8EG?=c$nBP(GFL|NmA%`>m0)^P=#{d8T literal 0 HcmV?d00001 diff --git a/public/images/events/valentines2025event-ja.png b/public/images/events/valentines2025event-ja.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec3bfe704ba2a2e3882ae80fbcbaa71d14b1069 GIT binary patch literal 3267 zcmeHKdrVVj6#q(jE1;-xjv{Wmp>aCR630_1A`9M-whI~`j95*`tY}LC6_iI2+?aC$ zxy>Z8Jj$FBhdibgAp(W@U=0!K5Rfc2x9{GuKz7lpa~*$* z*eRr0gVUw}{FW+tVU9sX#A>>vI{|i^J?>RpbxGgH!4Ld^kdIV|BtPctyZB*If;{>R zq49R5H`3lTlNO}U^1;U@*jA7Ui=Pbx09LNrh9W(3^7>?c3ePq&T$UL`&-X!TaS`jL zuP;vYDR(SvKtUAan&+$2FP6+>>V^9gIfs+A>GbKGbPir}Z5%b8BBG`9)00ftOW7U+ zT_D$7gB!tN9-u6Zs9cSb4=Y5H-#+_x^ij_XAN4d=%w^FVDb3LuQ(n4>2u&y73erjW zsb?USv&*-Pg|eI5{-;=)s}ZmhVX$O=+U`AK0Sm2>v9biDA6yeeVZf2HsEct%8#=S-_-E=gCH347AOg@5BjXU-Mlz*tk}b zBLYeT{(A&UJ{&8O{QYgic^;&uV$HJiK5_I$%7O~#XKrwmc!+>w1&N|WW5FK4%B_)D zG)5ESE7Fc6e5{GQ!kUEwfEKOAu*^7exUL2#UR|G5JD?!QxN8ZB+Fr-TTf7cQZF>S8 z=+n8n2N)6^x9BQl*y6psSTCe;Q3dS=LcQ$#HI#ffRwVi5*PFKb_WRt$FOw@|h~7wK z>UN!Wk5ni-U!P_Lsis6@+6B?~O@kJVQM8M@Th!W6#hq>bV^9Flq7~@Z8q|$v6J%Hy zw@2nS1S1tg#yxqA>ZG%LBA-)ha;!>pg&-@HC;_v&6;b^HHa?fQbsz|7+-gKYl?=^G zwo^DtJ{&7E`P>|VxK<>VZRWdoKR5@&MvN#8N&T%QCXKgKs?iDp*W`!?fR)?dKxs^A zpRz#KdLj7uv3Lz^qd2r^6I*9xnfe#r_$_%A+v@?0v==gN-i=eUg_yrI$++@_jzvG< z**wPBz(>`!DjxAlcc?xUX5@>P_nD^*&gLDK9XQu;HQ< z>ZfXp_{L(cyTM=$q1NOdz5rIPJe|^*_lLbe$6JnA1T*gtN6G#Xrh*eDxnV7h s@Q95x4tEN)SPeBoX?cqO|3sM;RJp8EG?=c$nBP(GFL|NmA%`>m0)^P=#{d8T literal 0 HcmV?d00001 diff --git a/public/images/events/valentines2025event-ko.png b/public/images/events/valentines2025event-ko.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec3bfe704ba2a2e3882ae80fbcbaa71d14b1069 GIT binary patch literal 3267 zcmeHKdrVVj6#q(jE1;-xjv{Wmp>aCR630_1A`9M-whI~`j95*`tY}LC6_iI2+?aC$ zxy>Z8Jj$FBhdibgAp(W@U=0!K5Rfc2x9{GuKz7lpa~*$* z*eRr0gVUw}{FW+tVU9sX#A>>vI{|i^J?>RpbxGgH!4Ld^kdIV|BtPctyZB*If;{>R zq49R5H`3lTlNO}U^1;U@*jA7Ui=Pbx09LNrh9W(3^7>?c3ePq&T$UL`&-X!TaS`jL zuP;vYDR(SvKtUAan&+$2FP6+>>V^9gIfs+A>GbKGbPir}Z5%b8BBG`9)00ftOW7U+ zT_D$7gB!tN9-u6Zs9cSb4=Y5H-#+_x^ij_XAN4d=%w^FVDb3LuQ(n4>2u&y73erjW zsb?USv&*-Pg|eI5{-;=)s}ZmhVX$O=+U`AK0Sm2>v9biDA6yeeVZf2HsEct%8#=S-_-E=gCH347AOg@5BjXU-Mlz*tk}b zBLYeT{(A&UJ{&8O{QYgic^;&uV$HJiK5_I$%7O~#XKrwmc!+>w1&N|WW5FK4%B_)D zG)5ESE7Fc6e5{GQ!kUEwfEKOAu*^7exUL2#UR|G5JD?!QxN8ZB+Fr-TTf7cQZF>S8 z=+n8n2N)6^x9BQl*y6psSTCe;Q3dS=LcQ$#HI#ffRwVi5*PFKb_WRt$FOw@|h~7wK z>UN!Wk5ni-U!P_Lsis6@+6B?~O@kJVQM8M@Th!W6#hq>bV^9Flq7~@Z8q|$v6J%Hy zw@2nS1S1tg#yxqA>ZG%LBA-)ha;!>pg&-@HC;_v&6;b^HHa?fQbsz|7+-gKYl?=^G zwo^DtJ{&7E`P>|VxK<>VZRWdoKR5@&MvN#8N&T%QCXKgKs?iDp*W`!?fR)?dKxs^A zpRz#KdLj7uv3Lz^qd2r^6I*9xnfe#r_$_%A+v@?0v==gN-i=eUg_yrI$++@_jzvG< z**wPBz(>`!DjxAlcc?xUX5@>P_nD^*&gLDK9XQu;HQ< z>ZfXp_{L(cyTM=$q1NOdz5rIPJe|^*_lLbe$6JnA1T*gtN6G#Xrh*eDxnV7h s@Q95x4tEN)SPeBoX?cqO|3sM;RJp8EG?=c$nBP(GFL|NmA%`>m0)^P=#{d8T literal 0 HcmV?d00001 diff --git a/public/images/events/valentines2025event-pt-BR.png b/public/images/events/valentines2025event-pt-BR.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec3bfe704ba2a2e3882ae80fbcbaa71d14b1069 GIT binary patch literal 3267 zcmeHKdrVVj6#q(jE1;-xjv{Wmp>aCR630_1A`9M-whI~`j95*`tY}LC6_iI2+?aC$ zxy>Z8Jj$FBhdibgAp(W@U=0!K5Rfc2x9{GuKz7lpa~*$* z*eRr0gVUw}{FW+tVU9sX#A>>vI{|i^J?>RpbxGgH!4Ld^kdIV|BtPctyZB*If;{>R zq49R5H`3lTlNO}U^1;U@*jA7Ui=Pbx09LNrh9W(3^7>?c3ePq&T$UL`&-X!TaS`jL zuP;vYDR(SvKtUAan&+$2FP6+>>V^9gIfs+A>GbKGbPir}Z5%b8BBG`9)00ftOW7U+ zT_D$7gB!tN9-u6Zs9cSb4=Y5H-#+_x^ij_XAN4d=%w^FVDb3LuQ(n4>2u&y73erjW zsb?USv&*-Pg|eI5{-;=)s}ZmhVX$O=+U`AK0Sm2>v9biDA6yeeVZf2HsEct%8#=S-_-E=gCH347AOg@5BjXU-Mlz*tk}b zBLYeT{(A&UJ{&8O{QYgic^;&uV$HJiK5_I$%7O~#XKrwmc!+>w1&N|WW5FK4%B_)D zG)5ESE7Fc6e5{GQ!kUEwfEKOAu*^7exUL2#UR|G5JD?!QxN8ZB+Fr-TTf7cQZF>S8 z=+n8n2N)6^x9BQl*y6psSTCe;Q3dS=LcQ$#HI#ffRwVi5*PFKb_WRt$FOw@|h~7wK z>UN!Wk5ni-U!P_Lsis6@+6B?~O@kJVQM8M@Th!W6#hq>bV^9Flq7~@Z8q|$v6J%Hy zw@2nS1S1tg#yxqA>ZG%LBA-)ha;!>pg&-@HC;_v&6;b^HHa?fQbsz|7+-gKYl?=^G zwo^DtJ{&7E`P>|VxK<>VZRWdoKR5@&MvN#8N&T%QCXKgKs?iDp*W`!?fR)?dKxs^A zpRz#KdLj7uv3Lz^qd2r^6I*9xnfe#r_$_%A+v@?0v==gN-i=eUg_yrI$++@_jzvG< z**wPBz(>`!DjxAlcc?xUX5@>P_nD^*&gLDK9XQu;HQ< z>ZfXp_{L(cyTM=$q1NOdz5rIPJe|^*_lLbe$6JnA1T*gtN6G#Xrh*eDxnV7h s@Q95x4tEN)SPeBoX?cqO|3sM;RJp8EG?=c$nBP(GFL|NmA%`>m0)^P=#{d8T literal 0 HcmV?d00001 diff --git a/public/images/events/valentines2025event-zh-CN.png b/public/images/events/valentines2025event-zh-CN.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec3bfe704ba2a2e3882ae80fbcbaa71d14b1069 GIT binary patch literal 3267 zcmeHKdrVVj6#q(jE1;-xjv{Wmp>aCR630_1A`9M-whI~`j95*`tY}LC6_iI2+?aC$ zxy>Z8Jj$FBhdibgAp(W@U=0!K5Rfc2x9{GuKz7lpa~*$* z*eRr0gVUw}{FW+tVU9sX#A>>vI{|i^J?>RpbxGgH!4Ld^kdIV|BtPctyZB*If;{>R zq49R5H`3lTlNO}U^1;U@*jA7Ui=Pbx09LNrh9W(3^7>?c3ePq&T$UL`&-X!TaS`jL zuP;vYDR(SvKtUAan&+$2FP6+>>V^9gIfs+A>GbKGbPir}Z5%b8BBG`9)00ftOW7U+ zT_D$7gB!tN9-u6Zs9cSb4=Y5H-#+_x^ij_XAN4d=%w^FVDb3LuQ(n4>2u&y73erjW zsb?USv&*-Pg|eI5{-;=)s}ZmhVX$O=+U`AK0Sm2>v9biDA6yeeVZf2HsEct%8#=S-_-E=gCH347AOg@5BjXU-Mlz*tk}b zBLYeT{(A&UJ{&8O{QYgic^;&uV$HJiK5_I$%7O~#XKrwmc!+>w1&N|WW5FK4%B_)D zG)5ESE7Fc6e5{GQ!kUEwfEKOAu*^7exUL2#UR|G5JD?!QxN8ZB+Fr-TTf7cQZF>S8 z=+n8n2N)6^x9BQl*y6psSTCe;Q3dS=LcQ$#HI#ffRwVi5*PFKb_WRt$FOw@|h~7wK z>UN!Wk5ni-U!P_Lsis6@+6B?~O@kJVQM8M@Th!W6#hq>bV^9Flq7~@Z8q|$v6J%Hy zw@2nS1S1tg#yxqA>ZG%LBA-)ha;!>pg&-@HC;_v&6;b^HHa?fQbsz|7+-gKYl?=^G zwo^DtJ{&7E`P>|VxK<>VZRWdoKR5@&MvN#8N&T%QCXKgKs?iDp*W`!?fR)?dKxs^A zpRz#KdLj7uv3Lz^qd2r^6I*9xnfe#r_$_%A+v@?0v==gN-i=eUg_yrI$++@_jzvG< z**wPBz(>`!DjxAlcc?xUX5@>P_nD^*&gLDK9XQu;HQ< z>ZfXp_{L(cyTM=$q1NOdz5rIPJe|^*_lLbe$6JnA1T*gtN6G#Xrh*eDxnV7h s@Q95x4tEN)SPeBoX?cqO|3sM;RJp8EG?=c$nBP(GFL|NmA%`>m0)^P=#{d8T literal 0 HcmV?d00001 diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index ead0443908b..351b969b1a8 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -887,16 +887,21 @@ export function getRandomEncounterSpecies(level: number, isBoss: boolean = false let bossSpecies: PokemonSpecies; let isEventEncounter = false; const eventEncounters = globalScene.eventManager.getEventEncounters(); + let formIndex; if (eventEncounters.length > 0 && randSeedInt(2) === 1) { const eventEncounter = randSeedItem(eventEncounters); const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, !eventEncounter.blockEvolution, isBoss, globalScene.gameMode); isEventEncounter = true; bossSpecies = getPokemonSpecies(levelSpecies); + formIndex = eventEncounter.formIndex; } else { bossSpecies = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, level, 0, getPartyLuckValue(globalScene.getPlayerParty()), isBoss); } const ret = new EnemyPokemon(bossSpecies, level, TrainerSlot.NONE, isBoss); + if (formIndex) { + ret.formIndex = formIndex; + } //Reroll shiny for event encounters if (isEventEncounter && !ret.shiny) { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 16af8364502..daab808918c 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4356,8 +4356,12 @@ export class PlayerPokemon extends Pokemon { ].filter(d => !!d); const amount = new Utils.NumberHolder(friendship); globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); - const candyFriendshipMultiplier = globalScene.eventManager.getClassicFriendshipMultiplier(); - const starterAmount = new Utils.NumberHolder(Math.floor(amount.value * (globalScene.gameMode.isClassic ? candyFriendshipMultiplier : 1) / (fusionStarterSpeciesId ? 2 : 1))); + const candyFriendshipMultiplier = globalScene.gameMode.isClassic ? globalScene.eventManager.getClassicFriendshipMultiplier() : 1; + const fusionReduction = fusionStarterSpeciesId + ? globalScene.eventManager.areFusionsBoosted() ? 1.5 // Divide candy gain for fusions by 1.5 during events + : 2 // 2 for fusions outside events + : 1; // 1 for non-fused mons + const starterAmount = new Utils.NumberHolder(Math.floor(amount.value * candyFriendshipMultiplier / fusionReduction)); // Add friendship to this PlayerPokemon this.friendship = Math.min(this.friendship + amount.value, 255); diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 60a0513f608..e8f817c1c39 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -249,9 +249,9 @@ export class LoadingScene extends SceneBase { } const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ]; if (lang && availableLangs.includes(lang)) { - this.loadImage("yearofthesnakeevent-" + lang, "events"); + this.loadImage("valentines2025event-" + lang, "events"); } else { - this.loadImage("yearofthesnakeevent-en", "events"); + this.loadImage("valentines2025event-en", "events"); } this.loadAtlas("statuses", ""); diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index b65f1b53441..b1e8b69df36 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1720,7 +1720,16 @@ const modifierPool: ModifierPool = { }, 4), new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), new WeightedModifierType(modifierTypes.TERA_SHARD, 1), - new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => globalScene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 4 : 0), + new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => { + if (party.filter(p => !p.fusionSpecies).length > 1) { + if (globalScene.gameMode.isSplicedOnly) { + return 4; + } else if (globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) { + return 1; + } + } + return 0; + }, 4), new WeightedModifierType(modifierTypes.VOUCHER, (_party: Pokemon[], rerollCount: number) => !globalScene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0, 1), ].map(m => { m.setTier(ModifierTier.GREAT); return m; @@ -1879,7 +1888,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.MULTI_LENS, 18), new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (_party: Pokemon[], rerollCount: number) => !globalScene.gameMode.isDaily && !globalScene.gameMode.isEndless && !globalScene.gameMode.isSplicedOnly ? Math.max(5 - rerollCount * 2, 0) : 0, 5), - new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !globalScene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24), + new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !(globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) && !globalScene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24), new WeightedModifierType(modifierTypes.MINI_BLACK_HOLE, () => (globalScene.gameMode.isDaily || (!globalScene.gameMode.isFreshStartChallenge() && globalScene.gameData.isUnlocked(Unlockables.MINI_BLACK_HOLE))) ? 1 : 0, 1), ].map(m => { m.setTier(ModifierTier.MASTER); return m; @@ -2538,7 +2547,7 @@ export function getPartyLuckValue(party: Pokemon[]): number { return DailyLuck.value; } const eventSpecies = globalScene.eventManager.getEventLuckBoostedSpecies(); - const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 1 : 0) : 0) + const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 3 : 0) : 0) .reduce((total: number, value: number) => total += value, 0), 0, 14); return Math.min(globalScene.eventManager.getEventLuckBoost() + (luck ?? 0), 14); } diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 7a9f0e59993..bebacf87ebc 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -27,6 +27,7 @@ interface EventBanner { interface EventEncounter { species: Species; blockEvolution?: boolean; + formIndex?: number; } interface EventMysteryEncounterTier { @@ -49,6 +50,7 @@ interface TimedEvent extends EventBanner { weather?: WeatherPoolEntry[]; mysteryEncounterTierChanges?: EventMysteryEncounterTier[]; luckBoostedSpecies?: Species[]; + boostFusions?: boolean; //MODIFIER REWORK PLEASE } const timedEvents: TimedEvent[] = [ @@ -144,6 +146,40 @@ const timedEvents: TimedEvent[] = [ Species.ROARING_MOON, Species.BLOODMOON_URSALUNA ] + }, + { + name: "Valentine", + eventType: EventType.SHINY, + startDate: new Date(Date.UTC(2025, 1, 10)), + endDate: new Date(Date.UTC(2025, 1, 21)), + boostFusions: true, + shinyMultiplier: 2, + bannerKey: "valentines2025event-", + scale: 0.21, + availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ], + eventEncounters: [ + { species: Species.NIDORAN_F }, + { species: Species.NIDORAN_M }, + { species: Species.IGGLYBUFF }, + { species: Species.SMOOCHUM }, + { species: Species.VOLBEAT }, + { species: Species.ILLUMISE }, + { species: Species.ROSELIA }, + { species: Species.LUVDISC }, + { species: Species.WOOBAT }, + { species: Species.FRILLISH }, + { species: Species.ALOMOMOLA }, + { species: Species.FURFROU, formIndex: 1 }, // Heart trim + { species: Species.ESPURR }, + { species: Species.SPRITZEE }, + { species: Species.SWIRLIX }, + { species: Species.APPLIN }, + { species: Species.MILCERY }, + { species: Species.INDEEDEE }, + { species: Species.TANDEMAUS }, + { species: Species.ENAMORUS } + ], + luckBoostedSpecies: [ Species.LUVDISC ] } ]; @@ -297,6 +333,10 @@ export class TimedEventManager { }); return ret; } + + areFusionsBoosted(): boolean { + return timedEvents.some((te) => this.isActive(te) && te.boostFusions); + } } export class TimedEventDisplay extends Phaser.GameObjects.Container { From 8012a1b55949233b376a28f7e6399c2c476c194e Mon Sep 17 00:00:00 2001 From: Chris <75648912+ChrisLolz@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:48:50 -0500 Subject: [PATCH 08/18] [Bug] Fix Fun and Games playing move animations even if off (#5187) --- .../mystery-encounters/encounters/fun-and-games-encounter.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index 53f89069491..287376f8bd0 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -41,8 +41,6 @@ export const FunAndGamesEncounter: MysteryEncounter = .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneRequirement(new MoneyRequirement(0, 1.5)) // Cost equal to 1 Max Potion to play .withAutoHideIntroVisuals(false) - // Allows using move without a visible enemy pokemon - .withBattleAnimationsWithoutTargets(true) // The Wobbuffet won't use moves .withSkipEnemyBattleTurns(true) // Will skip COMMAND selection menu and go straight to FIGHT (move select) menu From e75ddfa9750db7831d948dd1143ca5d000626cdc Mon Sep 17 00:00:00 2001 From: geeilhan <107366005+geeilhan@users.noreply.github.com> Date: Tue, 11 Feb 2025 01:56:56 +0100 Subject: [PATCH 09/18] [Ability] Ignore Held Items for Stat Calculation (#5254) * added the ability to ignore held items at stat calculation * integer -> number in src/field/pokemon.ts * added tests from @SirzBenjie * Update test * Fix test filename * added turnorder to tests * added tera_blast changes and tests --------- Co-authored-by: damocleas Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/battler-tags.ts | 12 +--- src/data/move.ts | 6 +- src/field/pokemon.ts | 16 +++-- src/test/abilities/protosynthesis.test.ts | 66 ++++++++++++++++++ src/test/moves/tera_blast.test.ts | 82 +++++++++++++++++++++-- 5 files changed, 157 insertions(+), 25 deletions(-) create mode 100644 src/test/abilities/protosynthesis.test.ts diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index c399a9bb595..43168ea5c0c 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1752,7 +1752,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag { super.onAdd(pokemon); let highestStat: EffectiveStat; - EFFECTIVE_STATS.map(s => pokemon.getEffectiveStat(s)).reduce((highestValue: number, value: number, i: number) => { + EFFECTIVE_STATS.map(s => pokemon.getEffectiveStat(s, undefined, undefined, undefined, undefined, undefined, undefined, true)).reduce((highestValue: number, value: number, i: number) => { if (value > highestValue) { highestStat = EFFECTIVE_STATS[i]; return value; @@ -1763,15 +1763,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag { highestStat = highestStat!; // tell TS compiler it's defined! this.stat = highestStat; - switch (this.stat) { - case Stat.SPD: - this.multiplier = 1.5; - break; - default: - this.multiplier = 1.3; - break; - } - + this.multiplier = this.stat === Stat.SPD ? 1.5 : 1.3; globalScene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: i18next.t(getStatKey(highestStat)) }), null, false, null, true); } diff --git a/src/data/move.ts b/src/data/move.ts index 48f90297115..967d2ee0cc2 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4559,7 +4559,8 @@ export class TeraMoveCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const category = (args[0] as Utils.NumberHolder); - if (user.isTerastallized() && user.getEffectiveStat(Stat.ATK, target, move) > user.getEffectiveStat(Stat.SPATK, target, move)) { + if (user.isTerastallized() && user.getEffectiveStat(Stat.ATK, target, move, true, true, false, false, true) > + user.getEffectiveStat(Stat.SPATK, target, move, true, true, false, false, true)) { category.value = MoveCategory.PHYSICAL; return true; } @@ -10905,8 +10906,7 @@ export function initMoves() { .attr(TeraMoveCategoryAttr) .attr(TeraBlastTypeAttr) .attr(TeraBlastPowerAttr) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR) }) - .partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */ + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR) }), new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9) .attr(ProtectAttr, BattlerTagType.SILK_TRAP) .condition(failIfLastCondition), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index daab808918c..80a2980c92b 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -947,11 +947,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param ignoreOppAbility during an attack, determines whether the opposing Pokemon's abilities should be ignored during the stat calculation. * @param isCritical determines whether a critical hit has occurred or not (`false` by default) * @param simulated if `true`, nullifies any effects that produce any changes to game state from triggering + * @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false` * @returns the final in-battle value of a stat */ - getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreAbility: boolean = false, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number { + getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreAbility: boolean = false, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true, ignoreHeldItems: boolean = false): number { const statValue = new Utils.NumberHolder(this.getStat(stat, false)); - globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); + if (!ignoreHeldItems) { + globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); + } // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway const fieldApplied = new Utils.BooleanHolder(false); @@ -965,7 +968,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue, simulated); } - let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated); + let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated, ignoreHeldItems); switch (stat) { case Stat.ATK: @@ -2487,9 +2490,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default) * @param isCritical determines whether a critical hit has occurred or not (`false` by default) * @param simulated determines whether effects are applied without altering game state (`true` by default) + * @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false` * @return the stat stage multiplier to be used for effective stat calculation */ - getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number { + getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true, ignoreHeldItems: boolean = false): number { const statStage = new Utils.IntegerHolder(this.getStatStage(stat)); const ignoreStatStage = new Utils.BooleanHolder(false); @@ -2516,7 +2520,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!ignoreStatStage.value) { const statStageMultiplier = new Utils.NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value)); - globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier); + if (!ignoreHeldItems) { + globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier); + } return Math.min(statStageMultiplier.value, 4); } return 1; diff --git a/src/test/abilities/protosynthesis.test.ts b/src/test/abilities/protosynthesis.test.ts new file mode 100644 index 00000000000..67786c3ae9e --- /dev/null +++ b/src/test/abilities/protosynthesis.test.ts @@ -0,0 +1,66 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Nature } from "#enums/nature"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { BattlerIndex } from "#app/battle"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Protosynthesis", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH, Moves.TACKLE ]) + .ability(Abilities.PROTOSYNTHESIS) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should not consider temporary items when determining which stat to boost", async() => { + // Mew has uniform base stats + game.override.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.DEF }]) + .enemyMoveset(Moves.SUNNY_DAY) + .startingLevel(100) + .enemyLevel(100); + await game.classicMode.startBattle([ Species.MEW ]); + const mew = game.scene.getPlayerPokemon()!; + // Nature of starting mon is randomized. We need to fix it to a neutral nature for the automated test. + mew.setNature(Nature.HARDY); + const enemy = game.scene.getEnemyPokemon()!; + const def_before_boost = mew.getEffectiveStat(Stat.DEF, undefined, undefined, false, undefined, false, false, true); + const atk_before_boost = mew.getEffectiveStat(Stat.ATK, undefined, undefined, false, undefined, false, false, true); + const initialHp = enemy.hp; + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + const unboosted_dmg = initialHp - enemy.hp; + enemy.hp = initialHp; + const def_after_boost = mew.getEffectiveStat(Stat.DEF, undefined, undefined, false, undefined, false, false, true); + const atk_after_boost = mew.getEffectiveStat(Stat.ATK, undefined, undefined, false, undefined, false, false, true); + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + const boosted_dmg = initialHp - enemy.hp; + expect(boosted_dmg).toBeGreaterThan(unboosted_dmg); + expect(def_after_boost).toEqual(def_before_boost); + expect(atk_after_boost).toBeGreaterThan(atk_before_boost); + }); +}); diff --git a/src/test/moves/tera_blast.test.ts b/src/test/moves/tera_blast.test.ts index 44dc29f68b5..21cbf4c1463 100644 --- a/src/test/moves/tera_blast.test.ts +++ b/src/test/moves/tera_blast.test.ts @@ -1,6 +1,6 @@ import { BattlerIndex } from "#app/battle"; import { Stat } from "#enums/stat"; -import { allMoves } from "#app/data/move"; +import { allMoves, TeraMoveCategoryAttr } from "#app/data/move"; import { Type } from "#enums/type"; import { Abilities } from "#app/enums/abilities"; import { HitResult } from "#app/field/pokemon"; @@ -14,6 +14,7 @@ describe("Moves - Tera Blast", () => { let phaserGame: Phaser.Game; let game: GameManager; const moveToCheck = allMoves[Moves.TERA_BLAST]; + const teraBlastAttr = moveToCheck.getAttrs(TeraMoveCategoryAttr)[0]; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -86,19 +87,86 @@ describe("Moves - Tera Blast", () => { expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE); }); - // Currently abilities are bugged and can't see when a move's category is changed - it.todo("uses the higher stat of the user's Atk and SpAtk for damage calculation", async () => { - game.override.enemyAbility(Abilities.TOXIC_DEBRIS); + it("uses the higher ATK for damage calculation", async () => { await game.startBattle(); const playerPokemon = game.scene.getPlayerPokemon()!; playerPokemon.stats[Stat.ATK] = 100; playerPokemon.stats[Stat.SPATK] = 1; + vi.spyOn(teraBlastAttr, "apply"); + game.move.select(Moves.TERA_BLAST); - await game.phaseInterceptor.to("TurnEndPhase"); - expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); - }, 20000); + await game.toNextTurn(); + expect(teraBlastAttr.apply).toHaveLastReturnedWith(true); + }); + + it("uses the higher SPATK for damage calculation", async () => { + await game.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.stats[Stat.ATK] = 1; + playerPokemon.stats[Stat.SPATK] = 100; + + vi.spyOn(teraBlastAttr, "apply"); + + game.move.select(Moves.TERA_BLAST); + await game.toNextTurn(); + expect(teraBlastAttr.apply).toHaveLastReturnedWith(false); + }); + + it("should stay as a special move if ATK turns lower than SPATK mid-turn", async () => { + game.override.enemyMoveset([ Moves.CHARM ]); + await game.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.stats[Stat.ATK] = 51; + playerPokemon.stats[Stat.SPATK] = 50; + + vi.spyOn(teraBlastAttr, "apply"); + + game.move.select(Moves.TERA_BLAST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + expect(teraBlastAttr.apply).toHaveLastReturnedWith(false); + }); + + it("does not change its move category from stat changes due to held items", async () => { + game.override + .startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }]) + .starterSpecies(Species.CUBONE); + await game.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + playerPokemon.stats[Stat.ATK] = 50; + playerPokemon.stats[Stat.SPATK] = 51; + + vi.spyOn(teraBlastAttr, "apply"); + + game.move.select(Moves.TERA_BLAST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(teraBlastAttr.apply).toHaveLastReturnedWith(false); + }); + + it("does not change its move category from stat changes due to abilities", async () => { + game.override.ability(Abilities.HUGE_POWER); + await game.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.stats[Stat.ATK] = 50; + playerPokemon.stats[Stat.SPATK] = 51; + + vi.spyOn(teraBlastAttr, "apply"); + + game.move.select(Moves.TERA_BLAST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + expect(teraBlastAttr.apply).toHaveLastReturnedWith(false); + }); + it("causes stat drops if user is Stellar tera type", async () => { game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]); From 20ed4db88b942677a104ae89966ceff57d3098aa Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Mon, 10 Feb 2025 21:15:59 -0500 Subject: [PATCH 10/18] Index Zangoose sprites (#5042) --- public/images/pokemon/335.json | 2455 ++++++-------------------- public/images/pokemon/335.png | Bin 66403 -> 10468 bytes public/images/pokemon/shiny/335.json | 2431 ++++++------------------- public/images/pokemon/shiny/335.png | Bin 24209 -> 10990 bytes 4 files changed, 1068 insertions(+), 3818 deletions(-) diff --git a/public/images/pokemon/335.json b/public/images/pokemon/335.json index 0279e0fba5a..a9313fcec5d 100644 --- a/public/images/pokemon/335.json +++ b/public/images/pokemon/335.json @@ -1,1910 +1,547 @@ -{ - "textures": [ - { - "image": "335.png", - "format": "RGBA8888", - "size": { - "w": 366, - "h": 366 - }, - "scale": 1, - "frames": [ - { - "filename": "0013.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0014.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0035.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0056.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0079.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 5, - "y": 0, - "w": 65, - "h": 66 - }, - "frame": { - "x": 0, - "y": 63, - "w": 65, - "h": 66 - } - }, - { - "filename": "0077.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 7, - "y": 3, - "w": 67, - "h": 63 - }, - "frame": { - "x": 68, - "y": 0, - "w": 67, - "h": 63 - } - }, - { - "filename": "0078.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 7, - "y": 3, - "w": 67, - "h": 63 - }, - "frame": { - "x": 68, - "y": 0, - "w": 67, - "h": 63 - } - }, - { - "filename": "0012.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0033.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0034.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0055.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0015.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0036.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0057.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0058.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0080.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 0, - "w": 61, - "h": 66 - }, - "frame": { - "x": 0, - "y": 192, - "w": 61, - "h": 66 - } - }, - { - "filename": "0085.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 61, - "h": 66 - }, - "frame": { - "x": 0, - "y": 258, - "w": 61, - "h": 66 - } - }, - { - "filename": "0086.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 61, - "h": 66 - }, - "frame": { - "x": 0, - "y": 258, - "w": 61, - "h": 66 - } - }, - { - "filename": "0069.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 0, - "y": 4, - "w": 64, - "h": 62 - }, - "frame": { - "x": 135, - "y": 0, - "w": 64, - "h": 62 - } - }, - { - "filename": "0070.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 0, - "y": 4, - "w": 64, - "h": 62 - }, - "frame": { - "x": 135, - "y": 0, - "w": 64, - "h": 62 - } - }, - { - "filename": "0076.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 65, - "h": 61 - }, - "frame": { - "x": 199, - "y": 0, - "w": 65, - "h": 61 - } - }, - { - "filename": "0011.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0032.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0053.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0054.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0009.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0010.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0031.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0052.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0071.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 0, - "y": 5, - "w": 64, - "h": 61 - }, - "frame": { - "x": 64, - "y": 183, - "w": 64, - "h": 61 - } - }, - { - "filename": "0087.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 1, - "w": 62, - "h": 65 - }, - "frame": { - "x": 61, - "y": 244, - "w": 62, - "h": 65 - } - }, - { - "filename": "0075.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 6, - "y": 9, - "w": 63, - "h": 57 - }, - "frame": { - "x": 61, - "y": 309, - "w": 63, - "h": 57 - } - }, - { - "filename": "0088.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 2, - "w": 61, - "h": 64 - }, - "frame": { - "x": 123, - "y": 244, - "w": 61, - "h": 64 - } - }, - { - "filename": "0072.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 63, - "h": 58 - }, - "frame": { - "x": 124, - "y": 308, - "w": 63, - "h": 58 - } - }, - { - "filename": "0008.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0029.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0030.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0051.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0001.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0002.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0003.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0004.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0005.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0006.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0007.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0016.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0021.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0022.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0023.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0024.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0025.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0026.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0027.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0028.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0037.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0038.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0043.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0044.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0045.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0046.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0047.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0048.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0049.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0050.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0059.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0064.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0073.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 9, - "w": 62, - "h": 57 - }, - "frame": { - "x": 187, - "y": 307, - "w": 62, - "h": 57 - } - }, - { - "filename": "0074.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 9, - "w": 62, - "h": 57 - }, - "frame": { - "x": 187, - "y": 307, - "w": 62, - "h": 57 - } - }, - { - "filename": "0068.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 3, - "w": 62, - "h": 63 - }, - "frame": { - "x": 264, - "y": 58, - "w": 62, - "h": 63 - } - }, - { - "filename": "0017.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0018.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0039.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0060.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0081.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 55, - "h": 66 - }, - "frame": { - "x": 188, - "y": 62, - "w": 55, - "h": 66 - } - }, - { - "filename": "0082.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 55, - "h": 66 - }, - "frame": { - "x": 188, - "y": 62, - "w": 55, - "h": 66 - } - }, - { - "filename": "0083.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 58, - "h": 66 - }, - "frame": { - "x": 189, - "y": 128, - "w": 58, - "h": 66 - } - }, - { - "filename": "0084.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 60, - "h": 66 - }, - "frame": { - "x": 247, - "y": 121, - "w": 60, - "h": 66 - } - }, - { - "filename": "0020.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0041.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0042.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0063.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0065.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0066.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0089.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0090.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0019.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0040.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0061.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0062.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0067.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 249, - "y": 250, - "w": 60, - "h": 63 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:9c4e9647cd30b406386dcfa45795951c:b817a280fcd689ce74ea32e378a31e74:40bb9f4809624b12bf79bbfe664bea73$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0002.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0003.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0004.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0005.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0006.png", + "frame": { "x": 0, "y": 185, "w": 59, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 7, "w": 59, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0007.png", + "frame": { "x": 119, "y": 182, "w": 62, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 9, "w": 62, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0008.png", + "frame": { "x": 119, "y": 125, "w": 64, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 10, "w": 64, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0009.png", + "frame": { "x": 195, "y": 0, "w": 66, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 7, "w": 66, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0010.png", + "frame": { "x": 129, "y": 0, "w": 66, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 66, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0011.png", + "frame": { "x": 320, "y": 0, "w": 62, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 62, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0012.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0013.png", + "frame": { "x": 0, "y": 244, "w": 53, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 53, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0014.png", + "frame": { "x": 59, "y": 188, "w": 56, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 56, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0015.png", + "frame": { "x": 306, "y": 187, "w": 57, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 57, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0016.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0017.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0018.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0019.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0020.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0021.png", + "frame": { "x": 0, "y": 185, "w": 59, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 7, "w": 59, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0022.png", + "frame": { "x": 119, "y": 182, "w": 62, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 9, "w": 62, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0023.png", + "frame": { "x": 119, "y": 125, "w": 64, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 10, "w": 64, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0024.png", + "frame": { "x": 195, "y": 0, "w": 66, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 7, "w": 66, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0025.png", + "frame": { "x": 129, "y": 0, "w": 66, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 66, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0026.png", + "frame": { "x": 320, "y": 0, "w": 62, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 62, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0027.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0028.png", + "frame": { "x": 0, "y": 244, "w": 53, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 53, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0029.png", + "frame": { "x": 59, "y": 188, "w": 56, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 56, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0030.png", + "frame": { "x": 306, "y": 187, "w": 57, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 57, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0031.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0032.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0033.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0034.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0035.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0036.png", + "frame": { "x": 0, "y": 185, "w": 59, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 7, "w": 59, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0037.png", + "frame": { "x": 119, "y": 182, "w": 62, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 9, "w": 62, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0038.png", + "frame": { "x": 119, "y": 125, "w": 64, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 10, "w": 64, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0039.png", + "frame": { "x": 195, "y": 0, "w": 66, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 7, "w": 66, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0040.png", + "frame": { "x": 129, "y": 0, "w": 66, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 66, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0041.png", + "frame": { "x": 320, "y": 0, "w": 62, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 62, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0042.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0043.png", + "frame": { "x": 0, "y": 244, "w": 53, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 53, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0044.png", + "frame": { "x": 59, "y": 188, "w": 56, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 56, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0045.png", + "frame": { "x": 306, "y": 187, "w": 57, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 57, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0046.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0047.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0048.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0049.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0050.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0051.png", + "frame": { "x": 248, "y": 129, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0052.png", + "frame": { "x": 188, "y": 123, "w": 60, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 5, "w": 60, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0053.png", + "frame": { "x": 0, "y": 125, "w": 61, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 2, "y": 6, "w": 61, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0054.png", + "frame": { "x": 0, "y": 66, "w": 63, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 0, "y": 7, "w": 63, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0055.png", + "frame": { "x": 234, "y": 190, "w": 61, "h": 56 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 2, "y": 10, "w": 61, "h": 56 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0056.png", + "frame": { "x": 234, "y": 246, "w": 60, "h": 55 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 11, "w": 60, "h": 55 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0057.png", + "frame": { "x": 115, "y": 239, "w": 61, "h": 56 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 7, "y": 11, "w": 61, "h": 56 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0058.png", + "frame": { "x": 63, "y": 62, "w": 62, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 10, "y": 7, "w": 62, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0059.png", + "frame": { "x": 63, "y": 0, "w": 66, "h": 62 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 8, "y": 4, "w": 66, "h": 62 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0060.png", + "frame": { "x": 0, "y": 0, "w": 63, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 6, "y": 0, "w": 63, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0061.png", + "frame": { "x": 261, "y": 0, "w": 59, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 0, "w": 59, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0062.png", + "frame": { "x": 181, "y": 184, "w": 53, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 0, "w": 53, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0063.png", + "frame": { "x": 63, "y": 122, "w": 56, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 0, "w": 56, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0064.png", + "frame": { "x": 320, "y": 61, "w": 58, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 1, "w": 58, "h": 65 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0065.png", + "frame": { "x": 129, "y": 61, "w": 59, "h": 64 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 2, "w": 59, "h": 64 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0066.png", + "frame": { "x": 195, "y": 60, "w": 60, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 3, "w": 60, "h": 63 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0067.png", + "frame": { "x": 255, "y": 66, "w": 59, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 3, "w": 59, "h": 63 }, + "sourceSize": { "w": 74, "h": 67 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.9.2-x64", + "image": "335.png", + "format": "I8", + "size": { "w": 382, "h": 305 }, + "scale": "1" + } } diff --git a/public/images/pokemon/335.png b/public/images/pokemon/335.png index e5d051dd8501f388456944fb48a9a00d0ec313a6..65b565823398d2fc5c1c107d23b78575de0e55e6 100644 GIT binary patch literal 10468 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 literal 66403 zcmYhj1yq$?7q+`MElPKHcc*lBr!+_jNOz}n3rM$gNtbj<`ywSEAl)Dh|Kj`4Ipa8B zFl0QnpS9LpbKdtg`J|>Ii-Jsu41qvU?$Wb=8oSfYeM9 z??E8s5P2zaEwAiD|3E#hot%?M0b=%J2VXBzGp_H1)~oKJn5zVVG+`^eO{^?eh~XKn zb}UJ~=23*^AC2EAc)xY1hp;tzv+NXce5&AU4UZSy=kYV9JAm7V+ z;g(19#yVg{bkcjOb7vycWFntycQ{fuL%{WK=i2rC{k{JhUTdh(|Gt)q;&duwHu?D= ze|1C^DSJQEx6=5mK*N`VX;Yb3f)S2)qv6Dhe(q;wWSm;Pe}DJUnsffL74}s!$Cv-O zISiQ{^QJtlk3(P1qobwuo6A8fMQ-<6EuXP-hss&#^BT7(*{u{r@=oB!+}}DZ<9}kE zn{DMYsAkPk8HNOrBlU8V>{yEL_BSw>a*-*~{3&hiGG=S+YQRiS$9;o>Lczr~7ryKz z!M*Fe92xdLW>6WMaF8mcZ#0tB-Oxqp7!KvzT(ho@d7vg%L}cW5=au4N0g{a?pUj+} zKYy*wV{?(M)I@#xg8)glIy)Wv!C^Slavoz8iKRWua>>BdxpTj3+Dh*ebOpLiT8 zdba0VRaGHFb1?0HPQ%zX=Q`VOubbFDdD zRmc05sMFzD;0jc=T;+&PzXibNs`(|Jl1uaFP3;`q-*B0%4d+Q4<@>!nQ&;(t^$(7_ zCoQd`j>z##sN9$KvRuN%LTu$2-{UsA{d)8v;oIpVM~M`r`oxqbn8Sg}mqr|Amq~sV$7^+k|E@@0ZUig6Opwrd|{oo@j7q= zMTDwjTFkytpcw|-^E$p+nv)&yrLNXJ3zdv~H|4ZU`&y6N7Ve;rrsn6hNTN*Z#Ic*0 zn44Rdc0O{dS>Df=n3`^`0wWxGlxc|Zx-^DOc(Q>@^dE{m{-@jBZuG`93;ZA^vEtVL^ z^5^WysfiTqh$d@6lERaH$r5cXJq6im$NHL}8^NRd$UOdzx&zwIvasL*S^M#hmB+U) z)sVL`!*ECN*%`j44e;)z@y6MN{g-UvGBuu2skZd#&`NoTU*AQ^5gcyiBGC2cszI^q zBkeAK8ANEguBVLP`E(8I4cJa$RMCxH6G%TL%GOz__t_TN4dHjAEx>2!R4 z4N1>?L^r_VAkm;UuKi5V`d8fPgd7>!Man=+C!#n!eHtbsy-k^r!e$fUp{YRB!c$Pq zV~>PiI#`)Mg9Z1ROIurgG-Qb#I>$Fl8B=R{=hk9o?al*%D?uX(##GY1lL}4Mb24q# zTQM=x{(c%7O3%QF(51pSy*|`VSU%?=MJ}6NB!A!l`@}0&m>5EV%>t1~co`awDb>P! z5G{_XYE-BszWAQJsU!SR&gl?sk6oMM_g97kSzfLkU4Pf#6McC+7HJ5&VI@AfMpw;j zQ!rz9g@c{88DA4c;(V})%7YQ4m)}d>J&QH+rf>W9)7fpV{7)RplOXFM95+N z%7;KmH2(K*aeR~|1)_W#s*My_wzXoqShSZH%@sC_0Auy?D=2d%YqPRp33g=-dg-T{ zD0nda9@b4Q@dUjfltlq9>omWf?`4F$Ma=T&_A-7lNph9MRw@|K!+ZL0zIXB@@<^8z zBg^DvgZ)sU^jT`^_P|YW$W#w^aS+uf7OLR8WIo$YnJ6-+B2p;+4*PmPtxbH*UOx*> z_G~VQd>!21Z12_r9db(M4<|=~v28|sWqG-HG%`7vF8P)xwaJSh#z9u9><=%-ikgCQ zDN2xPRUBsC4JF^3L~0{wI*xld z+^CmorHvbBRHk@<>ltN3VIwi?d&`w_)bt z>ER>tu90cioZ9Szl6Z{fPEClFJ{af>jvuiZgd2qznW_y95V-SKqGNP^vKLOQ#!@;u zxF9O?tf6H*H+~-EIJQ4Ep?c!0{+pXW<%lLvW^)RGu-qvvb88ewEpCeZoC&_}d1C&H zaK?Qt=ru3o)yZ$qyAl#KucfnLc3LD{+8@zLJluA7!OMx}`OLn{*nzhqNFhOIOGXfgWCZlb(c4#DbEHY(%<^aD;!{0e zvI1cOQt4J<=3H()Jgu$e?&cNS3&D9ea8S=x9xi-;N8bS&?PSJRE2DEBg9nR?i(bzM zQPS6E92lCZ)|Kdi5dn5;hk{gR|+Mf##i4%#e3U2hfn_E5#LfcJ2bh9d%~SDtmo{^I zY%mhn6yQtqV?zf7+k+$|mxv-_QTZM02P2E5AbNygK0fSm1?6`mm1D#Cp?ExHOq? z&c3xZaE|mUSd)aL?qZXj@L1w1yb`xVqmm(b;I5 zgBA(eIlB^o?35!K@ic3s=ktWWgohoap1D=IkdHr8?l(;)z)ey$qN{Jxa}Pm zw-hJNGr>ic80~A?gpRg-r$aQB$@!g6K~?r~7TiX`)2#jo30#HVGK_57Os`cAqn?J6 zY=wCM+xsK&&ar@Jn!d=hYhh45vr3@VTb-wKYY0UE6C*A6|KIiP(mMLhF znM|jpnE?)c=kuz*5p0oyQ4=Nq#EM8GA9x&lKJVXThAWC{xwvey5So$=4evO!W^H)SNg;Z>)6jxIX@X-F$cs-hXn11g*hs<8ag}&mj@+HI2@Wfg~(4GIDNr z3dEbzwywxe;ZI#%Nn;Z0H>EMmVIf%}Jq+G@^-d@S39>OFukoPhdvhzl`?*`L*}1GI zrCfc{xP*nX>k#dfaLhGURLZz=bo8tYTmF0+_>%}UuIQw%oQ{Fg5{ zfmpD7!om|^RB1(%M3CHn7liy*zcnqrX|zjmVbtu-5&zbclV?(ofOmzG z$~J-%y0gJsSIP51@ZocNO4_e$a>)AQSKo6V&#kAeV#nb!!h3g9pn+iWX1qb&_ucHQ z?~etrdC2XIS>8-;y2Tgc)nsf!gYI2j8AzuI*GQjlw1mmG&x~GX1Ls%>A&6ieJlyR4 zl9GC9qG&;RBHwpRzKGCB%cNox;E3drBS{&X7bY0|oxoM>n|(_=_`^9Hq~{B1Ou89P zA}(a4#%%3O5-Meb)hYsewJyS^=9sXEGD-$V1P- z;N$ifPWfKZ+vo3ll0}uv@sibL<|=IA3V9UJx3}_mUi|T{M-`Qd%lbxfZLT|ip1b`q z?0Z~9RpejOg-9?eO_TSv`jo6CzqhvDyt_Jh)s+;Rg-|wWi~zy@JJtWyv#<=7d{zP- zPPp4c;Ax6YL`*&j1H@!}0s{GcHMR5hSq~i@N+BUbV?RtxLT*mN_i*phL5xgFN=och z@;Ihiv)vh1Z#73nj){3I>R^ZI>3--aDXuQS5Fn%E6u-IBK}d-2^P!To4ec$as1#SX zWm>q}I{)O+&86mhnJgPFcyTcg4_=>1Bb3MQ>=$c49X{_-ug-CCa$;%+Yb@2HL-0Q( z%tDnCQTmZ}C;CT5uEOl`Gn$5v+yt$k{>{>$)faNTpLkQ-1XZ@eVSW$~VPfP#LF!|4 zATOUqkYY=IU}HEcu1%jaUP#2|L^O=0q6&F>N-2|0VA2!$1qNi)qwq2tEW`ChJd**{ zFIo8MR?gG!xb`Gtb5yJa-|~V2=P-sX;@LE;ft{BZfqL)3siOQNA6VP2@2ApzTlCY@ za6R^x%+R}j%X|NV`>A{DS;wbQk-?8ea zU0QYdOmE-pc~&;-CwM9Ryc)gj_M~0~#V-gfDW$>xiJ*pMntM%zLBRU@*B@yW+U)Wp zt6Wu7*LT@y)dS4wf*v{{(`V$yK~RL<#VUq0R2$CN)d&6S9-RNN)$6>1|CYBXSh{F6 zzOPVzO=AdbwICklS5!@>eK>Ob!pOKXz@CoH&@#YpQ}a_PDXjmASBTZvM*M!GA9{kZ z{iC7E@H{tkpHIP-NrX*YrR;}E6B1ZrSaJV2yjQ>{|2NGvaCi7Gh*FZjJp63SR;XLY zbYRo|GnEzcxeqm69RG?CKFXi)8b@^-YpeIP{+_{L)lW@M%ILO`+#5E*vtc)BP!CM= zJZ_;_zJK2y<*6K|!IE+RC5tb@5CDKj~Ud5hJRl)xWFvZymSNaW>g%QP7<(=WGcUkL6)>_D9d65v}uk%-e_ zkY|rc3f?$#NbBqT{cWuORm7Oh)`krCf7?{RL$;H7-_FIYUw5eJQ@xY9dEdhInKi~U zI`PATNFFXNsK7G|XQN|a_&{a8mn-6;3f}v_Tfda<=S?dsM{i<|X|Xu0XVqf)oiF~; zJIcIOYYl@xozEp(rgmIa$zueMF{?DDBIiTClteUZ=I-j{VuZEl9pjvKqYgVnUSG3 ztse)uEC0)qe47V!1pkf-#$8YJRdH;0F!NF@-$K@ED$X6~YtOqZzl}|G6?7^*1;lS+ zI?{#6mne)d@-IdITRiH;Qth?L2tH=3jqEPbPAYN{R!4xGA2fN^`!{m*V!=EVsDj;= ziPYP8wx?1?MkeC5*!=b9U2>**JsU`r8Tn#u7IP*m17{&)R-^*Cjkgh)p8_ooJHuuV zmHN|)ZXo@kj4Qq-QB5t_T{BteY$5FL@6VE+dQ?{Sx*_AUsB{a%zai7`+gEc0FH9n(T2Sem8Ah=!OWFhgL*I6*P8R-_!&$WZB9=%D@QW z$kB|-Ry@p!gLnmJEZUNA40Jk4ir6rHeO}u4@YqscunDVm@NBO&u1_#ucJi66IX;kh zL4!&qAt+CbZ&0}L8&0}t^B?F^@U8yzf(kaU&YYvSRN@C6L`_?V9`E$H<|~fLiYNB3 zF6dB^QR{u}`|$2(dyfxm#Coq2;cymb)4vv51Q&}T)OhFTE`iSLW+opfIvJKI?0bE2?uY2dWk!cops4IQ2FfWCP9rqesINc*_IgZ zKAVlH|4R+#bh9Bvqq`nmx7G-jdXKsj_)zM7eOHH#}8Ww z4E}!JxK8kncMm%HHXhd>8&B&y5NR(x;*46wyA6BAAnbZ^$C$za$+ zN0$qNFC3<~kq6(*dCf7S`g*6FE!_K&D*^C6D*U%n=} zBI5jl;$uca%xkzFM^ivct{@~8MjNQxnl9z^zB=E+R8RqCBx=c)dfobZiwtEV3dIF>HhuSKo`_F&hFHG+5bARKZ>0|KTn0P8J zEmzS$^YOQAYr;pxT_9&-V&X?Bf5u7Nl`VQaHPiaAM*8yx3krmcd-eX%|0!(4?FMzs zum4*qSjQ5xO@*dEV+n+k)TI&uu zIvYIM;KX}DW(n4SrIqp%owOZiq?W9%1cL*4>@drA&8!}TkM|JQgc{d<)lN(!j3PYg z@Yv%a2wz5i>>XRx>)pA-<3l)RT$^AoqN(TF=kVI9ir&D>rp-Xy^qJg-m}5#c-8g9A z$*~igk;_|6?ZL!XTw4$R^ql2;-xJkhV_B$C=%*-&QEVb+&fE3Vr4`1iO# zh`R+S2p_{^Rr~43Hx0X$>JLbQg3b_Pga;L2O;Ut+E??FJkcqqlmYXkAQ?b37sbLfp~3jk*00_5of?V=QZ!$3rd=^ zNC=g+kAN}qeSitlcF zeEnv<^FuBhv+tV^N1pXsf}DxQkm4G5HQUB8DYZecpF)sj8ih!vB`2QWwYkucb5kP; z5D?-M685dMxjtTpWz^1kW?0o;C>AyheMTO~&ii(;`6mAZ5!E1~n9?A+89JP}fm^+i zu@4C(3jN%@am$1@^f`NE-MXO~#IC=`^ zTT4@=V(n^Ubo674y^BjmeJ2SLkVtv>reAt$wq;cNbG z#)2VEseiR;DepSze~4}$+61$*@3?oyrTryjK2A+-+0qjb;jg`UleCIw{R4rV(YmlR zwD>~=`0^s$A*yt3^wJs${& z5A3!j`eAP`J_?87S=rhKlOwhAvj>&gP5inWz=EXWSU~t)@3e)rsR-d30%h3c`YL{H zhD=L{MV)3uq)W`=OX8z()PyroR=t$e{Sj-_S_tSL>zunnKjY|ig+>PdNdj6b7^8p9 zpZIFvza=6RVSL}i${7)v`AV+>k6o%W@k(A(o0N;X3dFAUt1&;dllA(g#7IPjO%_>; zjiKR&Y0nIv;j-;sbfgdn$bjy~Dv+g({#I5p;)}5kae|HdXPP-sgHzEmEQCZ=bgW>| z@tBUB?2rwLjZ8V)##%lkD0oxRbg)85YwDOs7M8={Vic;rNqd&yz#sy3zw8Za+GosFBXb-pU*O6+b;Rt zzkOdaIp6tHaxU`i+J9=dOn27}iYM#Q)a+*ANt6NnW>s@BX6Y72)%Y&|)xbj9rq@wZ zI{du0--AG2^wa$N`E6c`Yi)cK_vU@5VpG4 z3zndtUc2Ixl71i>NT4)g6cl-=Ftbqi%bBs`Mud~C=D6c7N#3{d4@$gAh$$i2vjLSE zm4XlshL4VMZGt|FG?qshESUmB2;1kHSE3@?12{^yu!XQ(yu79Oc(?vuUifRZl@==) zTkVKX+%>m8i~m)FBp9b_)q$RoC46c}(`$1U7baM7xa10zwy38vl5BgXeI4WBxah+4 zd*QeToCy7#O0m)G*>MNdM`BWX!QW6GEj^B2A`Ss;moN~++*fV1n$rhi)W6Khanwqo zHcl0XvqD~?Cmamef<;K7-JS2<#oY1L{yG<#=}^XclQ>z}J_&cW9C9$2d{e=dg0v>K za8xaz*t7&kajevxm|85(~e###i(BiubbMo(BW4vk{u;FyK*) zvw4W(|CAM+s7Vu(0Ob|bYmjxON=ArSBhk>C*1WZ5Wz@7lu+$%NQ`g`^u!V@Kb#(i4 z-^L2MlShpC@4Z&~NT5tRgwV^*kK9WaIR^Bw(cBm}gRVpO(U8gz;g*VWX3PQ_U*iXt zRo!|YBNRxIak&v<*P_$m+Gk-Wv3GWXcLSfw+u`M~+jU%QMs9#entwP;P&J_sD@L_X zePP`Q?;#V0pI3dOxI5)LaRRG=5PS;#l0~SFTz-9N-#hk0!*@A=nEM0{BE81BXUGPI z<$C9DVSf2LuAYvMqnHbS0&fs+oqSF5ZmPUfVThYHhSt|g*Mi;#mFmXWV;@jkt=9R% zs`(lFd#k~DEn|8diUbK8k{xQ)0`U#U&U}MCFEW>d$R`+h=(Qa zFrs|L3fY=8Db-l&+JAU2CPhiqv`SQXUNs*w-oTs=Vk@x3%(IlKtYn3q#L=f$7v+5y z&B8TF!eI(O>zCy*2?!`8=y8acYHW_D48e+NE%6Mxh^O#NC*_MRah3fv51wJyRNtYf z{o6NrZ8va&%Omp)xeyd|QNRZpjLY#OsnBMj-LX^vBL89V}4A_d3$J1??(p zqx>rohjQJF?DeAvfe==qku?Lc> zbpBIl|Ig#=KnJD57yQQm;NkH<3Qn7JDkfpgY2dmbmGc^J^fs9P{6fe_a@W5TGqQ|~ zC?v{?hXVBiVdvtZ*+B^iEzR0h$P;I`ez71wn>RZjo7M39e2WAdx>MiU2#paUiYlwN z47d{RD#u(w8p2Lh)x))ze zd5$L6|5&?ApP2a9E%J_NyS~ATrF%F4?mi#ImpHH&i9|-JggpZK+Vr|=G06c9*=mXv zB8IMJVp1!hzKn1`+i`m>%;q<=89pUz={EiUu>j~69?W#G5cbo^3omFWIyycEoR+tnQn;>pL)bv3pV0-rp}P(K$&`7?3K zsPgOm-oY5igINP)W*ip+hh^-(g_$_@CRFZ5suEV=;1QuK_>-1*8H9AH-g|?~I3QsJ zDW5}R5KmTAq&h7K4&03DuW$89eoxF{^hijW?yHJ9O99&YFXdB?h$~~5$m&Z-$sS8u z>Kp%jx+bc0m$#RCebbUoa=(h)v0=9hJtu_Fg3L0XFNy&(WhRPfq2~Md3-&CfkEg=z zLPGT}KM1SdH6joX$58|?D3nt$Io7Ja(~~fy1A*HVU!HvmJ)BZ?QcHhEu!~}=OZA*r zz*8G9{G66Pm6os zD~EM~qR7hZJMk=mPMYyy3y0Z(J~qylRl>#Zfn#&so7lmg%JogkOu5cd)~Qcd?5K^F za3uvD-BvbcmeTv*Ad`Qo;ywt42zEhHb3bbeF)c)WL*hhzN&K?Wsec($Z{))c^udlA z!4%5k&}UHX?aOCLbJ=hI&B#cTF&$E8y#yd|Z@A-o<&hZrw!V`CVustb03fC5U=A<*~y9-mm zQ9y3P&hilo3c8KCz2^I3KZbX|_*;Q`of(;S-;V@5ag63G-oo_pf$Fp^Wt~h>@ys8Q z%zymz)(S)+mz^Lkx-H(_Z-W>w=#pC1u~E!@2_2~`r&`lC_3u>+Pi^2g zBaHA_wk^<*(*0shObd?ArX2R(vU@Ko(cuA8rL}gUyDm@OTFY#OY8U0xt!}a^=6oZGYh2mF8rOpJ zc651t-CF>UPD~%i1Gd%v??liP&vB)Ausqvy?7N=aeK$ePd5cfjsJXh>+z@r|!DX*) zl0ZD75``n1pjF=q{vcxHUI2a-12v!r7l)f$OPXQmn z44$jAMx!jT2EiLx98%vOZ<9WLRz-%VefwmRCs}HU=hUyAJN%ezt#l*vHgYD!Yl(`A zia}2PEF)Hi`*0C!?aWr{t%30ApQ^$v zUaw7m1&e;NYY}e08g2KAC@6zeV`d%}0z9bjS-R0QMriSzJ$-?dK5obq-E*}}5Kd0v ztHsTAj&W1SXwO=!2TMMq675#L{w298*sJbln%@f~;2eFTwF9Ml?QXaNLL4tK1P4ZA zxqWEd3!^rv_|xQ0rfKo4R1z-Z&tDV_9|-A;eQAu0G75L`&cB(G7r2P4dux61R>qam zJ0KChL~!Q7`@x55Tx_Ur`}H4Xidv$E6t%SiZZSCxP0c3LL|!D23Lpcwf#Ra^?^5J> z6?i`(LT0>V$6>d8KIv#yRK+)cD|tFCIU*Hv`>^}qBaFd3C|fvIacyl`wv^=U&Fq7oaI9w~ zdoq$S0qQfU#MTRoKe@kJ$Kk>Z$i;?Ta<2g^$8Bs^mV~zD+qVLnBLlJa=i2-Gx2Lz{ zNH8kdA3n`NqCPTp0`zH)c>szhTRJZkt!L z<{ZKxtz8&LC-tiMk6F~PX_z=7B%K&I7^Y8n(m9acO)yLMV3;0b&_2{Rx9qGIkkVa*2;L8$2aqovGVz`x^LZG%_k*c6jsqW zzMnb=X8(NX{XnxmE;FzuEruPrP>(C6H@ma)yEembFDRqUkuP@ON5nU~2_NiT8RiMOx zBjiR?4s9i56Yn*A@CHfY_p`>&8|#CQW-597P>74k{vH|7c^_SY8eZiYC|8&;b61SG``#i|=!C zd>HEmPZOTVO@;hQZ4$QY+LeEL730Md?ASlpP9<^HiLFphM$M8&<65~`Mg9OBw!p?# z&UJT6rV#Qlw@#4c7)>2DM`Y7Ee|fKtm^3eUsG8dvWf*VI7^cqT>8Wm~XuqOM<%G{2 z9#v(>%0R{WPcIg4k^hs`VA=<#r+v=fzB_?2N1jGT{O*6LwEPY#LGq;c%{;dX9ceB) zH)1{Od62Et^L5`Z!{_@*u5h42VbykVYHIvPUE((Pv8kFxyKJs4*Z(`E@9&^IYmfP? zvm)4UP-zng|9k-Qg)46O6U;pksFFJw;_?p^8+%wU4HioXqslR8H z^Oz+5u9-lppti z7f(d%#DOFG8>pVg8a^TwHAh2}6VRCU4LNI#g>3cx$&gA^@ zq{>xvTI;l>rgZqhmq8rp6kWnel6P@DdQjQ8PTP~SG6U8JKbID`Z>?MVWKTgW_a}&u zHbY8di%)bf5X)%r3^4raK(Z~<|HjRISzTKP2@=Jwy;G3zp$JWpgYf-JsG?TQ{m7lc zIPk|g5?3ZkpQoQJNq>P|$b$@tAr|om&#(IXpux8` zuaOZsJI|eiV>P#E^xUsDw+1~{F^ZS;gY)6(vII(~koBPvIiHsUNWT3=Usk>=ND}J# z$%&2)5)m_f9g*gm4+lDnNkwnmrKV0mBz~R+my`AJ;~Dtihk$pdte7*9$(nLxU*MRSGH;nvROz`i3pdCOaYVy(Az*s@3w?Z z{^Ll#7aNnupt}3hr&j=UeNDXi_F9S)A!0h@0bQ!sUNZuu znvR?`GL_l^rIUR3ewmq>mk*J!;ND-?;eGswhi_YB->gNPyO2?LQ#=I0 z`sl>OWjN+9lx^+8^=PHMzgT$waEFySQTaH_-^rf&aR+H%vnW%Axb{59uyJv@x)6B| z1OcP-<@rtCzb~y4D|eRW5e0^Gf2^v`?2GYiWUHvg#*%R5O_1eeGyGSF;H3Eb9gE?+ z@8M^;HsR_zD->ZI8?+YS6+L9zsm5qDU<7;37;ux15xtTNl@r??a0VW)xkNl+VF4OS z{3eWz)Zd^lJoXybvGA9A<^*{DJyL`woa8!LYrN#f^KY+LHopLa)t7*p1)U)S&@GEh zMb4+gqW`R@%W-IO|I?ZS-m-yIAiTOY4;Lk>m)33z16oR{rL(0h zPe_4rYl)e!33VZi;|*%LgKokDwYA$K%8|8H1AmS?jH9q=TzN>j07_H$NiNh79{z}q zV4IqAe$$moL^z$D{eS$ptk*qiGQPotU``kZ=1Rf$E%wztuIeG{X1$eS*3=2xUZLrs zoY79nP+REB)l+a!L+Kr6libU~om{5Fd#D3|X=j9u6`55fqtoT!9jm{r3P^Z$XjYw)!1Ih{0b+ZjkbUvP`kis<^LfxWOqxX4BG zq^|MN<9L1&vAGS8@H?+op=l{8<5rsCEnb&Di{+rudfCh6VuvwlnH
  • @6EJBXu3S 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 F{|7T1mU;jH diff --git a/public/images/pokemon/shiny/335.json b/public/images/pokemon/shiny/335.json index ca797f1d7a4..80c43b41c12 100644 --- a/public/images/pokemon/shiny/335.json +++ b/public/images/pokemon/shiny/335.json @@ -1,1910 +1,523 @@ -{ - "textures": [ - { - "image": "335.png", - "format": "RGBA8888", - "size": { - "w": 366, - "h": 366 - }, - "scale": 1, - "frames": [ - { - "filename": "0013.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0014.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0035.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0056.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0079.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 5, - "y": 0, - "w": 65, - "h": 66 - }, - "frame": { - "x": 0, - "y": 63, - "w": 65, - "h": 66 - } - }, - { - "filename": "0077.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 7, - "y": 3, - "w": 67, - "h": 63 - }, - "frame": { - "x": 68, - "y": 0, - "w": 67, - "h": 63 - } - }, - { - "filename": "0078.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 7, - "y": 3, - "w": 67, - "h": 63 - }, - "frame": { - "x": 68, - "y": 0, - "w": 67, - "h": 63 - } - }, - { - "filename": "0012.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0033.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0034.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0055.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0015.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0036.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0057.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0058.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0080.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 0, - "w": 61, - "h": 66 - }, - "frame": { - "x": 0, - "y": 192, - "w": 61, - "h": 66 - } - }, - { - "filename": "0085.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 61, - "h": 66 - }, - "frame": { - "x": 0, - "y": 258, - "w": 61, - "h": 66 - } - }, - { - "filename": "0086.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 61, - "h": 66 - }, - "frame": { - "x": 0, - "y": 258, - "w": 61, - "h": 66 - } - }, - { - "filename": "0069.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 0, - "y": 4, - "w": 64, - "h": 62 - }, - "frame": { - "x": 135, - "y": 0, - "w": 64, - "h": 62 - } - }, - { - "filename": "0070.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 0, - "y": 4, - "w": 64, - "h": 62 - }, - "frame": { - "x": 135, - "y": 0, - "w": 64, - "h": 62 - } - }, - { - "filename": "0076.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 65, - "h": 61 - }, - "frame": { - "x": 199, - "y": 0, - "w": 65, - "h": 61 - } - }, - { - "filename": "0011.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0032.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0053.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0054.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0009.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0010.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0031.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0052.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0071.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 0, - "y": 5, - "w": 64, - "h": 61 - }, - "frame": { - "x": 64, - "y": 183, - "w": 64, - "h": 61 - } - }, - { - "filename": "0087.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 1, - "w": 62, - "h": 65 - }, - "frame": { - "x": 61, - "y": 244, - "w": 62, - "h": 65 - } - }, - { - "filename": "0075.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 6, - "y": 9, - "w": 63, - "h": 57 - }, - "frame": { - "x": 61, - "y": 309, - "w": 63, - "h": 57 - } - }, - { - "filename": "0088.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 2, - "w": 61, - "h": 64 - }, - "frame": { - "x": 123, - "y": 244, - "w": 61, - "h": 64 - } - }, - { - "filename": "0072.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 63, - "h": 58 - }, - "frame": { - "x": 124, - "y": 308, - "w": 63, - "h": 58 - } - }, - { - "filename": "0008.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0029.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0030.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0051.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0001.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0002.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0003.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0004.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0005.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0006.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0007.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0016.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0021.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0022.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0023.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0024.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0025.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0026.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0027.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0028.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0037.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0038.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0043.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0044.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0045.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0046.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0047.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0048.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0049.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0050.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0059.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0064.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0073.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 9, - "w": 62, - "h": 57 - }, - "frame": { - "x": 187, - "y": 307, - "w": 62, - "h": 57 - } - }, - { - "filename": "0074.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 9, - "w": 62, - "h": 57 - }, - "frame": { - "x": 187, - "y": 307, - "w": 62, - "h": 57 - } - }, - { - "filename": "0068.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 3, - "w": 62, - "h": 63 - }, - "frame": { - "x": 264, - "y": 58, - "w": 62, - "h": 63 - } - }, - { - "filename": "0017.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0018.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0039.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0060.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0081.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 55, - "h": 66 - }, - "frame": { - "x": 188, - "y": 62, - "w": 55, - "h": 66 - } - }, - { - "filename": "0082.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 55, - "h": 66 - }, - "frame": { - "x": 188, - "y": 62, - "w": 55, - "h": 66 - } - }, - { - "filename": "0083.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 58, - "h": 66 - }, - "frame": { - "x": 189, - "y": 128, - "w": 58, - "h": 66 - } - }, - { - "filename": "0084.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 60, - "h": 66 - }, - "frame": { - "x": 247, - "y": 121, - "w": 60, - "h": 66 - } - }, - { - "filename": "0020.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0041.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0042.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0063.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0065.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0066.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0089.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0090.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0019.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0040.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0061.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0062.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0067.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 249, - "y": 250, - "w": 60, - "h": 63 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:0df67af080306e793f3e63687a642a63:bd66cef8682173381b002070c3411214:40bb9f4809624b12bf79bbfe664bea73$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0002.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0003.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0004.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0005.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0006.png", + "frame": { "x": 0, "y": 185, "w": 59, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 7, "w": 59, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0007.png", + "frame": { "x": 119, "y": 182, "w": 62, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 9, "w": 62, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0008.png", + "frame": { "x": 119, "y": 125, "w": 64, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 10, "w": 64, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0009.png", + "frame": { "x": 195, "y": 0, "w": 66, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 7, "w": 66, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0010.png", + "frame": { "x": 129, "y": 0, "w": 66, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 66, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0011.png", + "frame": { "x": 320, "y": 0, "w": 62, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 62, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0012.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0013.png", + "frame": { "x": 0, "y": 244, "w": 53, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 53, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0014.png", + "frame": { "x": 59, "y": 188, "w": 56, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 56, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0015.png", + "frame": { "x": 306, "y": 187, "w": 57, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 57, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0016.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0017.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0018.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0019.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0020.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0021.png", + "frame": { "x": 0, "y": 185, "w": 59, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 7, "w": 59, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0022.png", + "frame": { "x": 119, "y": 182, "w": 62, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 9, "w": 62, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0023.png", + "frame": { "x": 119, "y": 125, "w": 64, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 10, "w": 64, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0024.png", + "frame": { "x": 195, "y": 0, "w": 66, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 7, "w": 66, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0025.png", + "frame": { "x": 129, "y": 0, "w": 66, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 66, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0026.png", + "frame": { "x": 320, "y": 0, "w": 62, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 62, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0027.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0028.png", + "frame": { "x": 0, "y": 244, "w": 53, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 53, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0029.png", + "frame": { "x": 59, "y": 188, "w": 56, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 56, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0030.png", + "frame": { "x": 306, "y": 187, "w": 57, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 57, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0031.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0032.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0033.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0034.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0035.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0036.png", + "frame": { "x": 0, "y": 185, "w": 59, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 7, "w": 59, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0037.png", + "frame": { "x": 119, "y": 182, "w": 62, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 9, "w": 62, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0038.png", + "frame": { "x": 119, "y": 125, "w": 64, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 10, "w": 64, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0039.png", + "frame": { "x": 195, "y": 0, "w": 66, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 7, "w": 66, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0040.png", + "frame": { "x": 129, "y": 0, "w": 66, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 66, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0041.png", + "frame": { "x": 320, "y": 0, "w": 62, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 62, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0042.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0043.png", + "frame": { "x": 0, "y": 244, "w": 53, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 53, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0044.png", + "frame": { "x": 59, "y": 188, "w": 56, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 56, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0045.png", + "frame": { "x": 306, "y": 187, "w": 57, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 57, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0046.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0047.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0048.png", + "frame": { "x": 248, "y": 129, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0049.png", + "frame": { "x": 188, "y": 123, "w": 60, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 5, "w": 60, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0050.png", + "frame": { "x": 0, "y": 125, "w": 61, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 2, "y": 6, "w": 61, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0051.png", + "frame": { "x": 0, "y": 66, "w": 63, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 0, "y": 7, "w": 63, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0052.png", + "frame": { "x": 234, "y": 190, "w": 61, "h": 56 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 2, "y": 10, "w": 61, "h": 56 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0053.png", + "frame": { "x": 234, "y": 246, "w": 60, "h": 55 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 11, "w": 60, "h": 55 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0054.png", + "frame": { "x": 115, "y": 239, "w": 61, "h": 56 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 7, "y": 11, "w": 61, "h": 56 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0055.png", + "frame": { "x": 63, "y": 62, "w": 62, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 10, "y": 7, "w": 62, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0056.png", + "frame": { "x": 63, "y": 0, "w": 66, "h": 62 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 8, "y": 4, "w": 66, "h": 62 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0057.png", + "frame": { "x": 0, "y": 0, "w": 63, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 6, "y": 0, "w": 63, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0058.png", + "frame": { "x": 261, "y": 0, "w": 59, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 0, "w": 59, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0059.png", + "frame": { "x": 181, "y": 184, "w": 53, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 0, "w": 53, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0060.png", + "frame": { "x": 63, "y": 122, "w": 56, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 0, "w": 56, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0061.png", + "frame": { "x": 320, "y": 61, "w": 58, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 1, "w": 58, "h": 65 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0062.png", + "frame": { "x": 129, "y": 61, "w": 59, "h": 64 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 2, "w": 59, "h": 64 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0063.png", + "frame": { "x": 195, "y": 60, "w": 60, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 3, "w": 60, "h": 63 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0064.png", + "frame": { "x": 255, "y": 66, "w": 59, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 3, "w": 59, "h": 63 }, + "sourceSize": { "w": 74, "h": 67 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.9.2-x64", + "image": "335.png", + "format": "I8", + "size": { "w": 382, "h": 305 }, + "scale": "1" + } } diff --git a/public/images/pokemon/shiny/335.png b/public/images/pokemon/shiny/335.png index 765344af6fd1936a10affb0e3e67229d7927dccf..fc7c325a46927c5b82aa214f1446a66b73e69d54 100644 GIT binary patch literal 10990 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 b3S)~T@p8Y3ar5ciDd3%wmSVNMRmlGZJ}mxb literal 24209 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 Date: Mon, 10 Feb 2025 21:17:01 -0500 Subject: [PATCH 11/18] Fix Greninja floating static backsprite (#5292) --- public/images/pokemon/back/658.json | 10 +++++----- public/images/pokemon/back/658.png | Bin 899 -> 890 bytes public/images/pokemon/back/shiny/658.json | 8 ++++---- public/images/pokemon/back/shiny/658.png | Bin 899 -> 890 bytes 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/public/images/pokemon/back/658.json b/public/images/pokemon/back/658.json index 050b63e3592..1d8893e2d5d 100644 --- a/public/images/pokemon/back/658.json +++ b/public/images/pokemon/back/658.json @@ -1,19 +1,19 @@ { "frames": [ { "filename": "0001.png", - "frame": { "x": 0, "y": 0, "w": 77, "h": 77 }, + "frame": { "x": 0, "y": 0, "w": 77, "h": 65 }, "rotated": false, "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 77 }, - "sourceSize": { "w": 77, "h": 77 }, + "spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 65 }, + "sourceSize": { "w": 77, "h": 65 }, "duration": 100 } ], "meta": { "app": "https://www.aseprite.org/", - "version": "1.3.7-x64", + "version": "1.3.9.2-x64", "format": "I8", - "size": { "w": 77, "h": 77 }, + "size": { "w": 77, "h": 65 }, "scale": "1" } } diff --git a/public/images/pokemon/back/658.png b/public/images/pokemon/back/658.png index ea24d9a63360e565b83f9eb6b08385d1d4f04df2..be286b886663611d6eb953682e17ed48ffc393fd 100644 GIT binary patch delta 796 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 ji~A|;8{hcGKgTc0Js Date: Mon, 10 Feb 2025 21:33:13 -0600 Subject: [PATCH 12/18] [Bug] Fix Fused Pokemon not having stats Flipped correctly (#5295) Co-authored-by: Scooom --- src/field/pokemon.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 80a2980c92b..79d7192b4db 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1066,6 +1066,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { globalScene.applyModifiers(PokemonBaseStatFlatModifier, this.isPlayer(), this, baseStats); if (this.isFusion()) { const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats; + applyChallenges(globalScene.gameMode, ChallengeType.FLIP_STAT, this, fusionBaseStats); + for (const s of PERMANENT_STATS) { baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2); } From 42c4ca27e6cf171c3ebc7900e58aca5062132e14 Mon Sep 17 00:00:00 2001 From: geeilhan <107366005+geeilhan@users.noreply.github.com> Date: Tue, 11 Feb 2025 07:53:37 +0100 Subject: [PATCH 13/18] [Ability][Move] Last Respects Refactor and Full Implementation (#5200) * full implementation of supreme overlord + test * removed unused import * changed documentation since Battle.playerFaints is not used in supreme overlord * Update faint-phase.ts * changed supreme overlords power calculation function and adjusted tests * added changes to Last Respects too * added playerFaints to SessionSaveData to make the counter saveable * Apply Kev's suggestions Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Reset enemy faint counter per battle * Re-mark supreme overlord as partial * added automated test case * moved playerFaints reset to resetArenaEffects * removed resetEnemyFaintCount() function since it is unused --------- Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/battle-scene.ts | 4 +- src/battle.ts | 12 +- src/data/ability.ts | 4 +- src/data/move.ts | 3 +- src/field/arena.ts | 13 +- src/phases/faint-phase.ts | 5 +- src/phases/game-over-phase.ts | 3 +- src/system/game-data.ts | 9 +- src/test/abilities/supreme_overlord.test.ts | 178 ++++++++++++++++ src/test/moves/last_respects.test.ts | 219 ++++++++++++++++++++ 10 files changed, 432 insertions(+), 18 deletions(-) create mode 100644 src/test/abilities/supreme_overlord.test.ts create mode 100644 src/test/moves/last_respects.test.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 9bfa153ef60..7aa0369877b 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1401,8 +1401,8 @@ export default class BattleScene extends SceneBase { return this.currentBattle; } - newArena(biome: Biome): Arena { - this.arena = new Arena(biome, Biome[biome].toLowerCase()); + newArena(biome: Biome, playerFaints?: number): Arena { + this.arena = new Arena(biome, Biome[biome].toLowerCase(), playerFaints); this.eventTarget.dispatchEvent(new NewArenaEvent()); this.arenaBg.pipelineData = { terrainColorRatio: this.arena.getBgTerrainColorRatioForBiome() }; diff --git a/src/battle.ts b/src/battle.ts index 3f36865c74b..7ede7b2982e 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -105,9 +105,11 @@ export default class Battle { public lastEnemyInvolved: number; public lastPlayerInvolved: number; public lastUsedPokeball: PokeballType | null = null; - /** The number of times a Pokemon on the player's side has fainted this battle */ - public playerFaints: number = 0; - /** The number of times a Pokemon on the enemy's side has fainted this battle */ + /** + * Saves the number of times a Pokemon on the enemy's side has fainted during this battle. + * This is saved here since we encounter a new enemy every wave. + * {@linkcode globalScene.arena.playerFaints} is the corresponding faint counter for the player and needs to be save across waves (reset every arena encounter). + */ public enemyFaints: number = 0; public playerFaintsHistory: FaintLogEntry[] = []; public enemyFaintsHistory: FaintLogEntry[] = []; @@ -118,7 +120,7 @@ export default class Battle { private rngCounter: number = 0; - constructor(gameMode: GameMode, waveIndex: number, battleType: BattleType, trainer?: Trainer, double?: boolean) { + constructor(gameMode: GameMode, waveIndex: number, battleType: BattleType, trainer?: Trainer, double: boolean = false) { this.gameMode = gameMode; this.waveIndex = waveIndex; this.battleType = battleType; @@ -127,7 +129,7 @@ export default class Battle { this.enemyLevels = battleType !== BattleType.TRAINER ? new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave()) : trainer?.getPartyLevels(this.waveIndex); - this.double = double ?? false; + this.double = double; } private initBattleSpec(): void { diff --git a/src/data/ability.ts b/src/data/ability.ts index 21ec5667426..cd31c62f7f6 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -6313,8 +6313,8 @@ export function initAbilities() { new Ability(Abilities.SHARPNESS, 9) .attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), new Ability(Abilities.SUPREME_OVERLORD, 9) - .attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.currentBattle.playerFaints : globalScene.currentBattle.enemyFaints, 5)) - .partial(), // Counter resets every wave instead of on arena reset + .attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5)) + .partial(), // Should only boost once, on summon new Ability(Abilities.COSTAR, 9) .attr(PostSummonCopyAllyStatsAbAttr), new Ability(Abilities.TOXIC_DEBRIS, 9) diff --git a/src/data/move.ts b/src/data/move.ts index 967d2ee0cc2..6c41f0b764d 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -10916,8 +10916,7 @@ export function initMoves() { .attr(ConfuseAttr) .recklessMove(), new AttackMove(Moves.LAST_RESPECTS, Type.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9) - .partial() // Counter resets every wave instead of on arena reset - .attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? globalScene.currentBattle.playerFaints : globalScene.currentBattle.enemyFaints, 100)) + .attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 100)) .makesContact(false), new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), diff --git a/src/field/arena.ts b/src/field/arena.ts index 67b83e9518f..5ee065d71dc 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -44,6 +44,11 @@ export class Arena { public bgm: string; public ignoreAbilities: boolean; public ignoringEffectSource: BattlerIndex | null; + /** + * Saves the number of times a party pokemon faints during a arena encounter. + * {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave). + */ + public playerFaints: number; private lastTimeOfDay: TimeOfDay; @@ -52,12 +57,13 @@ export class Arena { public readonly eventTarget: EventTarget = new EventTarget(); - constructor(biome: Biome, bgm: string) { + constructor(biome: Biome, bgm: string, playerFaints: number = 0) { this.biomeType = biome; this.tags = []; this.bgm = bgm; this.trainerPool = biomeTrainerPools[biome]; this.updatePoolsForTimeOfDay(); + this.playerFaints = playerFaints; } init() { @@ -688,6 +694,7 @@ export class Arena { this.trySetWeather(WeatherType.NONE, false); } this.trySetTerrain(TerrainType.NONE, false, true); + this.resetPlayerFaintCount(); this.removeAllTags(); } @@ -773,6 +780,10 @@ export class Arena { return 0; } } + + resetPlayerFaintCount(): void { + this.playerFaints = 0; + } } export function getBiomeKey(biome: Biome): string { diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 414aa84ce6c..340c5362087 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -96,10 +96,9 @@ export class FaintPhase extends PokemonPhase { doFaint(): void { const pokemon = this.getPokemon(); - - // Track total times pokemon have been KO'd for supreme overlord/last respects + // Track total times pokemon have been KO'd for Last Respects/Supreme Overlord if (pokemon.isPlayer()) { - globalScene.currentBattle.playerFaints += 1; + globalScene.arena.playerFaints += 1; globalScene.currentBattle.playerFaintsHistory.push({ pokemon: pokemon, turn: globalScene.currentBattle.turn }); } else { globalScene.currentBattle.enemyFaints += 1; diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 5e4e8e1cdf7..d4b529fe00e 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -249,7 +249,8 @@ export class GameOverPhase extends BattlePhase { timestamp: new Date().getTime(), challenges: globalScene.gameMode.challenges.map(c => new ChallengeData(c)), mysteryEncounterType: globalScene.currentBattle.mysteryEncounter?.encounterType ?? -1, - mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData + mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData, + playerFaints: globalScene.arena.playerFaints } as SessionSaveData; } } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 7282d2730a4..c16fab9db04 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -141,6 +141,10 @@ export interface SessionSaveData { challenges: ChallengeData[]; mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME, mysteryEncounterSaveData: MysteryEncounterSaveData; + /** + * Counts the amount of pokemon fainted in your party during the current arena encounter. + */ + playerFaints: number; } interface Unlocks { @@ -964,7 +968,8 @@ export class GameData { timestamp: new Date().getTime(), challenges: globalScene.gameMode.challenges.map(c => new ChallengeData(c)), mysteryEncounterType: globalScene.currentBattle.mysteryEncounter?.encounterType ?? -1, - mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData + mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData, + playerFaints: globalScene.arena.playerFaints } as SessionSaveData; } @@ -1056,7 +1061,7 @@ export class GameData { globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData); - globalScene.newArena(sessionData.arena.biome); + globalScene.newArena(sessionData.arena.biome, sessionData.playerFaints); const battleType = sessionData.battleType || 0; const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null; diff --git a/src/test/abilities/supreme_overlord.test.ts b/src/test/abilities/supreme_overlord.test.ts new file mode 100644 index 00000000000..ecd595cb6bb --- /dev/null +++ b/src/test/abilities/supreme_overlord.test.ts @@ -0,0 +1,178 @@ +import { Moves } from "#app/enums/moves"; +import { Abilities } from "#enums/abilities"; +import { Species } from "#enums/species"; +import { BattlerIndex } from "#app/battle"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { allMoves } from "#app/data/move"; + +describe("Abilities - Supreme Overlord", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + const move = allMoves[Moves.TACKLE]; + const basePower = move.power; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .enemySpecies(Species.MAGIKARP) + .enemyLevel(100) + .startingLevel(1) + .enemyAbility(Abilities.BALL_FETCH) + .ability(Abilities.SUPREME_OVERLORD) + .enemyMoveset([ Moves.SPLASH ]) + .moveset([ Moves.TACKLE, Moves.EXPLOSION, Moves.LUNAR_DANCE ]); + + vi.spyOn(move, "calculateBattlePower"); + }); + + it("should increase Power by 20% if 2 Pokemon are fainted in the party", async() => { + await game.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(2); + await game.toNextTurn(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(move.calculateBattlePower).toHaveReturnedWith(basePower * 1.2); + }); + + it("should increase Power by 30% if an ally fainted twice and another one once", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + /** + * Bulbasur faints once + */ + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Charmander faints once + */ + game.doRevivePokemon(1); + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Bulbasur faints twice + */ + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(2); + await game.toNextTurn(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(move.calculateBattlePower).toHaveReturnedWith(basePower * 1.3); + }); + + it("should maintain its power during next battle if it is within the same arena encounter", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(1) + .enemyLevel(1) + .startingLevel(100); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + /** + * The first Pokemon faints and another Pokemon in the party is selected. + */ + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Enemy Pokemon faints and new wave is entered. + */ + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower * 1.1); + }); + + it("should reset playerFaints count if we enter new trainer battle", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(4) + .enemyLevel(1) + .startingLevel(100); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); + + it("should reset playerFaints count if we enter new biome", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(10) + .enemyLevel(1) + .startingLevel(100); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); +}); diff --git a/src/test/moves/last_respects.test.ts b/src/test/moves/last_respects.test.ts new file mode 100644 index 00000000000..71a76e3fa1a --- /dev/null +++ b/src/test/moves/last_respects.test.ts @@ -0,0 +1,219 @@ +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import { allMoves } from "#app/data/move"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Last Respects", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + const move = allMoves[Moves.LAST_RESPECTS]; + const basePower = move.power; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .disableCrits() + .moveset([ Moves.LAST_RESPECTS, Moves.EXPLOSION, Moves.LUNAR_DANCE ]) + .ability(Abilities.BALL_FETCH) + .enemyAbility(Abilities.BALL_FETCH) + .enemySpecies(Species.MAGIKARP) + .enemyMoveset(Moves.SPLASH) + .startingLevel(1) + .enemyLevel(100); + + vi.spyOn(move, "calculateBattlePower"); + }); + + it("should have 150 power if 2 allies faint before using move", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + /** + * Bulbasur faints once + */ + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Charmander faints once + */ + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(2); + await game.toNextTurn(); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(move.calculateBattlePower).toHaveReturnedWith(basePower + (2 * 50)); + }); + + it("should have 200 power if an ally fainted twice and another one once", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + /** + * Bulbasur faints once + */ + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Charmander faints once + */ + game.doRevivePokemon(1); + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Bulbasur faints twice + */ + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(2); + await game.toNextTurn(); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(move.calculateBattlePower).toHaveReturnedWith(basePower + (3 * 50)); + }); + + it("should maintain its power for the player during the next battle if it is within the same arena encounter", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(1) + .enemyLevel(1) + .startingLevel(100) + .enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + /** + * The first Pokemon faints and another Pokemon in the party is selected. + */ + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Enemy Pokemon faints and new wave is entered. + */ + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + expect(game.scene.arena.playerFaints).toBe(1); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEndPhase"); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower + (1 * 50)); + }); + + it("should reset enemyFaints count on progressing to the next wave.", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(1) + .enemyLevel(1) + .startingLevel(100) + .enemyMoveset(Moves.LAST_RESPECTS) + .moveset([ Moves.LUNAR_DANCE, Moves.LAST_RESPECTS, Moves.SPLASH ]); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + /** + * The first Pokemon faints and another Pokemon in the party is selected. + */ + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Enemy Pokemon faints and new wave is entered. + */ + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + expect(game.scene.currentBattle.enemyFaints).toBe(0); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("MoveEndPhase"); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); + + it("should reset playerFaints count if we enter new trainer battle", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(4) + .enemyLevel(1) + .startingLevel(100); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); + + it("should reset playerFaints count if we enter new biome", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(10) + .enemyLevel(1) + .startingLevel(100); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); +}); From f5ef4a5da91ab5139c0036b6731d34613ab373fd Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Mon, 10 Feb 2025 23:14:54 -0800 Subject: [PATCH 14/18] [Test] Fix Tera Blast test (#5297) --- src/test/moves/tera_blast.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/moves/tera_blast.test.ts b/src/test/moves/tera_blast.test.ts index 21cbf4c1463..34d171b47bb 100644 --- a/src/test/moves/tera_blast.test.ts +++ b/src/test/moves/tera_blast.test.ts @@ -38,8 +38,8 @@ describe("Moves - Tera Blast", () => { .startingHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }]) .enemySpecies(Species.MAGIKARP) .enemyMoveset(Moves.SPLASH) - .enemyAbility(Abilities.BALL_FETCH) - .enemyLevel(20); + .enemyAbility(Abilities.STURDY) + .enemyLevel(50); vi.spyOn(moveToCheck, "calculateBattlePower"); }); From 60b27f4f62789a056eca644ecb6ee9d61e7b6eb1 Mon Sep 17 00:00:00 2001 From: Wlowscha <54003515+Wlowscha@users.noreply.github.com> Date: Tue, 11 Feb 2025 08:32:32 +0100 Subject: [PATCH 15/18] [UI/UX] Pokedex updates batch (#5282) * Introducing tray to display form icons in the pokedex; displaying correct information for uncaught and seen forms in pokedex page; dexForDevs now unlocks everything in the main page * Filtering correctly passive abilities and form abilities. Passive candy symbol is now colored * Pikachu does not break the dex due to having no passive * Fixed position of pokemonFormText * Added button instructions to show forms * Allowing candy upgrades for evolutions; too expensive options shown in shadow text * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fixed game crashing after save and quit * Updating import of BBCodeText * Restoring name on dex page * getStarterSpecies now looks at speciesStarterCosts to determine what is a starter instead of looking at game data (exception for Pikachu) * Selecting pokedex option in starter select menu does not play error sound * Mons having no TM moves don't freeze the game in the dex * Menu in pokedex page is not pushed to the left when localized options are long * Removed spurious globalScene.clearPhaseQueue() call * Showing error message when clicking tm option if no tm moves are available * Egg move icon and passive icon are darkened when filtering if the respective move or passive has not been unlocked * Hiding form button when switching to filters * Hiding "Show forms" button while forms are being shown --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/ui/abstact-option-select-ui-handler.ts | 2 +- src/ui/pokedex-mon-container.ts | 48 +- src/ui/pokedex-page-ui-handler.ts | 525 +++++++++++---------- src/ui/pokedex-ui-handler.ts | 343 ++++++++++++-- src/ui/starter-select-ui-handler.ts | 2 +- 5 files changed, 626 insertions(+), 294 deletions(-) diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index 1840792e667..10dbedd7b2f 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -6,7 +6,7 @@ import { addWindow } from "./ui-theme"; import * as Utils from "../utils"; import { argbFromRgba } from "@material/material-color-utilities"; import { Button } from "#enums/buttons"; -import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; +import BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText"; export interface OptionSelectConfig { xOffset?: number; diff --git a/src/ui/pokedex-mon-container.ts b/src/ui/pokedex-mon-container.ts index f3932aa90c8..31a98c30d1c 100644 --- a/src/ui/pokedex-mon-container.ts +++ b/src/ui/pokedex-mon-container.ts @@ -1,7 +1,17 @@ +import type { Variant } from "#app/data/variant"; import { globalScene } from "#app/global-scene"; +import { isNullOrUndefined } from "#app/utils"; import type PokemonSpecies from "../data/pokemon-species"; import { addTextObject, TextStyle } from "./text"; + +interface SpeciesDetails { + shiny?: boolean, + formIndex?: number + female?: boolean, + variant?: Variant +} + export class PokedexMonContainer extends Phaser.GameObjects.Container { public species: PokemonSpecies; public icon: Phaser.GameObjects.Sprite; @@ -19,16 +29,34 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container { public tmMove2Icon: Phaser.GameObjects.Image; public passive1Icon: Phaser.GameObjects.Image; public passive2Icon: Phaser.GameObjects.Image; + public passive1OverlayIcon: Phaser.GameObjects.Image; + public passive2OverlayIcon: Phaser.GameObjects.Image; public cost: number = 0; - constructor(species: PokemonSpecies) { + constructor(species: PokemonSpecies, options: SpeciesDetails = {}) { super(globalScene, 0, 0); this.species = species; + const { shiny, formIndex, female, variant } = options; + const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, false, true); const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); + if (!isNullOrUndefined(formIndex)) { + defaultProps.formIndex = formIndex; + } + if (!isNullOrUndefined(shiny)) { + defaultProps.shiny = shiny; + } + if (!isNullOrUndefined(variant)) { + defaultProps.variant = variant; + } + if (!isNullOrUndefined(female)) { + defaultProps.female = female; + } + + // starter passive bg const starterPassiveBg = globalScene.add.image(2, 5, "passive_bg"); starterPassiveBg.setOrigin(0, 0); @@ -137,7 +165,7 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container { this.tmMove2Icon = tmMove2Icon; - // move icons + // passive icons const passive1Icon = globalScene.add.image(3, 3, "candy"); passive1Icon.setOrigin(0, 0); passive1Icon.setScale(0.25); @@ -145,13 +173,27 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container { this.add(passive1Icon); this.passive1Icon = passive1Icon; - // move icons + const passive1OverlayIcon = globalScene.add.image(12, 12, "candy_overlay"); + passive1OverlayIcon.setOrigin(0, 0); + passive1OverlayIcon.setScale(0.25); + passive1OverlayIcon.setVisible(false); + this.add(passive1OverlayIcon); + this.passive1OverlayIcon = passive1OverlayIcon; + + // passive icons const passive2Icon = globalScene.add.image(12, 3, "candy"); passive2Icon.setOrigin(0, 0); passive2Icon.setScale(0.25); passive2Icon.setVisible(false); this.add(passive2Icon); this.passive2Icon = passive2Icon; + + const passive2OverlayIcon = globalScene.add.image(12, 12, "candy_overlay"); + passive2OverlayIcon.setOrigin(0, 0); + passive2OverlayIcon.setScale(0.25); + passive2OverlayIcon.setVisible(false); + this.add(passive2OverlayIcon); + this.passive2OverlayIcon = passive2OverlayIcon; } checkIconId(female, formIndex, shiny, variant) { diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index 7ab054ea71b..2047095d067 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -43,7 +43,6 @@ import type { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Button } from "#enums/buttons"; import { EggSourceType } from "#enums/egg-source-types"; -import { StarterContainer } from "#app/ui/starter-container"; import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters"; import { BooleanHolder, capitalizeString, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, rgbHexToRgba, toReadableString } from "#app/utils"; import type { Nature } from "#enums/nature"; @@ -128,7 +127,6 @@ interface SpeciesDetails { formIndex?: number female?: boolean, variant?: number, - forSeen?: boolean, // default = false } enum MenuOptions { @@ -147,8 +145,6 @@ enum MenuOptions { export default class PokedexPageUiHandler extends MessageUiHandler { private starterSelectContainer: Phaser.GameObjects.Container; private shinyOverlay: Phaser.GameObjects.Image; - private starterContainers: StarterContainer[] = []; - private filteredStarterContainers: StarterContainer[] = []; private pokemonNumberText: Phaser.GameObjects.Text; private pokemonSprite: Phaser.GameObjects.Sprite; private pokemonNameText: Phaser.GameObjects.Text; @@ -199,6 +195,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { private allSpecies: PokemonSpecies[] = []; private species: PokemonSpecies; + private starterId: number; private formIndex: number; private speciesLoaded: Map = new Map(); private levelMoves: LevelMoves; @@ -312,10 +309,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.speciesLoaded.set(species.speciesId, false); this.allSpecies.push(species); - - const starterContainer = new StarterContainer(species).setVisible(false); - this.starterContainers.push(starterContainer); - starterBoxContainer.add(starterContainer); } this.starterSelectContainer.add(starterBoxContainer); @@ -513,7 +506,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.scale = getTextStyleOptions(TextStyle.WINDOW, globalScene.uiTheme).scale; this.menuBg = addWindow( - (globalScene.game.canvas.width / 6) - (this.optionSelectText.displayWidth + 25), + (globalScene.game.canvas.width / 6 - 83), 0, this.optionSelectText.displayWidth + 19 + 24 * this.scale, (globalScene.game.canvas.height / 6) - 2 @@ -555,8 +548,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { // Filter bar sits above everything, except the message box this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer); - - this.updateInstructions(); } show(args: any[]): boolean { @@ -603,6 +594,8 @@ export default class PokedexPageUiHandler extends MessageUiHandler { const species = this.species; const formIndex = this.formIndex ?? 0; + this.starterId = this.getStarterSpeciesId(this.species.speciesId); + const allEvolutions = pokemonEvolutions.hasOwnProperty(species.speciesId) ? pokemonEvolutions[species.speciesId] : []; if (species.forms.length > 0) { @@ -629,17 +622,19 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.baseTotal = species.baseTotal; } - this.eggMoves = speciesEggMoves[this.getStarterSpeciesId(species.speciesId)] ?? []; - this.hasEggMoves = Array.from({ length: 4 }, (_, em) => (globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)].eggMoves & (1 << em)) !== 0); + this.eggMoves = speciesEggMoves[this.starterId] ?? []; + this.hasEggMoves = Array.from({ length: 4 }, (_, em) => (globalScene.gameData.starterData[this.starterId].eggMoves & (1 << em)) !== 0); const formKey = this.species?.forms.length > 0 ? this.species.forms[this.formIndex].formKey : ""; this.tmMoves = speciesTmMoves[species.speciesId]?.filter(m => Array.isArray(m) ? (m[0] === formKey ? true : false ) : true) .map(m => Array.isArray(m) ? m[1] : m).sort((a, b) => allMoves[a].name > allMoves[b].name ? 1 : -1) ?? []; - const passives = starterPassiveAbilities[this.getStarterSpeciesId(species.speciesId)]; + const passiveId = starterPassiveAbilities.hasOwnProperty(species.speciesId) ? species.speciesId : + starterPassiveAbilities.hasOwnProperty(this.starterId) ? this.starterId : pokemonPrevolutions[this.starterId]; + const passives = starterPassiveAbilities[passiveId]; this.passive = (this.formIndex in passives) ? passives[formIndex] : passives[0]; - const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)]; + const starterData = globalScene.gameData.starterData[this.starterId]; const abilityAttr = starterData.abilityAttr; this.hasPassive = starterData.passiveAttr > 0; @@ -655,9 +650,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler { const allBiomes = catchableSpecies[species.speciesId] ?? []; this.preBiomes = this.sanitizeBiomes( - (catchableSpecies[this.getStarterSpeciesId(species.speciesId)] ?? []) + (catchableSpecies[this.starterId] ?? []) .filter(b => !allBiomes.some(bm => (b.biome === bm.biome && b.tier === bm.tier)) && !(b.biome === Biome.TOWN)), - this.getStarterSpeciesId(species.speciesId)); + this.starterId); this.biomes = this.sanitizeBiomes(allBiomes, species.speciesId); const allFormChanges = pokemonFormChanges.hasOwnProperty(species.speciesId) ? pokemonFormChanges[species.speciesId] : []; @@ -799,39 +794,43 @@ export default class PokedexPageUiHandler extends MessageUiHandler { const hasShiny = caughtAttr & DexAttr.SHINY; const hasNonShiny = caughtAttr & DexAttr.NON_SHINY; - if (starterAttributes.shiny && !hasShiny) { + if (!hasShiny || (starterAttributes.shiny === undefined && hasNonShiny)) { // shiny form wasn't unlocked, purging shiny and variant setting starterAttributes.shiny = false; starterAttributes.variant = 0; - } else if (starterAttributes.shiny === false && !hasNonShiny) { - // non shiny form wasn't unlocked, purging shiny setting - starterAttributes.shiny = false; + } else if (!hasNonShiny || (starterAttributes.shiny === undefined && hasShiny)) { + starterAttributes.shiny = true; + starterAttributes.variant = 0; } - if (starterAttributes.variant !== undefined) { - const unlockedVariants = [ - hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT, - hasShiny && caughtAttr & DexAttr.VARIANT_2, - hasShiny && caughtAttr & DexAttr.VARIANT_3 - ]; - if (isNaN(starterAttributes.variant) || starterAttributes.variant < 0) { - starterAttributes.variant = 0; - } else if (!unlockedVariants[starterAttributes.variant]) { - let highestValidIndex = -1; - for (let i = 0; i <= starterAttributes.variant && i < unlockedVariants.length; i++) { - if (unlockedVariants[i] !== 0n) { - highestValidIndex = i; - } + const unlockedVariants = [ + hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT, + hasShiny && caughtAttr & DexAttr.VARIANT_2, + hasShiny && caughtAttr & DexAttr.VARIANT_3 + ]; + if (starterAttributes.variant === undefined || isNaN(starterAttributes.variant) || starterAttributes.variant < 0) { + starterAttributes.variant = 0; + } else if (!unlockedVariants[starterAttributes.variant]) { + let highestValidIndex = -1; + for (let i = 0; i <= starterAttributes.variant && i < unlockedVariants.length; i++) { + if (unlockedVariants[i] !== 0n) { + highestValidIndex = i; } - // Set to the highest valid index found or default to 0 - starterAttributes.variant = highestValidIndex !== -1 ? highestValidIndex : 0; } + // Set to the highest valid index found or default to 0 + starterAttributes.variant = highestValidIndex !== -1 ? highestValidIndex : 0; } if (starterAttributes.female !== undefined) { if ((starterAttributes.female && !(caughtAttr & DexAttr.FEMALE)) || (!starterAttributes.female && !(caughtAttr & DexAttr.MALE))) { starterAttributes.female = !starterAttributes.female; } + } else { + if (caughtAttr & DexAttr.FEMALE) { + starterAttributes.female = true; + } else if (caughtAttr & DexAttr.MALE) { + starterAttributes.female = false; + } } return starterAttributes; @@ -878,7 +877,14 @@ export default class PokedexPageUiHandler extends MessageUiHandler { * @returns the id of the corresponding starter */ getStarterSpeciesId(speciesId): number { - if (globalScene.gameData.starterData.hasOwnProperty(speciesId)) { + if (speciesId === Species.PIKACHU) { + if ([ 0, 1, 8 ].includes(this.formIndex)) { + return Species.PICHU; + } else { + return Species.PIKACHU; + } + } + if (speciesStarterCosts.hasOwnProperty(speciesId)) { return speciesId; } else { return pokemonStarters[speciesId]; @@ -886,7 +892,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { } getStarterSpecies(species): PokemonSpecies { - if (globalScene.gameData.starterData.hasOwnProperty(species.speciesId)) { + if (speciesStarterCosts.hasOwnProperty(species.speciesId)) { return species; } else { return allSpecies.find(sp => sp.speciesId === pokemonStarters[species.speciesId]) ?? species; @@ -970,7 +976,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { } } else { - const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(this.species.speciesId)]; + const starterData = globalScene.gameData.starterData[this.starterId]; // prepare persistent starter data to store changes const starterAttributes = this.starterAttributes; @@ -1126,6 +1132,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler { if (!isCaught || !isFormCaught) { error = true; + } else if (this.tmMoves.length < 1) { + ui.showText(i18next.t("pokedexUiHandler:noTmMoves")); + error = true; } else { this.blockInput = true; @@ -1633,90 +1642,55 @@ export default class PokedexPageUiHandler extends MessageUiHandler { error = true; } else { const ui = this.getUi(); + ui.showText(""); const options: any[] = []; // TODO: add proper type const passiveAttr = starterData.passiveAttr; const candyCount = starterData.candyCount; - if (!pokemonPrevolutions.hasOwnProperty(this.species.speciesId)) { - if (!(passiveAttr & PassiveAttr.UNLOCKED)) { - const passiveCost = getPassiveCandyCount(speciesStarterCosts[this.getStarterSpeciesId(this.species.speciesId)]); - options.push({ - label: `x${passiveCost} ${i18next.t("pokedexUiHandler:unlockPassive")} (${allAbilities[this.passive].name})`, - handler: () => { - if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= passiveCost) { - starterData.passiveAttr |= PassiveAttr.UNLOCKED | PassiveAttr.ENABLED; - if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { - starterData.candyCount -= passiveCost; - } - this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); - globalScene.gameData.saveSystem().then(success => { - if (!success) { - return globalScene.reset(true); - } - }); - ui.setMode(Mode.POKEDEX_PAGE, "refresh"); - this.setSpeciesDetails(this.species); - globalScene.playSound("se/buy"); - - return true; - } - return false; - }, - item: "candy", - itemArgs: starterColors[this.getStarterSpeciesId(this.species.speciesId)] - }); - } - - // Reduce cost option - const valueReduction = starterData.valueReduction; - if (valueReduction < valueReductionMax) { - const reductionCost = getValueReductionCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(this.species.speciesId)])[valueReduction]; - options.push({ - label: `x${reductionCost} ${i18next.t("pokedexUiHandler:reduceCost")}`, - handler: () => { - if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= reductionCost) { - starterData.valueReduction++; - if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { - starterData.candyCount -= reductionCost; - } - this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); - globalScene.gameData.saveSystem().then(success => { - if (!success) { - return globalScene.reset(true); - } - }); - ui.setMode(Mode.POKEDEX_PAGE, "refresh"); - globalScene.playSound("se/buy"); - - return true; - } - return false; - }, - item: "candy", - itemArgs: starterColors[this.getStarterSpeciesId(this.species.speciesId)] - }); - } - - // Same species egg menu option. - const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(this.species.speciesId)]); + if (!(passiveAttr & PassiveAttr.UNLOCKED)) { + const passiveCost = getPassiveCandyCount(speciesStarterCosts[this.starterId]); options.push({ - label: `x${sameSpeciesEggCost} ${i18next.t("pokedexUiHandler:sameSpeciesEgg")}`, + label: `x${passiveCost} ${i18next.t("pokedexUiHandler:unlockPassive")} (${allAbilities[this.passive].name})`, handler: () => { - if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost) { - if (globalScene.gameData.eggs.length >= 99 && !Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) { - // Egg list full, show error message at the top of the screen and abort - this.showText(i18next.t("egg:tooManyEggs"), undefined, () => this.showText("", 0, () => this.tutorialActive = false), 2000, false, undefined, true); - return false; - } + if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= passiveCost) { + starterData.passiveAttr |= PassiveAttr.UNLOCKED | PassiveAttr.ENABLED; if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { - starterData.candyCount -= sameSpeciesEggCost; + starterData.candyCount -= passiveCost; } this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); + globalScene.gameData.saveSystem().then(success => { + if (!success) { + return globalScene.reset(true); + } + }); + this.setSpeciesDetails(this.species); + globalScene.playSound("se/buy"); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); - const egg = new Egg({ scene: globalScene, species: this.species.speciesId, sourceType: EggSourceType.SAME_SPECIES_EGG }); - egg.addEggToGameData(); + return true; + } + return false; + }, + style: this.isPassiveAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, + item: "candy", + itemArgs: this.isPassiveAvailable() ? starterColors[this.starterId] : [ "808080", "808080" ] + }); + } + // Reduce cost option + const valueReduction = starterData.valueReduction; + if (valueReduction < valueReductionMax) { + const reductionCost = getValueReductionCandyCounts(speciesStarterCosts[this.starterId])[valueReduction]; + options.push({ + label: `x${reductionCost} ${i18next.t("pokedexUiHandler:reduceCost")}`, + handler: () => { + if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= reductionCost) { + starterData.valueReduction++; + if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { + starterData.candyCount -= reductionCost; + } + this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); globalScene.gameData.saveSystem().then(success => { if (!success) { return globalScene.reset(true); @@ -1729,24 +1703,59 @@ export default class PokedexPageUiHandler extends MessageUiHandler { } return false; }, + style: this.isValueReductionAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, item: "candy", - itemArgs: starterColors[this.getStarterSpeciesId(this.species.speciesId)] + itemArgs: this.isValueReductionAvailable() ? starterColors[this.starterId] : [ "808080", "808080" ] }); - options.push({ - label: i18next.t("menu:cancel"), - handler: () => { + } + + // Same species egg menu option. + const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.starterId]); + options.push({ + label: `x${sameSpeciesEggCost} ${i18next.t("pokedexUiHandler:sameSpeciesEgg")}`, + handler: () => { + if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost) { + if (globalScene.gameData.eggs.length >= 99 && !Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) { + // Egg list full, show error message at the top of the screen and abort + this.showText(i18next.t("egg:tooManyEggs"), undefined, () => this.showText("", 0, () => this.tutorialActive = false), 2000, false, undefined, true); + return false; + } + if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { + starterData.candyCount -= sameSpeciesEggCost; + } + this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); + + const egg = new Egg({ scene: globalScene, species: this.species.speciesId, sourceType: EggSourceType.SAME_SPECIES_EGG }); + egg.addEggToGameData(); + + globalScene.gameData.saveSystem().then(success => { + if (!success) { + return globalScene.reset(true); + } + }); ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + globalScene.playSound("se/buy"); + return true; } - }); - ui.setModeWithoutClear(Mode.OPTION_SELECT, { - options: options, - yOffset: 47 - }); - success = true; - } else { - error = true; - } + return false; + }, + style: this.isSameSpeciesEggAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, + item: "candy", + itemArgs: this.isSameSpeciesEggAvailable() ? starterColors[this.starterId] : [ "808080", "808080" ] + }); + options.push({ + label: i18next.t("menu:cancel"), + handler: () => { + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + return true; + } + }); + ui.setModeWithoutClear(Mode.OPTION_SELECT, { + options: options, + yOffset: 47 + }); + success = true; } break; case Button.CYCLE_ABILITY: @@ -1877,9 +1886,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { if (this.isCaught()) { if (isFormCaught) { - if (!pokemonPrevolutions.hasOwnProperty(this.species.speciesId)) { - this.updateButtonIcon(SettingKeyboard.Button_Stats, gamepadType, this.candyUpgradeIconElement, this.candyUpgradeLabel); - } + this.updateButtonIcon(SettingKeyboard.Button_Stats, gamepadType, this.candyUpgradeIconElement, this.candyUpgradeLabel); if (this.canCycleShiny) { this.updateButtonIcon(SettingKeyboard.Button_Cycle_Shiny, gamepadType, this.shinyIconElement, this.shinyLabel); } @@ -1936,16 +1943,51 @@ export default class PokedexPageUiHandler extends MessageUiHandler { } getFriendship(speciesId: number) { - let currentFriendship = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship; + let currentFriendship = globalScene.gameData.starterData[this.starterId].friendship; if (!currentFriendship || currentFriendship === undefined) { currentFriendship = 0; } - const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[this.getStarterSpeciesId(speciesId)]); + const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[this.starterId]); return { currentFriendship, friendshipCap }; } + /** + * Determines if a passive upgrade is available for the current species + * @returns true if the user has enough candies and a passive has not been unlocked already + */ + isPassiveAvailable(): boolean { + // Get this species ID's starter data + const starterData = globalScene.gameData.starterData[this.starterId]; + + return starterData.candyCount >= getPassiveCandyCount(speciesStarterCosts[this.starterId]) + && !(starterData.passiveAttr & PassiveAttr.UNLOCKED); + } + + /** + * Determines if a value reduction upgrade is available for the current species + * @returns true if the user has enough candies and all value reductions have not been unlocked already + */ + isValueReductionAvailable(): boolean { + // Get this species ID's starter data + const starterData = globalScene.gameData.starterData[this.starterId]; + + return starterData.candyCount >= getValueReductionCandyCounts(speciesStarterCosts[this.starterId])[starterData.valueReduction] + && starterData.valueReduction < valueReductionMax; + } + + /** + * Determines if an same species egg can be bought for the current species + * @returns true if the user has enough candies + */ + isSameSpeciesEggAvailable(): boolean { + // Get this species ID's starter data + const starterData = globalScene.gameData.starterData[this.starterId]; + + return starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[this.starterId]); + } + setSpecies() { const species = this.species; const starterAttributes : StarterAttributes | null = species ? { ...this.starterAttributes } : null; @@ -1967,88 +2009,10 @@ export default class PokedexPageUiHandler extends MessageUiHandler { if (species && (this.speciesStarterDexEntry?.seenAttr || this.isCaught())) { this.pokemonNumberText.setText(padInt(species.speciesId, 4)); - if (starterAttributes?.nickname) { - const name = decodeURIComponent(escape(atob(starterAttributes.nickname))); - this.pokemonNameText.setText(name); - } else { - this.pokemonNameText.setText(species.name); - } if (this.isCaught()) { - const colorScheme = starterColors[species.speciesId]; - - const luck = globalScene.gameData.getDexAttrLuck(this.isCaught()); - this.pokemonLuckText.setVisible(!!luck); - this.pokemonLuckText.setText(luck.toString()); - this.pokemonLuckText.setTint(getVariantTint(Math.min(luck - 1, 2) as Variant)); - this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible); - - //Growth translate - let growthReadable = toReadableString(GrowthRate[species.growthRate]); - const growthAux = growthReadable.replace(" ", "_"); - if (i18next.exists("growth:" + growthAux)) { - growthReadable = i18next.t("growth:" + growthAux as any); - } - this.pokemonGrowthRateText.setText(growthReadable); - - this.pokemonGrowthRateText.setColor(getGrowthRateColor(species.growthRate)); - this.pokemonGrowthRateText.setShadowColor(getGrowthRateColor(species.growthRate, true)); - this.pokemonGrowthRateLabelText.setVisible(true); - this.pokemonUncaughtText.setVisible(false); - this.pokemonCaughtCountText.setText(`${this.speciesStarterDexEntry?.caughtCount}`); - if (species.speciesId === Species.MANAPHY || species.speciesId === Species.PHIONE) { - this.pokemonHatchedIcon.setFrame("manaphy"); - } else { - this.pokemonHatchedIcon.setFrame(getEggTierForSpecies(species)); - } - this.pokemonHatchedCountText.setText(`${this.speciesStarterDexEntry?.hatchedCount}`); const defaultDexAttr = this.getCurrentDexProps(species.speciesId); - const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); - const variant = defaultProps.variant; - const tint = getVariantTint(variant); - this.pokemonShinyIcon.setFrame(getVariantIcon(variant)); - this.pokemonShinyIcon.setTint(tint); - this.pokemonShinyIcon.setVisible(defaultProps.shiny); - this.pokemonCaughtHatchedContainer.setVisible(true); - this.pokemonFormText.setVisible(true); - - if (pokemonPrevolutions.hasOwnProperty(species.speciesId)) { - this.pokemonCaughtHatchedContainer.setY(16); - this.pokemonShinyIcon.setY(135); - this.pokemonShinyIcon.setFrame(getVariantIcon(variant)); - [ - this.pokemonCandyContainer, - this.pokemonHatchedIcon, - this.pokemonHatchedCountText - ].map(c => c.setVisible(false)); - this.pokemonFormText.setY(25); - } else { - this.pokemonCaughtHatchedContainer.setY(25); - this.pokemonShinyIcon.setY(117); - this.pokemonCandyIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[0]))); - this.pokemonCandyOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[1]))); - this.pokemonCandyCountText.setText(`x${globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)].candyCount}`); - this.pokemonCandyContainer.setVisible(true); - this.pokemonFormText.setY(42); - this.pokemonHatchedIcon.setVisible(true); - this.pokemonHatchedCountText.setVisible(true); - - const { currentFriendship, friendshipCap } = this.getFriendship(this.species.speciesId); - const candyCropY = 16 - (16 * (currentFriendship / friendshipCap)); - this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY); - - this.pokemonCandyContainer.on("pointerover", () => { - globalScene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true); - this.activeTooltip = "CANDY"; - }); - this.pokemonCandyContainer.on("pointerout", () => { - globalScene.ui.hideTooltip(); - this.activeTooltip = undefined; - }); - - } - // Set default attributes if for some reason starterAttributes does not exist or attributes missing const props: StarterAttributes = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); if (starterAttributes?.variant && !isNaN(starterAttributes.variant)) { @@ -2065,12 +2029,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { female: props.female, variant: props.variant ?? 0, }); - - if (this.isFormCaught(this.species, props.form)) { - const speciesForm = getPokemonSpeciesForm(species.speciesId, props.form ?? 0); - this.setTypeIcons(speciesForm.type1, speciesForm.type2); - this.pokemonSprite.clearTint(); - } } else { this.pokemonGrowthRateText.setText(""); this.pokemonGrowthRateLabelText.setVisible(false); @@ -2092,7 +2050,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { formIndex: props.formIndex, female: props.female, variant: props.variant, - forSeen: true }); this.pokemonSprite.setTint(0x808080); } @@ -2123,7 +2080,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, forceUpdate?: boolean): void { let { shiny, formIndex, female, variant } = options; - const forSeen: boolean = options.forSeen ?? false; const oldProps = species ? this.starterAttributes : null; // We will only update the sprite if there is a change to form, shiny/variant @@ -2194,12 +2150,12 @@ export default class PokedexPageUiHandler extends MessageUiHandler { } const isFormCaught = this.isFormCaught(); + const isFormSeen = dexEntry ? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false; this.shinyOverlay.setVisible(shiny ?? false); // TODO: is false the correct default? this.pokemonNumberText.setColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, false)); this.pokemonNumberText.setShadowColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, true)); - const assetLoadCancelled = new BooleanHolder(false); this.assetLoadCancelled = assetLoadCancelled; @@ -2221,13 +2177,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.pokemonSprite.setVisible(!this.statsMode); } - const currentFilteredContainer = this.filteredStarterContainers.find(p => p.species.speciesId === species.speciesId); - if (currentFilteredContainer) { - const starterSprite = currentFilteredContainer.icon as Phaser.GameObjects.Sprite; - starterSprite.setTexture(species.getIconAtlasKey(formIndex, shiny, variant), species.getIconId(female!, formIndex, shiny, variant)); - currentFilteredContainer.checkIconId(female, formIndex, shiny, variant); - } - const isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY); const isShinyCaught = !!(caughtAttr & DexAttr.SHINY); @@ -2250,27 +2199,129 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.pokemonGenderText.setText(""); } - if (caughtAttr) { - if (isFormCaught) { - this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => { - const crier = (this.species.forms && this.species.forms.length > 0) ? this.species.forms[formIndex ?? this.formIndex] : this.species; - crier.cry(); - }); - - this.pokemonSprite.clearTint(); - } else { - this.pokemonSprite.setTint(0x000000); - } + // Setting the name + if (isFormCaught || isFormSeen) { + this.pokemonNameText.setText(species.name); + } else { + this.pokemonNameText.setText(species ? "???" : ""); } - if (caughtAttr || forSeen) { + // Setting tint of the sprite + if (isFormCaught) { + this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => { + const crier = (this.species.forms && this.species.forms.length > 0) ? this.species.forms[formIndex ?? this.formIndex] : this.species; + crier.cry(); + }); + this.pokemonSprite.clearTint(); + } else if (isFormSeen) { + this.pokemonSprite.setTint(0x808080); + } else { + this.pokemonSprite.setTint(0); + } + + // Setting luck text and sparks + if (isFormCaught) { + const luck = globalScene.gameData.getDexAttrLuck(this.isCaught()); + this.pokemonLuckText.setVisible(!!luck); + this.pokemonLuckText.setText(luck.toString()); + this.pokemonLuckText.setTint(getVariantTint(Math.min(luck - 1, 2) as Variant)); + this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible); + } else { + this.pokemonLuckText.setVisible(false); + this.pokemonLuckLabelText.setVisible(false); + } + + // Setting growth rate text + if (isFormCaught) { + let growthReadable = toReadableString(GrowthRate[species.growthRate]); + const growthAux = growthReadable.replace(" ", "_"); + if (i18next.exists("growth:" + growthAux)) { + growthReadable = i18next.t("growth:" + growthAux as any); + } + this.pokemonGrowthRateText.setText(growthReadable); + + this.pokemonGrowthRateText.setColor(getGrowthRateColor(species.growthRate)); + this.pokemonGrowthRateText.setShadowColor(getGrowthRateColor(species.growthRate, true)); + this.pokemonGrowthRateLabelText.setVisible(true); + } else { + this.pokemonGrowthRateText.setText(""); + this.pokemonGrowthRateLabelText.setVisible(false); + } + + // Caught and hatched + if (isFormCaught) { + const colorScheme = starterColors[this.starterId]; + + this.pokemonUncaughtText.setVisible(false); + this.pokemonCaughtCountText.setText(`${this.speciesStarterDexEntry?.caughtCount}`); + if (species.speciesId === Species.MANAPHY || species.speciesId === Species.PHIONE) { + this.pokemonHatchedIcon.setFrame("manaphy"); + } else { + this.pokemonHatchedIcon.setFrame(getEggTierForSpecies(species)); + } + this.pokemonHatchedCountText.setText(`${this.speciesStarterDexEntry?.hatchedCount}`); + + const defaultDexAttr = this.getCurrentDexProps(species.speciesId); + const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); + const variant = defaultProps.variant; + const tint = getVariantTint(variant); + this.pokemonShinyIcon.setFrame(getVariantIcon(variant)); + this.pokemonShinyIcon.setTint(tint); + this.pokemonShinyIcon.setVisible(defaultProps.shiny); + this.pokemonCaughtHatchedContainer.setVisible(true); + + this.pokemonCaughtHatchedContainer.setY(25); + this.pokemonCandyIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[0]))); + this.pokemonCandyOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[1]))); + this.pokemonCandyCountText.setText(`x${globalScene.gameData.starterData[this.starterId].candyCount}`); + this.pokemonCandyContainer.setVisible(true); + + if (pokemonPrevolutions.hasOwnProperty(species.speciesId)) { + this.pokemonShinyIcon.setY(135); + this.pokemonShinyIcon.setFrame(getVariantIcon(variant)); + this.pokemonHatchedIcon.setVisible(false); + this.pokemonHatchedCountText.setVisible(false); + this.pokemonFormText.setY(36); + } else { + this.pokemonShinyIcon.setY(117); + this.pokemonHatchedIcon.setVisible(true); + this.pokemonHatchedCountText.setVisible(true); + this.pokemonFormText.setY(42); + + const { currentFriendship, friendshipCap } = this.getFriendship(this.species.speciesId); + const candyCropY = 16 - (16 * (currentFriendship / friendshipCap)); + this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY); + + this.pokemonCandyContainer.on("pointerover", () => { + globalScene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true); + this.activeTooltip = "CANDY"; + }); + this.pokemonCandyContainer.on("pointerout", () => { + globalScene.ui.hideTooltip(); + this.activeTooltip = undefined; + }); + + } + } else { + this.pokemonUncaughtText.setVisible(true); + this.pokemonCaughtHatchedContainer.setVisible(false); + this.pokemonCandyContainer.setVisible(false); + this.pokemonShinyIcon.setVisible(false); + } + + // Setting type icons and form text + if (isFormCaught || isFormSeen) { const speciesForm = getPokemonSpeciesForm(species.speciesId, formIndex!); // TODO: is the bang correct? this.setTypeIcons(speciesForm.type1, speciesForm.type2); this.pokemonFormText.setText(this.getFormString((speciesForm as PokemonForm).formKey, species)); - + this.pokemonFormText.setVisible(true); + if (!isFormCaught) { + this.pokemonFormText.setY(18); + } } else { this.setTypeIcons(null, null); this.pokemonFormText.setText(""); + this.pokemonFormText.setVisible(false); } } else { this.shinyOverlay.setVisible(false); diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index 410bb53906a..4c920a094c6 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -11,7 +11,7 @@ import { allSpecies, getPokemonSpeciesForm, getPokerusStarters } from "#app/data import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters"; import { catchableSpecies } from "#app/data/balance/biomes"; import { Type } from "#enums/type"; -import type { DexAttrProps, DexEntry, StarterMoveset, StarterAttributes, StarterPreferences } from "#app/system/game-data"; +import type { DexAttrProps, DexEntry, StarterAttributes, StarterPreferences } from "#app/system/game-data"; import { AbilityAttr, DexAttr, StarterPrefs } from "#app/system/game-data"; import MessageUiHandler from "#app/ui/message-ui-handler"; import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler"; @@ -19,7 +19,6 @@ import { TextStyle, addTextObject } from "#app/ui/text"; import { Mode } from "#app/ui/ui"; import { SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { Passive as PassiveAttr } from "#enums/passive"; -import type { Moves } from "#enums/moves"; import type { Species } from "#enums/species"; import { Button } from "#enums/buttons"; import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#app/ui/dropdown"; @@ -42,7 +41,6 @@ import { pokemonStarters } from "#app/data/balance/pokemon-evolutions"; import { Biome } from "#enums/biome"; import { globalScene } from "#app/global-scene"; - interface LanguageSetting { starterInfoTextSize: string, instructionTextSize: string, @@ -139,7 +137,6 @@ interface SpeciesDetails { variant?: Variant, abilityIndex?: number, natureIndex?: number, - forSeen?: boolean, // default = false } export default class PokedexUiHandler extends MessageUiHandler { @@ -161,7 +158,6 @@ export default class PokedexUiHandler extends MessageUiHandler { private filterMode: boolean; private filterBarCursor: number = 0; - private starterMoveset: StarterMoveset | null; private scrollCursor: number; private allSpecies: PokemonSpecies[] = []; @@ -169,7 +165,6 @@ export default class PokedexUiHandler extends MessageUiHandler { private speciesLoaded: Map = new Map(); private pokerusSpecies: PokemonSpecies[] = []; private speciesStarterDexEntry: DexEntry | null; - private speciesStarterMoves: Moves[]; private assetLoadCancelled: BooleanHolder | null; public cursorObj: Phaser.GameObjects.Image; @@ -206,6 +201,20 @@ export default class PokedexUiHandler extends MessageUiHandler { private toggleDecorationsIconElement: Phaser.GameObjects.Sprite; private toggleDecorationsLabel: Phaser.GameObjects.Text; + private formTrayContainer: Phaser.GameObjects.Container; + private trayBg: Phaser.GameObjects.NineSlice; + private trayForms: PokemonForm[]; + private trayContainers: PokedexMonContainer[] = []; + private trayNumIcons: number; + private trayRows: number; + private trayColumns: number; + private trayCursorObj: Phaser.GameObjects.Image; + private trayCursor: number = 0; + private showingTray: boolean = false; + private showFormTrayIconElement: Phaser.GameObjects.Sprite; + private showFormTrayLabel: Phaser.GameObjects.Text; + private canShowFormTray: boolean; + constructor() { super(Mode.POKEDEX); } @@ -425,7 +434,6 @@ export default class PokedexUiHandler extends MessageUiHandler { this.cursorObj = globalScene.add.image(0, 0, "select_cursor"); this.cursorObj.setOrigin(0, 0); - starterBoxContainer.add(this.cursorObj); for (const species of allSpecies) { @@ -438,6 +446,20 @@ export default class PokedexUiHandler extends MessageUiHandler { starterBoxContainer.add(pokemonContainer); } + // Tray to display forms + this.formTrayContainer = globalScene.add.container(0, 0); + + this.trayBg = addWindow(0, 0, 0, 0); + this.trayBg.setOrigin(0, 0); + this.formTrayContainer.add(this.trayBg); + + this.trayCursorObj = globalScene.add.image(0, 0, "select_cursor"); + this.trayCursorObj.setOrigin(0, 0); + this.formTrayContainer.add(this.trayCursorObj); + starterBoxContainer.add(this.formTrayContainer); + starterBoxContainer.bringToTop(this.formTrayContainer); + this.formTrayContainer.setVisible(false); + this.starterSelectContainer.add(starterBoxContainer); this.pokemonSprite = globalScene.add.sprite(96, 143, "pkmn__sub"); @@ -449,7 +471,7 @@ export default class PokedexUiHandler extends MessageUiHandler { this.type1Icon.setOrigin(0, 0); this.starterSelectContainer.add(this.type1Icon); - this.type2Icon = globalScene.add.sprite(10, 166, getLocalizedSpriteKey("types")); + this.type2Icon = globalScene.add.sprite(28, 158, getLocalizedSpriteKey("types")); this.type2Icon.setScale(0.5); this.type2Icon.setOrigin(0, 0); this.starterSelectContainer.add(this.type2Icon); @@ -488,6 +510,17 @@ export default class PokedexUiHandler extends MessageUiHandler { this.starterSelectContainer.add(this.toggleDecorationsIconElement); this.starterSelectContainer.add(this.toggleDecorationsLabel); + this.showFormTrayIconElement = new Phaser.GameObjects.Sprite(globalScene, 6, 168, "keyboard", "F.png"); + this.showFormTrayIconElement.setName("sprite-showFormTray-icon-element"); + this.showFormTrayIconElement.setScale(0.675); + this.showFormTrayIconElement.setOrigin(0.0, 0.0); + this.showFormTrayLabel = addTextObject(16, 168, i18next.t("pokedexUiHandler:showForms"), TextStyle.PARTY, { fontSize: instructionTextSize }); + this.showFormTrayLabel.setName("text-showFormTray-label"); + this.showFormTrayIconElement.setVisible(false); + this.showFormTrayLabel.setVisible(false); + this.starterSelectContainer.add(this.showFormTrayIconElement); + this.starterSelectContainer.add(this.showFormTrayLabel); + this.message = addTextObject(8, 8, "", TextStyle.WINDOW, { maxLines: 2 }); this.message.setOrigin(0, 0); this.starterSelectMessageBoxContainer.add(this.message); @@ -527,7 +560,7 @@ export default class PokedexUiHandler extends MessageUiHandler { this.starterPreferences[species.speciesId] = this.initStarterPrefs(species); - if (dexEntry.caughtAttr) { + if (dexEntry.caughtAttr || globalScene.dexForDevs) { icon.clearTint(); } else if (dexEntry.seenAttr) { icon.setTint(0x808080); @@ -860,32 +893,42 @@ export default class PokedexUiHandler extends MessageUiHandler { } else if (this.filterTextMode && !(this.filterText.getValue(this.filterTextCursor) === this.filterText.defaultText)) { this.filterText.resetSelection(this.filterTextCursor); success = true; + } else if (this.showingTray) { + success = this.closeFormTray(); } else { this.tryExit(); success = true; } } else if (button === Button.STATS) { - if (!this.filterMode) { + if (!this.filterMode && !this.showingTray) { this.cursorObj.setVisible(false); this.setSpecies(null); this.filterText.cursorObj.setVisible(false); this.filterTextMode = false; this.filterBarCursor = 0; this.setFilterMode(true); + } else { + error = true; } } else if (button === Button.V) { - if (!this.filterTextMode) { + if (!this.filterTextMode && !this.showingTray) { this.cursorObj.setVisible(false); this.setSpecies(null); this.filterBar.cursorObj.setVisible(false); this.filterMode = false; this.filterTextCursor = 0; this.setFilterTextMode(true); + } else { + error = true; } } else if (button === Button.CYCLE_SHINY) { - this.showDecorations = !this.showDecorations; - this.updateScroll(); - success = true; + if (!this.showingTray) { + this.showDecorations = !this.showDecorations; + this.updateScroll(); + success = true; + } else { + error = true; + } } else if (this.filterMode) { switch (button) { case Button.LEFT: @@ -982,8 +1025,55 @@ export default class PokedexUiHandler extends MessageUiHandler { success = true; break; } + } else if (this.showingTray) { + if (button === Button.ACTION) { + const formIndex = this.trayForms[this.trayCursor].formIndex; + ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, formIndex, { form: formIndex }); + success = true; + } else { + const numberOfForms = this.trayContainers.length; + const numOfRows = Math.ceil(numberOfForms / maxColumns); + const currentRow = Math.floor(this.trayCursor / maxColumns); + switch (button) { + case Button.UP: + if (currentRow > 0) { + success = this.setTrayCursor(this.trayCursor - 9); + } else { + const targetCol = this.trayCursor; + if (numberOfForms % 9 > targetCol) { + success = this.setTrayCursor(numberOfForms - (numberOfForms) % 9 + targetCol); + } else { + success = this.setTrayCursor(Math.max(numberOfForms - (numberOfForms) % 9 + targetCol - 9, 0)); + } + } + break; + case Button.DOWN: + if (currentRow < numOfRows - 1) { + success = this.setTrayCursor(this.trayCursor + 9); + } else { + success = this.setTrayCursor(this.trayCursor % 9); + } + break; + case Button.LEFT: + if (this.trayCursor % 9 !== 0) { + success = this.setTrayCursor(this.trayCursor - 1); + } else { + success = this.setTrayCursor(currentRow < numOfRows - 1 ? (currentRow + 1) * maxColumns - 1 : numberOfForms - 1); + } + break; + case Button.RIGHT: + if (this.trayCursor % 9 < (currentRow < numOfRows - 1 ? 8 : (numberOfForms - 1) % 9)) { + success = this.setTrayCursor(this.trayCursor + 1); + } else { + success = this.setTrayCursor(currentRow * 9); + } + break; + case Button.CYCLE_FORM: + success = this.closeFormTray(); + break; + } + } } else { - if (button === Button.ACTION) { ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, 0); success = true; @@ -1042,6 +1132,12 @@ export default class PokedexUiHandler extends MessageUiHandler { success = true; } break; + case Button.CYCLE_FORM: + const species = this.filteredPokemonContainers[this.cursor].species; + if (this.canShowFormTray) { + success = this.openFormTray(species); + } + break; } } } @@ -1068,6 +1164,9 @@ export default class PokedexUiHandler extends MessageUiHandler { case SettingKeyboard.Button_Cycle_Variant: iconPath = "V.png"; break; + case SettingKeyboard.Button_Cycle_Form: + iconPath = "F.png"; + break; case SettingKeyboard.Button_Stats: iconPath = "C.png"; break; @@ -1145,13 +1244,15 @@ export default class PokedexUiHandler extends MessageUiHandler { this.validPokemonContainers.forEach(container => { container.setVisible(false); - container.cost = globalScene.gameData.getSpeciesStarterValue(this.getStarterSpeciesId(container.species.speciesId)); + const starterId = this.getStarterSpeciesId(container.species.speciesId); + + container.cost = globalScene.gameData.getSpeciesStarterValue(starterId); // First, ensure you have the caught attributes for the species else default to bigint 0 // TODO: This might be removed depending on how accessible we want the pokedex function to be const caughtAttr = globalScene.gameData.dexData[container.species.speciesId]?.caughtAttr || BigInt(0); - const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(container.species.speciesId)]; - const isStarterProgressable = speciesEggMoves.hasOwnProperty(this.getStarterSpeciesId(container.species.speciesId)); + const starterData = globalScene.gameData.starterData[starterId]; + const isStarterProgressable = speciesEggMoves.hasOwnProperty(starterId); // Name filter const selectedName = this.filterText.getValue(FilterTextRow.NAME); @@ -1162,8 +1263,8 @@ export default class PokedexUiHandler extends MessageUiHandler { // On the other hand, in some cases it is possible to switch between different forms and combine (Deoxys) const levelMoves = pokemonSpeciesLevelMoves[container.species.speciesId].map(m => allMoves[m[1]].name); // This always gets egg moves from the starter - const eggMoves = speciesEggMoves[this.getStarterSpeciesId(container.species.speciesId)]?.map(m => allMoves[m].name) ?? []; - const tmMoves = speciesTmMoves[this.getStarterSpeciesId(container.species.speciesId)]?.map(m => allMoves[Array.isArray(m) ? m[1] : m].name) ?? []; + const eggMoves = speciesEggMoves[starterId]?.map(m => allMoves[m].name) ?? []; + const tmMoves = speciesTmMoves[starterId]?.map(m => allMoves[Array.isArray(m) ? m[1] : m].name) ?? []; const selectedMove1 = this.filterText.getValue(FilterTextRow.MOVE_1); const selectedMove2 = this.filterText.getValue(FilterTextRow.MOVE_2); @@ -1185,27 +1286,40 @@ export default class PokedexUiHandler extends MessageUiHandler { container.tmMove2Icon.setVisible(false); if (fitsEggMove1 && !fitsLevelMove1) { container.eggMove1Icon.setVisible(true); + const em1 = eggMoves.findIndex(name => name === selectedMove1); + if ((starterData[starterId].eggMoves & (1 << em1)) === 0) { + container.eggMove1Icon.setTint(0x808080); + } else { + container.eggMove1Icon.clearTint(); + } } else if (fitsTmMove1 && !fitsLevelMove1) { container.tmMove1Icon.setVisible(true); } if (fitsEggMove2 && !fitsLevelMove2) { container.eggMove2Icon.setVisible(true); + const em2 = eggMoves.findIndex(name => name === selectedMove2); + if ((starterData[starterId].eggMoves & (1 << em2)) === 0) { + container.eggMove2Icon.setTint(0x808080); + } else { + container.eggMove2Icon.clearTint(); + } } else if (fitsTmMove2 && !fitsLevelMove2) { container.tmMove2Icon.setVisible(true); } // Ability filter const abilities = [ container.species.ability1, container.species.ability2, container.species.abilityHidden ].map(a => allAbilities[a].name); - const passives = starterPassiveAbilities[this.getStarterSpeciesId(container.species.speciesId)] ?? {} as PassiveAbilities; + const passives = starterPassiveAbilities[starterId] ?? {} as PassiveAbilities; const selectedAbility1 = this.filterText.getValue(FilterTextRow.ABILITY_1); - const fitsFormAbility = container.species.forms.some(form => allAbilities[form.ability1].name === selectedAbility1); - const fitsAbility1 = abilities.includes(selectedAbility1) || fitsFormAbility || selectedAbility1 === this.filterText.defaultText; - const fitsPassive1 = Object.values(passives).some(p => p.name === selectedAbility1); + const fitsFormAbility1 = container.species.forms.some(form => [ form.ability1, form.ability2, form.abilityHidden ].map(a => allAbilities[a].name).includes(selectedAbility1)); + const fitsAbility1 = abilities.includes(selectedAbility1) || fitsFormAbility1 || selectedAbility1 === this.filterText.defaultText; + const fitsPassive1 = Object.values(passives).some(p => allAbilities[p].name === selectedAbility1); const selectedAbility2 = this.filterText.getValue(FilterTextRow.ABILITY_2); - const fitsAbility2 = abilities.includes(selectedAbility2) || fitsFormAbility || selectedAbility2 === this.filterText.defaultText; - const fitsPassive2 = Object.values(passives).some(p => p.name === selectedAbility2); + const fitsFormAbility2 = container.species.forms.some(form => [ form.ability1, form.ability2, form.abilityHidden ].map(a => allAbilities[a].name).includes(selectedAbility2)); + const fitsAbility2 = abilities.includes(selectedAbility2) || fitsFormAbility2 || selectedAbility2 === this.filterText.defaultText; + const fitsPassive2 = Object.values(passives).some(p => allAbilities[p].name === selectedAbility2); // If both fields have been set to the same ability, show both ability and passive const fitsAbilities = (fitsAbility1 && (fitsPassive2 || selectedAbility2 === this.filterText.defaultText)) || @@ -1213,11 +1327,26 @@ export default class PokedexUiHandler extends MessageUiHandler { container.passive1Icon.setVisible(false); container.passive2Icon.setVisible(false); - if (fitsPassive1) { - container.passive1Icon.setVisible(true); - } - if (fitsPassive2) { - container.passive2Icon.setVisible(true); + if (fitsPassive1 || fitsPassive2) { + if (fitsPassive1) { + if (starterData.passiveAttr > 0) { + container.passive1Icon.clearTint(); + container.passive1OverlayIcon.clearTint(); + } else { + container.passive1Icon.setTint(0x808080); + container.passive1OverlayIcon.setTint(0x808080); + } + container.passive1Icon.setVisible(true); + } else { + if (starterData.passiveAttr > 0) { + container.passive2Icon.clearTint(); + container.passive2OverlayIcon.clearTint(); + } else { + container.passive2Icon.setTint(0x808080); + container.passive2OverlayIcon.setTint(0x808080); + } + container.passive2Icon.setVisible(true); + } } // Gen filter @@ -1236,7 +1365,7 @@ export default class PokedexUiHandler extends MessageUiHandler { // We get biomes for both the mon and its starters to ensure that evolutions get the correct filters. // TODO: We might also need to do it the other way around. - const biomes = catchableSpecies[container.species.speciesId].concat(catchableSpecies[this.getStarterSpeciesId(container.species.speciesId)]).map(b => Biome[b.biome]); + const biomes = catchableSpecies[container.species.speciesId].concat(catchableSpecies[starterId]).map(b => Biome[b.biome]); if (biomes.length === 0) { biomes.push("Uncatchable"); } @@ -1530,6 +1659,8 @@ export default class PokedexUiHandler extends MessageUiHandler { this.cursorObj.setVisible(!filterMode); this.filterBar.cursorObj.setVisible(filterMode); this.pokemonSprite.setVisible(false); + this.showFormTrayIconElement.setVisible(false); + this.showFormTrayLabel.setVisible(false); if (filterMode !== this.filterMode) { this.filterMode = filterMode; @@ -1546,6 +1677,8 @@ export default class PokedexUiHandler extends MessageUiHandler { this.cursorObj.setVisible(!filterTextMode); this.filterText.cursorObj.setVisible(filterTextMode); this.pokemonSprite.setVisible(false); + this.showFormTrayIconElement.setVisible(false); + this.showFormTrayLabel.setVisible(false); if (filterTextMode !== this.filterTextMode) { this.filterTextMode = filterTextMode; @@ -1558,6 +1691,101 @@ export default class PokedexUiHandler extends MessageUiHandler { return false; } + openFormTray(species: PokemonSpecies): boolean { + + this.trayForms = species.forms; + + this.trayNumIcons = this.trayForms.length; + this.trayRows = Math.floor(this.trayNumIcons / 9) + (this.trayNumIcons % 9 === 0 ? 0 : 1); + this.trayColumns = Math.min(this.trayNumIcons, 9); + + const maxColumns = 9; + const onScreenFirstIndex = this.scrollCursor * maxColumns; + const boxCursor = this.cursor - onScreenFirstIndex; + const boxCursorY = Math.floor(boxCursor / maxColumns); + const boxCursorX = boxCursor - boxCursorY * 9; + const spaceBelow = 9 - 1 - boxCursorY; + const spaceRight = 9 - boxCursorX; + const boxPos = calcStarterPosition(this.cursor, this.scrollCursor); + const goUp = this.trayRows <= spaceBelow - 1 ? 0 : 1; + const goLeft = this.trayColumns <= spaceRight ? 0 : 1; + + this.trayBg.setSize(13 + this.trayColumns * 17, 8 + this.trayRows * 18); + this.formTrayContainer.setX( + (goLeft ? boxPos.x - 18 * (this.trayColumns - spaceRight) : boxPos.x) - 3 + ); + this.formTrayContainer.setY( + goUp ? boxPos.y - this.trayBg.height : boxPos.y + 17 + ); + + const dexEntry = globalScene.gameData.dexData[species.speciesId]; + const dexAttr = this.getCurrentDexProps(species.speciesId); + const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(this.lastSpecies, dexAttr)); + + this.trayContainers = []; + this.trayForms.map((f, index) => { + const isFormCaught = dexEntry ? (dexEntry.caughtAttr & globalScene.gameData.getFormAttr(f.formIndex ?? 0)) > 0n : false; + const isFormSeen = dexEntry ? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(f.formIndex ?? 0)) > 0n : false; + const formContainer = new PokedexMonContainer(species, { formIndex: f.formIndex, female: props.female, shiny: props.shiny, variant: props.variant }); + this.iconAnimHandler.addOrUpdate(formContainer.icon, PokemonIconAnimMode.NONE); + // Setting tint, for all saves some caught forms may only show up as seen + if (isFormCaught || globalScene.dexForDevs) { + formContainer.icon.clearTint(); + } else if (isFormSeen) { + formContainer.icon.setTint(0x808080); + } + formContainer.setPosition(5 + (index % 9) * 18, 4 + Math.floor(index / 9) * 17); + this.formTrayContainer.add(formContainer); + this.trayContainers.push(formContainer); + }); + + this.showingTray = true; + + this.setTrayCursor(0); + + this.formTrayContainer.setVisible(true); + + this.showFormTrayIconElement.setVisible(false); + this.showFormTrayLabel.setVisible(false); + + return true; + } + + closeFormTray(): boolean { + + this.trayContainers.forEach(obj => { + this.formTrayContainer.remove(obj, true); // Removes from container and destroys it + }); + + this.trayContainers = []; + this.formTrayContainer.setVisible(false); + this.showingTray = false; + + this.setSpeciesDetails(this.lastSpecies); + return true; + } + + setTrayCursor(cursor: number): boolean { + if (!this.showingTray) { + return false; + } + + cursor = Phaser.Math.Clamp(this.trayContainers.length - 1, cursor, 0); + const changed = this.trayCursor !== cursor; + if (changed) { + this.trayCursor = cursor; + } + + this.trayCursorObj.setPosition(5 + (cursor % 9) * 18, 4 + Math.floor(cursor / 9) * 17); + + const species = this.lastSpecies; + const formIndex = this.trayForms[cursor].formIndex; + + this.setSpeciesDetails(species, { formIndex: formIndex }); + + return changed; + } + getFriendship(speciesId: number) { let currentFriendship = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship; if (!currentFriendship || currentFriendship === undefined) { @@ -1592,13 +1820,13 @@ export default class PokedexUiHandler extends MessageUiHandler { this.lastSpecies = species!; // TODO: is this bang correct? - if (species && (this.speciesStarterDexEntry?.seenAttr || this.speciesStarterDexEntry?.caughtAttr)) { + if (species && (this.speciesStarterDexEntry?.seenAttr || this.speciesStarterDexEntry?.caughtAttr || globalScene.dexForDevs)) { this.pokemonNumberText.setText(i18next.t("pokedexUiHandler:pokemonNumber") + padInt(species.speciesId, 4)); this.pokemonNameText.setText(species.name); - if (this.speciesStarterDexEntry?.caughtAttr) { + if (this.speciesStarterDexEntry?.caughtAttr || globalScene.dexForDevs) { // Pause the animation when the species is selected const speciesIndex = this.allSpecies.indexOf(species); @@ -1627,9 +1855,7 @@ export default class PokedexUiHandler extends MessageUiHandler { this.type1Icon.setVisible(true); this.type2Icon.setVisible(true); - this.setSpeciesDetails(species, { - forSeen: true - }); + this.setSpeciesDetails(species); this.pokemonSprite.setTint(0x808080); } } else { @@ -1646,7 +1872,6 @@ export default class PokedexUiHandler extends MessageUiHandler { setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void { let { shiny, formIndex, female, variant } = options; - const forSeen: boolean = options.forSeen ?? false; // We will only update the sprite if there is a change to form, shiny/variant // or gender for species with gender sprite differences @@ -1667,34 +1892,33 @@ export default class PokedexUiHandler extends MessageUiHandler { this.assetLoadCancelled = null; } - this.starterMoveset = null; - this.speciesStarterMoves = []; - if (species) { const dexEntry = globalScene.gameData.dexData[species.speciesId]; if (!dexEntry.caughtAttr) { const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId))); - if (shiny === undefined || shiny !== props.shiny) { + if (shiny === undefined) { shiny = props.shiny; } - if (formIndex === undefined || formIndex !== props.formIndex) { + if (formIndex === undefined) { formIndex = props.formIndex; } - if (female === undefined || female !== props.female) { + if (female === undefined) { female = props.female; } - if (variant === undefined || variant !== props.variant) { + if (variant === undefined) { variant = props.variant; } } + const isFormCaught = dexEntry ? (dexEntry.caughtAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false; + const isFormSeen = dexEntry ? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false; + const assetLoadCancelled = new BooleanHolder(false); this.assetLoadCancelled = assetLoadCancelled; if (shouldUpdateSprite) { - species.loadAssets(female!, formIndex, shiny, variant, true).then(() => { // TODO: is this bang correct? if (assetLoadCancelled.value) { return; @@ -1711,21 +1935,37 @@ export default class PokedexUiHandler extends MessageUiHandler { this.pokemonSprite.setVisible(!(this.filterMode || this.filterTextMode)); } - if (dexEntry.caughtAttr || forSeen) { + if (isFormCaught || globalScene.dexForDevs) { + this.pokemonSprite.clearTint(); + } else if (isFormSeen) { + this.pokemonSprite.setTint(0x808080); + } else { + this.pokemonSprite.setTint(0); + } + if (isFormCaught || isFormSeen || globalScene.dexForDevs) { const speciesForm = getPokemonSpeciesForm(species.speciesId, 0); // TODO: always selecting the first form - this.setTypeIcons(speciesForm.type1, speciesForm.type2); } else { this.setTypeIcons(null, null); } + + if (species?.forms?.length > 1) { + if (!this.showingTray) { + this.showFormTrayIconElement.setVisible(true); + this.showFormTrayLabel.setVisible(true); + } + this.canShowFormTray = true; + } else { + this.showFormTrayIconElement.setVisible(false); + this.showFormTrayLabel.setVisible(false); + this.canShowFormTray = false; + } + } else { this.setTypeIcons(null, null); } - if (!this.starterMoveset) { - this.starterMoveset = this.speciesStarterMoves.slice(0, 4) as StarterMoveset; - } } setTypeIcons(type1: Type | null, type2: Type | null): void { @@ -1784,7 +2024,6 @@ export default class PokedexUiHandler extends MessageUiHandler { ui.showText(i18next.t("pokedexUiHandler:confirmExit"), null, () => { ui.setModeWithoutClear(Mode.CONFIRM, () => { ui.setMode(Mode.POKEDEX, "refresh"); - globalScene.clearPhaseQueue(); this.clearText(); this.clear(); ui.revertMode(); diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 20ca2cc88da..5476f38cc6a 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -1981,8 +1981,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { female: starterAttributes.female }; ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, starterAttributes.form, attributes); - return true; }); + return true; } }); options.push({ From 702a6ba482b5b996c601afb2829d86823f79a986 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 11 Feb 2025 03:24:48 -0800 Subject: [PATCH 16/18] [i18n] Update locales submodule (#5298) --- public/locales | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales b/public/locales index 5f6fa82c17d..bfcd7f91c39 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 5f6fa82c17d5981eaec15f105880ac2b4c99cc8d +Subproject commit bfcd7f91c39630f155839872c8f66fd0a89e12ac From 5296966f70002f65b550fbe355a456fadd04006b Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Tue, 11 Feb 2025 05:25:36 -0600 Subject: [PATCH 17/18] [Ability] [Move] Implement Magic Bounce and Magic Coat (#5225) * Add unit tests for magic bounce * Add reflectable tag and apply to moves * Add BattlerTagType for Magic Coat * Add more magic bounce tests * Add magic bounce test for sticky web source * Mostly working magic bounce and magic coat * Fix missing negation on mayBounce check * Move onto the next target after bouncing * Fix magic bounce accuracy check test * Finish magic bounce impl * Make spikes use leftmost magic bounce target * Add magic coat tests * Add MagicCoatTag to battler-tags.ts * Add final set of tests for Magic Coat / Bounce * Fix semi invulnerbale check in hitCheck * Fix magic bounce semi-invulnerable interaction This was based on smogon's incorrect handling of this situation * Magic bounce should not bounce anything during semi-invulnerable state * Activate mirror armor interaction test Also update i18 locales key to `magicCoatActivated` --- src/battle-scene.ts | 32 ++- src/data/ability.ts | 12 +- src/data/battler-tags.ts | 20 ++ src/data/move.ts | 279 +++++++++++++------ src/enums/battler-tag-type.ts | 1 + src/phases/move-effect-phase.ts | 141 ++++++++-- src/phases/move-phase.ts | 14 +- src/test/abilities/magic_bounce.test.ts | 351 ++++++++++++++++++++++++ src/test/moves/magic_coat.test.ts | 286 +++++++++++++++++++ 9 files changed, 1005 insertions(+), 131 deletions(-) create mode 100644 src/test/abilities/magic_bounce.test.ts create mode 100644 src/test/moves/magic_coat.test.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 7aa0369877b..3f285c274af 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2353,14 +2353,14 @@ export default class BattleScene extends SceneBase { } /** - * Adds Phase to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex - * @param phase {@linkcode Phase} the phase to add + * Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex + * @param phases {@linkcode Phase} the phase(s) to add */ - unshiftPhase(phase: Phase): void { + unshiftPhase(...phases: Phase[]): void { if (this.phaseQueuePrependSpliceIndex === -1) { - this.phaseQueuePrepend.push(phase); + this.phaseQueuePrepend.push(...phases); } else { - this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, phase); + this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, ...phases); } } @@ -2498,32 +2498,38 @@ export default class BattleScene extends SceneBase { * @param targetPhase {@linkcode Phase} the type of phase to search for in phaseQueue * @returns boolean if a targetPhase was found and added */ - prependToPhase(phase: Phase, targetPhase: Constructor): boolean { + prependToPhase(phase: Phase | Phase [], targetPhase: Constructor): boolean { + if (!Array.isArray(phase)) { + phase = [ phase ]; + } const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase); if (targetIndex !== -1) { - this.phaseQueue.splice(targetIndex, 0, phase); + this.phaseQueue.splice(targetIndex, 0, ...phase); return true; } else { - this.unshiftPhase(phase); + this.unshiftPhase(...phase); return false; } } /** - * Tries to add the input phase to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} - * @param phase {@linkcode Phase} the phase to be added + * Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} + * @param phase {@linkcode Phase} the phase(s) to be added * @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue} * @returns `true` if a `targetPhase` was found to append to */ - appendToPhase(phase: Phase, targetPhase: Constructor): boolean { + appendToPhase(phase: Phase | Phase[], targetPhase: Constructor): boolean { + if (!Array.isArray(phase)) { + phase = [ phase ]; + } const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase); if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { - this.phaseQueue.splice(targetIndex + 1, 0, phase); + this.phaseQueue.splice(targetIndex + 1, 0, ...phase); return true; } else { - this.unshiftPhase(phase); + this.unshiftPhase(...phase); return false; } } diff --git a/src/data/ability.ts b/src/data/ability.ts index cd31c62f7f6..a6d00b29fbc 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4484,6 +4484,13 @@ export class InfiltratorAbAttr extends AbAttr { } } +/** + * Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Magic_Bounce_(ability) | Magic Bounce}. + * Allows the source to bounce back {@linkcode MoveFlags.REFLECTABLE | Reflectable} + * moves as if the user had used {@linkcode Moves.MAGIC_COAT | Magic Coat}. + */ +export class ReflectStatusMoveAbAttr extends AbAttr { } + export class UncopiableAbilityAbAttr extends AbAttr { constructor() { super(false); @@ -5805,8 +5812,11 @@ export function initAbilities() { }, Stat.SPD, 1) .attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1), new Ability(Abilities.MAGIC_BOUNCE, 5) + .attr(ReflectStatusMoveAbAttr) .ignorable() - .unimplemented(), + // Interactions with stomping tantrum, instruct, encore, and probably other moves that + // rely on move history + .edgeCase(), new Ability(Abilities.SAP_SIPPER, 5) .attr(TypeImmunityStatStageChangeAbAttr, Type.GRASS, Stat.ATK, 1) .ignorable(), diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 43168ea5c0c..91ab10aecfa 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2975,6 +2975,24 @@ export class PsychoShiftTag extends BattlerTag { } } +/** + * Tag associated with the move Magic Coat. + */ +export class MagicCoatTag extends BattlerTag { + constructor() { + super(BattlerTagType.MAGIC_COAT, BattlerTagLapseType.TURN_END, 1, Moves.MAGIC_COAT); + } + + /** + * Queues the "[PokemonName] shrouded itself with Magic Coat" message when the tag is added. + * @param pokemon - The target {@linkcode Pokemon} + */ + override onAdd(pokemon: Pokemon) { + // "{pokemonNameWithAffix} shrouded itself with Magic Coat!" + globalScene.queueMessage(i18next.t("battlerTags:magicCoatOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); + } +} + /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * @param sourceId - The ID of the pokemon adding the tag @@ -3164,6 +3182,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new GrudgeTag(); case BattlerTagType.PSYCHO_SHIFT: return new PsychoShiftTag(); + case BattlerTagType.MAGIC_COAT: + return new MagicCoatTag(); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/move.ts b/src/data/move.ts index 6c41f0b764d..75908f86a14 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -125,7 +125,9 @@ export enum MoveFlags { /** Indicates a move is able to bypass its target's Substitute (if the target has one) */ IGNORE_SUBSTITUTE = 1 << 17, /** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */ - REDIRECT_COUNTER = 1 << 18, + REDIRECT_COUNTER = 1 << 18, + /** Indicates a move is able to be reflected by {@linkcode Abilities.MAGIC_BOUNCE} and {@linkcode Moves.MAGIC_COAT} */ + REFLECTABLE = 1 << 19, } type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; @@ -610,6 +612,16 @@ export default class Move implements Localizable { return this; } + /** + * Sets the {@linkcode MoveFlags.REFLECTABLE} flag for the calling Move + * @see {@linkcode Moves.ATTRACT} + * @returns The {@linkcode Move} that called this function + */ + reflectable(): this { + this.setFlag(MoveFlags.REFLECTABLE, true); + return this; + } + /** * Checks if the move flag applies to the pokemon(s) using/receiving the move * @param flag {@linkcode MoveFlags} MoveFlag to check on user and/or target @@ -5332,6 +5344,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { case BattlerTagType.INGRAIN: case BattlerTagType.IGNORE_ACCURACY: case BattlerTagType.AQUA_RING: + case BattlerTagType.MAGIC_COAT: return 3; case BattlerTagType.PROTECTED: case BattlerTagType.FLYING: @@ -8334,7 +8347,8 @@ export function initMoves() { .attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH) .ignoresSubstitute() .hidesTarget() - .windMove(), + .windMove() + .reflectable(), new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1) .chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" })) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) @@ -8358,7 +8372,8 @@ export function initMoves() { new AttackMove(Moves.ROLLING_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, 30, 0, 1) .attr(FlinchAttr), new StatusMove(Moves.SAND_ATTACK, Type.GROUND, 100, 15, -1, 0, 1) - .attr(StatStageChangeAttr, [ Stat.ACC ], -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1) + .reflectable(), new AttackMove(Moves.HEADBUTT, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 15, 30, 0, 1) .attr(FlinchAttr), new AttackMove(Moves.HORN_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 25, -1, 0, 1), @@ -8387,7 +8402,8 @@ export function initMoves() { .recklessMove(), new StatusMove(Moves.TAIL_WHIP, Type.NORMAL, 100, 30, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.POISON_STING, Type.POISON, MoveCategory.PHYSICAL, 15, 100, 35, 30, 0, 1) .attr(StatusEffectAttr, StatusEffect.POISON) .makesContact(false), @@ -8400,30 +8416,36 @@ export function initMoves() { .makesContact(false), new StatusMove(Moves.LEER, Type.NORMAL, 100, 30, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.BITE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, 30, 0, 1) .attr(FlinchAttr) .bitingMove(), new StatusMove(Moves.GROWL, Type.NORMAL, 100, 40, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.ATK ], -1) .soundBased() - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1) .attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH) .soundBased() - .hidesTarget(), + .hidesTarget() + .reflectable(), new StatusMove(Moves.SING, Type.NORMAL, 55, 15, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.SLEEP) - .soundBased(), + .soundBased() + .reflectable(), new StatusMove(Moves.SUPERSONIC, Type.NORMAL, 55, 20, -1, 0, 1) .attr(ConfuseAttr) - .soundBased(), + .soundBased() + .reflectable(), new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1) .attr(FixedDamageAttr, 20), new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1) .attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true) .condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined) - .ignoresSubstitute(), + .ignoresSubstitute() + .reflectable(), new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -8476,7 +8498,8 @@ export function initMoves() { .triageMove(), new StatusMove(Moves.LEECH_SEED, Type.GRASS, 90, 10, -1, 0, 1) .attr(LeechSeedAttr) - .condition((user, target, move) => !target.getTag(BattlerTagType.SEEDED) && !target.isOfType(Type.GRASS)), + .condition((user, target, move) => !target.getTag(BattlerTagType.SEEDED) && !target.isOfType(Type.GRASS)) + .reflectable(), new SelfStatusMove(Moves.GROWTH, Type.NORMAL, -1, 20, -1, 0, 1) .attr(GrowthStatStageChangeAttr), new AttackMove(Moves.RAZOR_LEAF, Type.GRASS, MoveCategory.PHYSICAL, 55, 95, 25, -1, 0, 1) @@ -8490,13 +8513,16 @@ export function initMoves() { .attr(AntiSunlightPowerDecreaseAttr), new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.POISON) - .powderMove(), + .powderMove() + .reflectable(), new StatusMove(Moves.STUN_SPORE, Type.GRASS, 75, 30, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) - .powderMove(), + .powderMove() + .reflectable(), new StatusMove(Moves.SLEEP_POWDER, Type.GRASS, 75, 15, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.SLEEP) - .powderMove(), + .powderMove() + .reflectable(), new AttackMove(Moves.PETAL_DANCE, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1) .attr(FrenzyAttr) .attr(MissEffectAttr, frenzyMissFunc) @@ -8506,7 +8532,8 @@ export function initMoves() { .target(MoveTarget.RANDOM_NEAR_ENEMY), new StatusMove(Moves.STRING_SHOT, Type.BUG, 95, 40, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPD ], -2) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.DRAGON_RAGE, Type.DRAGON, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 1) .attr(FixedDamageAttr, 40), new AttackMove(Moves.FIRE_SPIN, Type.FIRE, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 1) @@ -8517,7 +8544,8 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new StatusMove(Moves.THUNDER_WAVE, Type.ELECTRIC, 90, 20, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) - .attr(RespectAttackTypeImmunityAttr), + .attr(RespectAttackTypeImmunityAttr) + .reflectable(), new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(ThunderAccuracyAttr) @@ -8539,13 +8567,15 @@ export function initMoves() { .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND), new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.TOXIC) - .attr(ToxicAccuracyAttr), + .attr(ToxicAccuracyAttr) + .reflectable(), new AttackMove(Moves.CONFUSION, Type.PSYCHIC, MoveCategory.SPECIAL, 50, 100, 25, 10, 0, 1) .attr(ConfuseAttr), new AttackMove(Moves.PSYCHIC, Type.PSYCHIC, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1), new StatusMove(Moves.HYPNOSIS, Type.PSYCHIC, 60, 20, -1, 0, 1) - .attr(StatusEffectAttr, StatusEffect.SLEEP), + .attr(StatusEffectAttr, StatusEffect.SLEEP) + .reflectable(), new SelfStatusMove(Moves.MEDITATE, Type.PSYCHIC, -1, 40, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true), new SelfStatusMove(Moves.AGILITY, Type.PSYCHIC, -1, 30, -1, 0, 1) @@ -8563,7 +8593,8 @@ export function initMoves() { .ignoresSubstitute(), new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.DEF ], -2) - .soundBased(), + .soundBased() + .reflectable(), new SelfStatusMove(Moves.DOUBLE_TEAM, Type.NORMAL, -1, 15, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.EVA ], 1, true), new SelfStatusMove(Moves.RECOVER, Type.NORMAL, -1, 5, -1, 0, 1) @@ -8575,9 +8606,11 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.MINIMIZED, true, false) .attr(StatStageChangeAttr, [ Stat.EVA ], 2, true), new StatusMove(Moves.SMOKESCREEN, Type.NORMAL, 100, 20, -1, 0, 1) - .attr(StatStageChangeAttr, [ Stat.ACC ], -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1) + .reflectable(), new StatusMove(Moves.CONFUSE_RAY, Type.GHOST, 100, 10, -1, 0, 1) - .attr(ConfuseAttr), + .attr(ConfuseAttr) + .reflectable(), new SelfStatusMove(Moves.WITHDRAW, Type.WATER, -1, 40, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), new SelfStatusMove(Moves.DEFENSE_CURL, Type.NORMAL, -1, 40, -1, 0, 1) @@ -8638,7 +8671,8 @@ export function initMoves() { new SelfStatusMove(Moves.AMNESIA, Type.PSYCHIC, -1, 20, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], 2, true), new StatusMove(Moves.KINESIS, Type.PSYCHIC, 80, 15, -1, 0, 1) - .attr(StatStageChangeAttr, [ Stat.ACC ], -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1) + .reflectable(), new SelfStatusMove(Moves.SOFT_BOILED, Type.NORMAL, -1, 5, -1, 0, 1) .attr(HealAttr, 0.5) .triageMove(), @@ -8648,14 +8682,16 @@ export function initMoves() { .condition(failOnGravityCondition) .recklessMove(), new StatusMove(Moves.GLARE, Type.NORMAL, 100, 30, -1, 0, 1) - .attr(StatusEffectAttr, StatusEffect.PARALYSIS), + .attr(StatusEffectAttr, StatusEffect.PARALYSIS) + .reflectable(), new AttackMove(Moves.DREAM_EATER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 15, -1, 0, 1) .attr(HitHealAttr) .condition(targetSleptOrComatoseCondition) .triageMove(), new StatusMove(Moves.POISON_GAS, Type.POISON, 90, 40, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.POISON) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.BARRAGE, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1) .attr(MultiHitAttr) .makesContact(false) @@ -8664,7 +8700,8 @@ export function initMoves() { .attr(HitHealAttr) .triageMove(), new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1) - .attr(StatusEffectAttr, StatusEffect.SLEEP), + .attr(StatusEffectAttr, StatusEffect.SLEEP) + .reflectable(), new ChargingAttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1) .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .attr(HighCritAttr) @@ -8683,9 +8720,11 @@ export function initMoves() { .punchingMove(), new StatusMove(Moves.SPORE, Type.GRASS, 100, 15, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.SLEEP) - .powderMove(), + .powderMove() + .reflectable(), new StatusMove(Moves.FLASH, Type.NORMAL, 100, 20, -1, 0, 1) - .attr(StatStageChangeAttr, [ Stat.ACC ], -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1) + .reflectable(), new AttackMove(Moves.PSYWAVE, Type.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1) .attr(RandomLevelDamageAttr), new SelfStatusMove(Moves.SPLASH, Type.NORMAL, -1, 40, -1, 0, 1) @@ -8744,7 +8783,8 @@ export function initMoves() { .attr(StealHeldItemChanceAttr, 0.3), new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2) .condition(failIfGhostTypeCondition) - .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), + .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1) + .reflectable(), new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2) .attr(IgnoreAccuracyAttr), new StatusMove(Moves.NIGHTMARE, Type.GHOST, 100, 15, -1, 0, 2) @@ -8775,12 +8815,14 @@ export function initMoves() { new StatusMove(Moves.COTTON_SPORE, Type.GRASS, 100, 40, -1, 0, 2) .attr(StatStageChangeAttr, [ Stat.SPD ], -2) .powderMove() - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.REVERSAL, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2) .attr(LowHpPowerAttr), new StatusMove(Moves.SPITE, Type.GHOST, 100, 10, -1, 0, 2) .ignoresSubstitute() - .attr(ReducePpMoveAttr, 4), + .attr(ReducePpMoveAttr, 4) + .reflectable(), new AttackMove(Moves.POWDER_SNOW, Type.ICE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 2) .attr(StatusEffectAttr, StatusEffect.FREEZE) .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -8790,10 +8832,12 @@ export function initMoves() { new AttackMove(Moves.MACH_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2) .punchingMove(), new StatusMove(Moves.SCARY_FACE, Type.NORMAL, 100, 10, -1, 0, 2) - .attr(StatStageChangeAttr, [ Stat.SPD ], -2), + .attr(StatStageChangeAttr, [ Stat.SPD ], -2) + .reflectable(), new AttackMove(Moves.FEINT_ATTACK, Type.DARK, MoveCategory.PHYSICAL, 60, -1, 20, -1, 0, 2), new StatusMove(Moves.SWEET_KISS, Type.FAIRY, 75, 10, -1, 0, 2) - .attr(ConfuseAttr), + .attr(ConfuseAttr) + .reflectable(), new SelfStatusMove(Moves.BELLY_DRUM, Type.NORMAL, -1, 10, -1, 0, 2) .attr(CutHpStatStageBoostAttr, [ Stat.ATK ], 12, 2, (user) => { globalScene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", { pokemonName: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })); @@ -8808,13 +8852,15 @@ export function initMoves() { .ballBombMove(), new StatusMove(Moves.SPIKES, Type.GROUND, -1, 20, -1, 0, 2) .attr(AddArenaTrapTagAttr, ArenaTagType.SPIKES) - .target(MoveTarget.ENEMY_SIDE), + .target(MoveTarget.ENEMY_SIDE) + .reflectable(), new AttackMove(Moves.ZAP_CANNON, Type.ELECTRIC, MoveCategory.SPECIAL, 120, 50, 5, 100, 0, 2) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .ballBombMove(), new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST) - .ignoresSubstitute(), + .ignoresSubstitute() + .reflectable(), new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2) .ignoresProtect() .attr(DestinyBondAttr) @@ -8860,7 +8906,8 @@ export function initMoves() { .attr(ProtectAttr, BattlerTagType.ENDURING) .condition(failIfLastCondition), new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2) - .attr(StatStageChangeAttr, [ Stat.ATK ], -2), + .attr(StatStageChangeAttr, [ Stat.ATK ], -2) + .reflectable(), new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2) .partial() // Does not lock the user, also does not increase damage properly .attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL), @@ -8868,7 +8915,8 @@ export function initMoves() { .attr(SurviveDamageAttr), new StatusMove(Moves.SWAGGER, Type.NORMAL, 85, 15, -1, 0, 2) .attr(StatStageChangeAttr, [ Stat.ATK ], 2) - .attr(ConfuseAttr), + .attr(ConfuseAttr) + .reflectable(), new SelfStatusMove(Moves.MILK_DRINK, Type.NORMAL, -1, 5, -1, 0, 2) .attr(HealAttr, 0.5) .triageMove(), @@ -8881,11 +8929,13 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2) .condition(failIfGhostTypeCondition) - .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), + .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1) + .reflectable(), new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.INFATUATED) .ignoresSubstitute() - .condition((user, target, move) => user.isOppositeGender(target)), + .condition((user, target, move) => user.isOppositeGender(target)) + .reflectable(), new SelfStatusMove(Moves.SLEEP_TALK, Type.NORMAL, -1, 10, -1, 0, 2) .attr(BypassSleepAttr) .attr(RandomMovesetMoveAttr, invalidSleepTalkMoves, false) @@ -8932,7 +8982,8 @@ export function initMoves() { new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) .ignoresSubstitute() - .condition((user, target, move) => new EncoreTag(user.id).canAdd(target)), + .condition((user, target, move) => new EncoreTag(user.id).canAdd(target)) + .reflectable(), new AttackMove(Moves.PURSUIT, Type.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2) .partial(), // No effect implemented new AttackMove(Moves.RAPID_SPIN, Type.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2) @@ -8953,7 +9004,8 @@ export function initMoves() { .attr(RemoveArenaTrapAttr), new StatusMove(Moves.SWEET_SCENT, Type.NORMAL, 100, 20, -1, 0, 2) .attr(StatStageChangeAttr, [ Stat.EVA ], -2) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.IRON_TAIL, Type.STEEL, MoveCategory.PHYSICAL, 100, 75, 15, 30, 0, 2) .attr(StatStageChangeAttr, [ Stat.DEF ], -1), new AttackMove(Moves.METAL_CLAW, Type.STEEL, MoveCategory.PHYSICAL, 50, 95, 35, 10, 0, 2) @@ -9041,12 +9093,15 @@ export function initMoves() { new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3) .ignoresSubstitute() .edgeCase() // Incomplete implementation because of Uproar's partial implementation - .attr(AddBattlerTagAttr, BattlerTagType.TORMENT, false, true, 1), + .attr(AddBattlerTagAttr, BattlerTagType.TORMENT, false, true, 1) + .reflectable(), new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1) - .attr(ConfuseAttr), + .attr(ConfuseAttr) + .reflectable(), new StatusMove(Moves.WILL_O_WISP, Type.FIRE, 85, 15, -1, 0, 3) - .attr(StatusEffectAttr, StatusEffect.BURN), + .attr(StatusEffectAttr, StatusEffect.BURN) + .reflectable(), new StatusMove(Moves.MEMENTO, Type.DARK, 100, 10, -1, 0, 3) .attr(SacrificialAttrOnHit) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -2), @@ -9070,7 +9125,8 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false), new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3) .ignoresSubstitute() - .attr(AddBattlerTagAttr, BattlerTagType.TAUNT, false, true, 4), + .attr(AddBattlerTagAttr, BattlerTagType.TAUNT, false, true, 4) + .reflectable(), new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3) .attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND) .ignoresSubstitute() @@ -9093,7 +9149,12 @@ export function initMoves() { new AttackMove(Moves.SUPERPOWER, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1, true), new SelfStatusMove(Moves.MAGIC_COAT, Type.PSYCHIC, -1, 15, -1, 4, 3) - .unimplemented(), + .attr(AddBattlerTagAttr, BattlerTagType.MAGIC_COAT, true, true, 0) + .condition(failIfLastCondition) + // Interactions with stomping tantrum, instruct, and other moves that + // rely on move history + // Also will not reflect roar / whirlwind if the target has ForceSwitchOutImmunityAbAttr + .edgeCase(), new SelfStatusMove(Moves.RECYCLE, Type.NORMAL, -1, 10, -1, 0, 3) .unimplemented(), new AttackMove(Moves.REVENGE, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, -4, 3) @@ -9102,7 +9163,8 @@ export function initMoves() { .attr(RemoveScreensAttr), new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3) .attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true) - .condition((user, target, move) => !target.status && !target.isSafeguarded(user)), + .condition((user, target, move) => !target.status && !target.isSafeguarded(user)) + .reflectable(), new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1) .attr(RemoveHeldItemAttr, false), @@ -9146,7 +9208,8 @@ export function initMoves() { .ballBombMove(), new StatusMove(Moves.FEATHER_DANCE, Type.FLYING, 100, 15, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.ATK ], -2) - .danceMove(), + .danceMove() + .reflectable(), new StatusMove(Moves.TEETER_DANCE, Type.NORMAL, 100, 20, -1, 0, 3) .attr(ConfuseAttr) .danceMove() @@ -9192,7 +9255,8 @@ export function initMoves() { .attr(PartyStatusCureAttr, i18next.t("moveTriggers:soothingAromaWaftedThroughArea"), Abilities.SAP_SIPPER) .target(MoveTarget.PARTY), new StatusMove(Moves.FAKE_TEARS, Type.DARK, 100, 20, -1, 0, 3) - .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2) + .reflectable(), new AttackMove(Moves.AIR_CUTTER, Type.FLYING, MoveCategory.SPECIAL, 60, 95, 25, -1, 0, 3) .attr(HighCritAttr) .slicingMove() @@ -9203,7 +9267,8 @@ export function initMoves() { .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE), new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST) - .ignoresSubstitute(), + .ignoresSubstitute() + .reflectable(), new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .makesContact(false), @@ -9212,12 +9277,15 @@ export function initMoves() { .windMove(), new StatusMove(Moves.METAL_SOUND, Type.STEEL, 85, 40, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2) - .soundBased(), + .soundBased() + .reflectable(), new StatusMove(Moves.GRASS_WHISTLE, Type.GRASS, 55, 15, -1, 0, 3) .attr(StatusEffectAttr, StatusEffect.SLEEP) - .soundBased(), + .soundBased() + .reflectable(), new StatusMove(Moves.TICKLE, Type.NORMAL, 100, 20, -1, 0, 3) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1) + .reflectable(), new SelfStatusMove(Moves.COSMIC_POWER, Type.PSYCHIC, -1, 20, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, true), new AttackMove(Moves.WATER_SPOUT, Type.WATER, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 3) @@ -9255,7 +9323,8 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3) .condition(failIfGhostTypeCondition) - .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), + .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1) + .reflectable(), new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.ATK ], 1) .soundBased() @@ -9318,7 +9387,8 @@ export function initMoves() { .target(MoveTarget.BOTH_SIDES), new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK) - .ignoresSubstitute(), + .ignoresSubstitute() + .reflectable(), new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4) .attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1) .attr(HealStatusEffectAttr, false, StatusEffect.SLEEP), @@ -9364,6 +9434,7 @@ export function initMoves() { new AttackMove(Moves.ASSURANCE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 4) .attr(MovePowerMultiplierAttr, (user, target, move) => target.turnData.damageTaken > 0 ? 2 : 1), new StatusMove(Moves.EMBARGO, Type.DARK, 100, 15, -1, 0, 4) + .reflectable() .unimplemented(), new AttackMove(Moves.FLING, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 4) .makesContact(false) @@ -9383,14 +9454,16 @@ export function initMoves() { .attr(LessPPMorePowerAttr), new StatusMove(Moves.HEAL_BLOCK, Type.PSYCHIC, 100, 15, -1, 0, 4) .attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, true, 5) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.WRING_OUT, Type.NORMAL, MoveCategory.SPECIAL, -1, 100, 5, -1, 0, 4) .attr(OpponentHighHpPowerAttr, 120) .makesContact(), new SelfStatusMove(Moves.POWER_TRICK, Type.PSYCHIC, -1, 10, -1, 0, 4) .attr(AddBattlerTagAttr, BattlerTagType.POWER_TRICK, true), new StatusMove(Moves.GASTRO_ACID, Type.POISON, 100, 10, -1, 0, 4) - .attr(SuppressAbilitiesAttr), + .attr(SuppressAbilitiesAttr) + .reflectable(), new StatusMove(Moves.LUCKY_CHANT, Type.NORMAL, -1, 30, -1, 0, 4) .attr(AddArenaTagAttr, ArenaTagType.NO_CRIT, 5, true, true) .target(MoveTarget.USER_SIDE), @@ -9412,12 +9485,14 @@ export function initMoves() { new AttackMove(Moves.LAST_RESORT, Type.NORMAL, MoveCategory.PHYSICAL, 140, 100, 5, -1, 0, 4) .attr(LastResortAttr), new StatusMove(Moves.WORRY_SEED, Type.GRASS, 100, 10, -1, 0, 4) - .attr(AbilityChangeAttr, Abilities.INSOMNIA), + .attr(AbilityChangeAttr, Abilities.INSOMNIA) + .reflectable(), new AttackMove(Moves.SUCKER_PUNCH, Type.DARK, MoveCategory.PHYSICAL, 70, 100, 5, -1, 1, 4) .condition((user, target, move) => globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS), // TODO: is this bang correct? new StatusMove(Moves.TOXIC_SPIKES, Type.POISON, -1, 20, -1, 0, 4) .attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES) - .target(MoveTarget.ENEMY_SIDE), + .target(MoveTarget.ENEMY_SIDE) + .reflectable(), new StatusMove(Moves.HEART_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4) .attr(SwapStatStagesAttr, BATTLE_STATS) .ignoresSubstitute(), @@ -9529,7 +9604,8 @@ export function initMoves() { .attr(ClearTerrainAttr) .attr(RemoveScreensAttr, false) .attr(RemoveArenaTrapAttr, true) - .attr(RemoveArenaTagsAttr, [ ArenaTagType.MIST, ArenaTagType.SAFEGUARD ], false), + .attr(RemoveArenaTagsAttr, [ ArenaTagType.MIST, ArenaTagType.SAFEGUARD ], false) + .reflectable(), new StatusMove(Moves.TRICK_ROOM, Type.PSYCHIC, -1, 5, -1, -7, 4) .attr(AddArenaTagAttr, ArenaTagType.TRICK_ROOM, 5) .ignoresProtect() @@ -9567,10 +9643,12 @@ export function initMoves() { new StatusMove(Moves.CAPTIVATE, Type.NORMAL, 100, 20, -1, 0, 4) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2) .condition((user, target, move) => target.isOppositeGender(user)) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new StatusMove(Moves.STEALTH_ROCK, Type.ROCK, -1, 20, -1, 0, 4) .attr(AddArenaTrapTagAttr, ArenaTagType.STEALTH_ROCK) - .target(MoveTarget.ENEMY_SIDE), + .target(MoveTarget.ENEMY_SIDE) + .reflectable(), new AttackMove(Moves.GRASS_KNOT, Type.GRASS, MoveCategory.SPECIAL, -1, 100, 20, -1, 0, 4) .attr(WeightPowerAttr) .makesContact(), @@ -9614,7 +9692,8 @@ export function initMoves() { .attr(TrapAttr, BattlerTagType.MAGMA_STORM), new StatusMove(Moves.DARK_VOID, Type.DARK, 80, 10, -1, 0, 4) //Accuracy from Generations 4-6 .attr(StatusEffectAttr, StatusEffect.SLEEP) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.SEED_FLARE, Type.GRASS, MoveCategory.SPECIAL, 120, 85, 5, 40, 0, 4) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4) @@ -9654,7 +9733,8 @@ export function initMoves() { .condition((_user, target, _move) => !(target.species.speciesId === Species.GENGAR && target.getFormKey() === "mega")) .condition((_user, target, _move) => Utils.isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && Utils.isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING))) .attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3) - .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3), + .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3) + .reflectable(), new StatusMove(Moves.MAGIC_ROOM, Type.PSYCHIC, -1, 10, -1, 0, 5) .ignoresProtect() .target(MoveTarget.BOTH_SIDES) @@ -9687,7 +9767,8 @@ export function initMoves() { .attr(ElectroBallPowerAttr) .ballBombMove(), new StatusMove(Moves.SOAK, Type.WATER, 100, 20, -1, 0, 5) - .attr(ChangeTypeAttr, Type.WATER), + .attr(ChangeTypeAttr, Type.WATER) + .reflectable(), new AttackMove(Moves.FLAME_CHARGE, Type.FIRE, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 5) .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true), new SelfStatusMove(Moves.COIL, Type.POISON, -1, 20, -1, 0, 5) @@ -9700,9 +9781,11 @@ export function initMoves() { new AttackMove(Moves.FOUL_PLAY, Type.DARK, MoveCategory.PHYSICAL, 95, 100, 15, -1, 0, 5) .attr(TargetAtkUserAtkAttr), new StatusMove(Moves.SIMPLE_BEAM, Type.NORMAL, 100, 15, -1, 0, 5) - .attr(AbilityChangeAttr, Abilities.SIMPLE), + .attr(AbilityChangeAttr, Abilities.SIMPLE) + .reflectable(), new StatusMove(Moves.ENTRAINMENT, Type.NORMAL, 100, 15, -1, 0, 5) - .attr(AbilityGiveAttr), + .attr(AbilityGiveAttr) + .reflectable(), new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5) .ignoresProtect() .ignoresSubstitute() @@ -9740,7 +9823,8 @@ export function initMoves() { new StatusMove(Moves.HEAL_PULSE, Type.PSYCHIC, -1, 10, -1, 0, 5) .attr(HealAttr, 0.5, false, false) .pulseMove() - .triageMove(), + .triageMove() + .reflectable(), new AttackMove(Moves.HEX, Type.GHOST, MoveCategory.SPECIAL, 65, 100, 10, -1, 0, 5) .attr( MovePowerMultiplierAttr, @@ -9943,7 +10027,8 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }), new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6) .attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB) - .target(MoveTarget.ENEMY_SIDE), + .target(MoveTarget.ENEMY_SIDE) + .reflectable(), new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6) .attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ), new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) @@ -9951,10 +10036,12 @@ export function initMoves() { .chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN) .ignoresProtect(), new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) - .attr(AddTypeAttr, Type.GHOST), + .attr(AddTypeAttr, Type.GHOST) + .reflectable(), new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1) - .soundBased(), + .soundBased() + .reflectable(), new StatusMove(Moves.ION_DELUGE, Type.ELECTRIC, -1, 25, -1, 1, 6) .attr(AddArenaTagAttr, ArenaTagType.ION_DELUGE) .target(MoveTarget.BOTH_SIDES), @@ -9963,7 +10050,8 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS) .triageMove(), new StatusMove(Moves.FORESTS_CURSE, Type.GRASS, 100, 20, -1, 0, 6) - .attr(AddTypeAttr, Type.GRASS), + .attr(AddTypeAttr, Type.GRASS) + .reflectable(), new AttackMove(Moves.PETAL_BLIZZARD, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 6) .windMove() .makesContact(false) @@ -9977,9 +10065,11 @@ export function initMoves() { new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY }) .attr(ForceSwitchOutAttr, true) - .soundBased(), + .soundBased() + .reflectable(), new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) - .attr(InvertStatsAttr), + .attr(InvertStatsAttr) + .reflectable(), new AttackMove(Moves.DRAINING_KISS, Type.FAIRY, MoveCategory.SPECIAL, 50, 100, 10, -1, 0, 6) .attr(HitHealAttr, 0.75) .makesContact() @@ -10018,10 +10108,12 @@ export function initMoves() { .condition(failIfLastCondition), new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK ], -1) - .ignoresSubstitute(), + .ignoresSubstitute() + .reflectable(), new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) - .soundBased(), + .soundBased() + .reflectable(), new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true }) .makesContact(false) @@ -10048,14 +10140,17 @@ export function initMoves() { .condition(failIfSingleBattle) .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.SPATK ], -2), + .attr(StatStageChangeAttr, [ Stat.SPATK ], -2) + .reflectable(), new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC }) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) .attr(AddBattlerTagAttr, BattlerTagType.POWDER, false, true) .ignoresSubstitute() - .powderMove(), + .powderMove() + .reflectable(), new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) .chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" })) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true), @@ -10077,7 +10172,8 @@ export function initMoves() { .ignoresSubstitute() .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.BABY_DOLL_EYES, Type.FAIRY, 100, 30, -1, 1, 6) - .attr(StatStageChangeAttr, [ Stat.ATK ], -1), + .attr(StatStageChangeAttr, [ Stat.ATK ], -1) + .reflectable(), new AttackMove(Moves.NUZZLE, Type.ELECTRIC, MoveCategory.PHYSICAL, 20, 100, 20, 100, 0, 6) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new AttackMove(Moves.HOLD_BACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 6) @@ -10221,13 +10317,15 @@ export function initMoves() { .punchingMove(), new StatusMove(Moves.FLORAL_HEALING, Type.FAIRY, -1, 10, -1, 0, 7) .attr(BoostHealAttr, 0.5, 2 / 3, true, false, (user, target, move) => globalScene.arena.terrain?.terrainType === TerrainType.GRASSY) - .triageMove(), + .triageMove() + .reflectable(), new AttackMove(Moves.HIGH_HORSEPOWER, Type.GROUND, MoveCategory.PHYSICAL, 95, 95, 10, -1, 0, 7), new StatusMove(Moves.STRENGTH_SAP, Type.GRASS, 100, 10, -1, 0, 7) .attr(HitHealAttr, null, Stat.ATK) .attr(StatStageChangeAttr, [ Stat.ATK ], -1) .condition((user, target, move) => target.getStatStage(Stat.ATK) > -6) - .triageMove(), + .triageMove() + .reflectable(), new ChargingAttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7) .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]) @@ -10237,10 +10335,12 @@ export function initMoves() { .makesContact(false), new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7) .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false) - .condition(failIfSingleBattle), + .condition(failIfSingleBattle) + .reflectable(), new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7) .attr(StatusEffectAttr, StatusEffect.POISON) - .attr(StatStageChangeAttr, [ Stat.SPD ], -1), + .attr(StatStageChangeAttr, [ Stat.SPD ], -1) + .reflectable(), new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7) .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7) @@ -10284,7 +10384,8 @@ export function initMoves() { (user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct? .attr(HealAttr, 0.5) .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) - .triageMove(), + .triageMove() + .reflectable(), new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7) .danceMove() .attr(MatchUserTypeAttr), @@ -10373,7 +10474,8 @@ export function initMoves() { new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) .ignoresAbilities(), new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1) + .reflectable(), new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7) .attr(FlinchAttr), new AttackMove(Moves.NATURES_MADNESS, Type.FAIRY, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 7) @@ -10492,10 +10594,12 @@ export function initMoves() { .condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.SPD ], -1) - .attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false), + .attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false) + .reflectable(), new StatusMove(Moves.MAGIC_POWDER, Type.PSYCHIC, 100, 20, -1, 0, 8) .attr(ChangeTypeAttr, Type.PSYCHIC) - .powderMove(), + .powderMove() + .reflectable(), new AttackMove(Moves.DRAGON_DARTS, Type.DRAGON, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 8) .attr(MultiHitAttr, MultiHitType._2) .makesContact(false) @@ -10672,6 +10776,7 @@ export function initMoves() { .makesContact(false), new StatusMove(Moves.CORROSIVE_GAS, Type.POISON, 100, 40, -1, 0, 8) .target(MoveTarget.ALL_NEAR_OTHERS) + .reflectable() .unimplemented(), new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1) diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index f28ac37ae27..719b08c5b81 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -94,4 +94,5 @@ export enum BattlerTagType { PSYCHO_SHIFT = "PSYCHO_SHIFT", ENDURE_TOKEN = "ENDURE_TOKEN", POWDER = "POWDER", + MAGIC_COAT = "MAGIC_COAT", } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index be9a36940ea..35fe446fc43 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -12,6 +12,7 @@ import { PostAttackAbAttr, PostDamageAbAttr, PostDefendAbAttr, + ReflectStatusMoveAbAttr, TypeImmunityAbAttr, } from "#app/data/ability"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; @@ -31,6 +32,7 @@ import { AttackMove, DelayedAttackAttr, FlinchAttr, + getMoveTargets, HitsTagAttr, MissEffectAttr, MoveCategory, @@ -47,7 +49,7 @@ import { } from "#app/data/move"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { Type } from "#enums/type"; -import type { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { HitResult, MoveResult } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -60,17 +62,27 @@ import { } from "#app/modifier/modifier"; import { PokemonPhase } from "#app/phases/pokemon-phase"; import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/utils"; +import { type nil } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import type { Moves } from "#enums/moves"; import i18next from "i18next"; +import type { Phase } from "#app/phase"; +import { ShowAbilityPhase } from "./show-ability-phase"; +import { MovePhase } from "./move-phase"; +import { MoveEndPhase } from "./move-end-phase"; export class MoveEffectPhase extends PokemonPhase { public move: PokemonMove; protected targets: BattlerIndex[]; + protected reflected: boolean = false; - constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) { + /** + * @param reflected Indicates that the move was reflected by the user due to magic coat or magic bounce + */ + constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove, reflected: boolean = false) { super(battlerIndex); this.move = move; + this.reflected = reflected; /** * In double battles, if the right Pokemon selects a spread move and the left Pokemon dies * with no party members available to switch in, then the right Pokemon takes the index @@ -184,12 +196,14 @@ export class MoveEffectPhase extends PokemonPhase { && (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) && !targets[0]?.getTag(SemiInvulnerableTag); + const mayBounce = move.hasFlag(MoveFlags.REFLECTABLE) && !this.reflected && targets.some(t => t.hasAbilityWithAttr(ReflectStatusMoveAbAttr) || !!t.getTag(BattlerTagType.MAGIC_COAT)); + /** - * If no targets are left for the move to hit (FAIL), or the invoked move is single-target + * If no targets are left for the move to hit (FAIL), or the invoked move is non-reflectable, single-target * (and not random target) and failed the hit check against its target (MISS), log the move * as FAILed or MISSed (depending on the conditions above) and end this phase. */ - if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { + if (!hasActiveTargets || (!mayBounce && !move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { this.stopMultiHit(); if (hasActiveTargets) { globalScene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" })); @@ -211,12 +225,21 @@ export class MoveEffectPhase extends PokemonPhase { new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(move.hitsSubstitute(user, this.getFirstTarget()!), () => { /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; - for (const target of targets) { - // Prevent ENEMY_SIDE targeted moves from occurring twice in double battles - if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length - 1]) { - continue; - } + // Prevent ENEMY_SIDE targeted moves from occurring twice in double battles + // and check which target will magic bounce. + const trueTargets: Pokemon[] = move.moveTarget !== MoveTarget.ENEMY_SIDE ? targets : (() => { + const magicCoatTargets = targets.filter(t => t.getTag(BattlerTagType.MAGIC_COAT) || t.hasAbilityWithAttr(ReflectStatusMoveAbAttr)); + + // only magic coat effect cares about order + if (!mayBounce || magicCoatTargets.length === 0) { + return [ targets[0] ]; + } + return [ magicCoatTargets[0] ]; + })(); + + const queuedPhases: Phase[] = []; + for (const target of trueTargets) { /** The {@linkcode ArenaTagSide} to which the target belongs */ const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ @@ -229,7 +252,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ - const isProtected = ( + const isProtected = !([ MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES ].includes(this.move.getMove().moveTarget)) && ( bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) && (hasConditionalProtectApplied.value @@ -238,13 +261,39 @@ export class MoveEffectPhase extends PokemonPhase { || (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); + /** Is the target hidden by the effects of its Commander ability? */ + const isCommanding = globalScene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon() === target; + + /** Is the target reflecting status moves from the magic coat move? */ + const isReflecting = !!target.getTag(BattlerTagType.MAGIC_COAT); + + /** Is the target's magic bounce ability not ignored and able to reflect this move? */ + const canMagicBounce = !isReflecting && !move.checkFlag(MoveFlags.IGNORE_ABILITIES, user, target) && target.hasAbilityWithAttr(ReflectStatusMoveAbAttr); + + const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); + + /** Is the target reflecting the effect, not protected, and not in an semi-invulnerable state?*/ + const willBounce = (!isProtected && !this.reflected && !isCommanding + && move.hasFlag(MoveFlags.REFLECTABLE) + && (isReflecting || canMagicBounce) + && !semiInvulnerableTag); + + // If the move will bounce, then queue the bounce and move on to the next target + if (!target.switchOutStatus && willBounce) { + const newTargets = move.isMultiTarget() ? getMoveTargets(target, move.id).targets : [ user.getBattlerIndex() ]; + if (!isReflecting) { + queuedPhases.push(new ShowAbilityPhase(target.getBattlerIndex(), target.getPassiveAbility().hasAttr(ReflectStatusMoveAbAttr))); + } + + queuedPhases.push(new MovePhase(target, newTargets, new PokemonMove(move.id, 0, 0, true), true, true, true)); + continue; + } + /** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */ const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) - && !target.getTag(SemiInvulnerableTag); + && !semiInvulnerableTag; - /** Is the target hidden by the effects of its Commander ability? */ - const isCommanding = globalScene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon() === target; /** * If the move missed a target, stop all future hits against that target @@ -371,6 +420,10 @@ export class MoveEffectPhase extends PokemonPhase { applyAttrs.push(k); } + // Apply queued phases + if (queuedPhases.length) { + globalScene.appendToPhase(queuedPhases, MoveEndPhase); + } // Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ? applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : @@ -586,12 +639,7 @@ export class MoveEffectPhase extends PokemonPhase { } } - if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { - return true; - } - - // If the user should ignore accuracy on a target, check who the user targeted last turn and see if they match - if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) { + if (this.checkBypassAccAndInvuln(target)) { return true; } @@ -599,15 +647,12 @@ export class MoveEffectPhase extends PokemonPhase { return true; } - if (target.getTag(BattlerTagType.TELEKINESIS) && !target.getTag(SemiInvulnerableTag) && !this.move.getMove().hasAttr(OneHitKOAttr)) { + const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); + if (target.getTag(BattlerTagType.TELEKINESIS) && !semiInvulnerableTag && !this.move.getMove().hasAttr(OneHitKOAttr)) { return true; } - const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); - if (semiInvulnerableTag - && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType) - && !(this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON)) - ) { + if (semiInvulnerableTag && !this.checkBypassSemiInvuln(semiInvulnerableTag)) { return false; } @@ -623,6 +668,52 @@ export class MoveEffectPhase extends PokemonPhase { return rand < (moveAccuracy * accuracyMultiplier); } + /** + * Check whether the move should bypass *both* the accuracy *and* semi-invulnerable states. + * @param target - The {@linkcode Pokemon} targeted by the invoked move + * @returns `true` if the move should bypass accuracy and semi-invulnerability + * + * Accuracy and semi-invulnerability can be bypassed by: + * - An ability like {@linkcode Abilities.NO_GUARD | No Guard} + * - A poison type using {@linkcode Moves.TOXIC | Toxic} + * - A move like {@linkcode Moves.LOCK_ON | Lock-On} or {@linkcode Moves.MIND_READER | Mind Reader}. + * + * Does *not* check against effects {@linkcode Moves.GLAIVE_RUSH | Glaive Rush} status (which + * should not bypass semi-invulnerability), or interactions like Earthquake hitting against Dig, + * (which should not bypass the accuracy check). + * + * @see {@linkcode hitCheck} + */ + public checkBypassAccAndInvuln(target: Pokemon) { + const user = this.getUserPokemon(); + if (!user) { + return false; + } + if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { + return true; + } + if ((this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON))) { + return true; + } + // TODO: Fix lock on / mind reader check. + if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) { + return true; + } + } + + /** + * Check whether the move is able to ignore the given `semiInvulnerableTag` + * @param semiInvulnerableTag - The semiInvulnerbale tag to check against + * @returns `true` if the move can ignore the semi-invulnerable state + */ + public checkBypassSemiInvuln(semiInvulnerableTag: SemiInvulnerableTag | nil): boolean { + if (!semiInvulnerableTag) { + return false; + } + const move = this.move.getMove(); + return move.getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType); + } + /** @returns The {@linkcode Pokemon} using this phase's invoked move */ public getUserPokemon(): Pokemon | null { if (this.battlerIndex > BattlerIndex.ENEMY_2) { diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 5330540c8b2..9d32189edb5 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -58,6 +58,7 @@ export class MovePhase extends BattlePhase { protected ignorePp: boolean; protected failed: boolean = false; protected cancelled: boolean = false; + protected reflected: boolean = false; public get pokemon(): Pokemon { return this._pokemon; @@ -84,10 +85,12 @@ export class MovePhase extends BattlePhase { } /** - * @param followUp Indicates that the move being uses is a "follow-up" - for example, a move being used by Metronome or Dancer. + * @param followUp Indicates that the move being used is a "follow-up" - for example, a move being used by Metronome or Dancer. * Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc. + * @param reflected Indicates that the move was reflected by Magic Coat or Magic Bounce. + * Reflected moves cannot be reflected again and will not trigger Dancer. */ - constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false) { + constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false, reflected: boolean = false) { super(); this.pokemon = pokemon; @@ -95,6 +98,7 @@ export class MovePhase extends BattlePhase { this.move = move; this.followUp = followUp; this.ignorePp = ignorePp; + this.reflected = reflected; } /** @@ -140,7 +144,7 @@ export class MovePhase extends BattlePhase { } // Check move to see if arena.ignoreAbilities should be true. - if (!this.followUp) { + if (!this.followUp || this.reflected) { if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) { globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex()); } @@ -335,7 +339,7 @@ export class MovePhase extends BattlePhase { */ if (success) { applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove()); - globalScene.unshiftPhase(new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, this.move)); + globalScene.unshiftPhase(new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, this.move, this.reflected)); } else { if ([ Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE ].includes(this.move.moveId)) { @@ -543,7 +547,7 @@ export class MovePhase extends BattlePhase { return; } - globalScene.queueMessage(i18next.t("battle:useMove", { + globalScene.queueMessage(i18next.t(this.reflected ? "battle:magicCoatActivated" : "battle:useMove", { pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), moveName: this.move.getName() }), 500); diff --git a/src/test/abilities/magic_bounce.test.ts b/src/test/abilities/magic_bounce.test.ts new file mode 100644 index 00000000000..2fc460662ca --- /dev/null +++ b/src/test/abilities/magic_bounce.test.ts @@ -0,0 +1,351 @@ +import { BattlerIndex } from "#app/battle"; +import { allAbilities } from "#app/data/ability"; +import { ArenaTagSide } from "#app/data/arena-tag"; +import { allMoves } from "#app/data/move"; +import { ArenaTagType } from "#app/enums/arena-tag-type"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { Stat } from "#app/enums/stat"; +import { StatusEffect } from "#app/enums/status-effect"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Magic Bounce", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .ability(Abilities.BALL_FETCH) + .battleType("single") + .moveset( [ Moves.GROWL, Moves.SPLASH ]) + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.MAGIC_BOUNCE) + .enemyMoveset(Moves.SPLASH); + }); + + it("should reflect basic status moves", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should not bounce moves while the target is in the semi-invulnerable state", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.override.moveset([ Moves.GROWL ]); + game.override.enemyMoveset( [ Moves.FLY ]); + + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.FLY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0); + }); + + it("should individually bounce back multi-target moves", async () => { + game.override.battleType("double"); + game.override.moveset([ Moves.GROWL, Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]); + + game.move.select(Moves.GROWL, 0); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + + const user = game.scene.getPlayerField()[0]; + expect(user.getStatStage(Stat.ATK)).toBe(-2); + }); + + it("should still bounce back a move that would otherwise fail", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.scene.getEnemyPokemon()?.setStatStage(Stat.ATK, -6); + game.override.moveset([ Moves.GROWL ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should not bounce back a move that was just bounced", async () => { + game.override.ability(Abilities.MAGIC_BOUNCE); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should receive the stat change after reflecting a move back to a mirror armor user", async () => { + game.override.ability(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should not bounce back a move from a mold breaker user", async () => { + game.override.ability(Abilities.MOLD_BREAKER); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should bounce back a spread status move against both pokemon", async () => { + game.override.battleType("double"); + game.override.moveset([ Moves.GROWL, Moves.SPLASH ]); + game.override.enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]); + + game.move.select(Moves.GROWL, 0); + game.move.select(Moves.SPLASH, 1); + + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -2)).toBeTruthy(); + }); + + it("should only bounce spikes back once in doubles when both targets have magic bounce", async () => { + game.override.battleType("double"); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.override.moveset([ Moves.SPIKES ]); + + game.move.select(Moves.SPIKES); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1); + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)).toBeUndefined(); + }); + + it("should bounce spikes even when the target is protected", async () => { + game.override.moveset([ Moves.SPIKES ]); + game.override.enemyMoveset([ Moves.PROTECT ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.SPIKES); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1); + }); + + it("should not bounce spikes when the target is in the semi-invulnerable state", async () => { + game.override.moveset([ Moves.SPIKES ]); + game.override.enemyMoveset([ Moves.FLY ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.SPIKES); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)!["layers"]).toBe(1); + }); + + it("should not bounce back curse", async() => { + game.override.starterSpecies(Species.GASTLY); + await game.classicMode.startBattle([ Species.GASTLY ]); + game.override.moveset([ Moves.CURSE ]); + + game.move.select(Moves.CURSE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.getTag(BattlerTagType.CURSED)).toBeDefined(); + }); + + it("should not cause encore to be interrupted after bouncing", async () => { + game.override.moveset([ Moves.SPLASH, Moves.GROWL, Moves.ENCORE ]); + game.override.enemyMoveset([ Moves.TACKLE, Moves.GROWL ]); + // game.override.ability(Abilities.MOLD_BREAKER); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // Give the player MOLD_BREAKER for this turn to bypass Magic Bounce. + vi.spyOn(playerPokemon, "getAbility").mockReturnValue(allAbilities[Abilities.MOLD_BREAKER]); + + // turn 1 + game.move.select(Moves.ENCORE); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE); + + // turn 2 + vi.spyOn(playerPokemon, "getAbility").mockRestore(); + game.move.select(Moves.GROWL); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE); + expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.TACKLE); + + }); + + // TODO: encore is failing if the last move was virtual. + it.todo("should not cause the bounced move to count for encore", async () => { + game.override.moveset([ Moves.SPLASH, Moves.GROWL, Moves.ENCORE ]); + game.override.enemyMoveset([ Moves.GROWL, Moves.TACKLE ]); + game.override.enemyAbility(Abilities.MAGIC_BOUNCE); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // turn 1 + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + // Give the player MOLD_BREAKER for this turn to bypass Magic Bounce. + vi.spyOn(playerPokemon, "getAbility").mockReturnValue(allAbilities[Abilities.MOLD_BREAKER]); + + // turn 2 + game.move.select(Moves.ENCORE); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE); + expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.TACKLE); + }); + + // TODO: stomping tantrum should consider moves that were bounced. + it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => { + game.override.battleType("single"); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.override.moveset([ Moves.STOMPING_TANTRUM, Moves.CHARM ]); + + const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM]; + vi.spyOn(stomping_tantrum, "calculateBattlePower"); + + game.move.select(Moves.CHARM); + await game.toNextTurn(); + + game.move.select(Moves.STOMPING_TANTRUM); + await game.phaseInterceptor.to("BerryPhase"); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150); + }); + + // TODO: stomping tantrum should consider moves that were bounced. + it.todo("should properly cause the enemy's stomping tantrum to be doubled in power after bouncing and failing", async () => { + game.override.enemyMoveset([ Moves.STOMPING_TANTRUM, Moves.SPLASH, Moves.CHARM ]); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM]; + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(stomping_tantrum, "calculateBattlePower"); + + game.move.select(Moves.SPORE); + await game.forceEnemyMove(Moves.CHARM); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemy.getLastXMoves(1)[0].result).toBe("success"); + + await game.phaseInterceptor.to("BerryPhase"); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); + + await game.toNextTurn(); + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); + }); + + it("should respect immunities when bouncing a move", async () => { + vi.spyOn(allMoves[Moves.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100); + game.override.moveset([ Moves.THUNDER_WAVE, Moves.GROWL ]); + game.override.ability(Abilities.SOUNDPROOF); + await game.classicMode.startBattle([ Species.PHANPY ]); + + // Turn 1 - thunder wave immunity test + game.move.select(Moves.THUNDER_WAVE); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); + + // Turn 2 - soundproof immunity test + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0); + }); + + it("should bounce back a move before the accuracy check", async () => { + game.override.moveset([ Moves.SPORE ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const attacker = game.scene.getPlayerPokemon()!; + + vi.spyOn(attacker, "getAccuracyMultiplier").mockReturnValue(0.0); + game.move.select(Moves.SPORE); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.SLEEP); + }); + + it("should take the accuracy of the magic bounce user into account", async () => { + game.override.moveset([ Moves.SPORE ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + const opponent = game.scene.getEnemyPokemon()!; + + vi.spyOn(opponent, "getAccuracyMultiplier").mockReturnValue(0); + game.move.select(Moves.SPORE); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); + }); + + it("should always apply the leftmost available target's magic bounce when bouncing moves like sticky webs in doubles", async () => { + game.override.battleType("double"); + game.override.moveset([ Moves.STICKY_WEB, Moves.SPLASH, Moves.TRICK_ROOM ]); + + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]); + const [ enemy_1, enemy_2 ] = game.scene.getEnemyField(); + // set speed just incase logic erroneously checks for speed order + enemy_1.setStat(Stat.SPD, enemy_2.getStat(Stat.SPD) + 1); + + // turn 1 + game.move.select(Moves.STICKY_WEB, 0); + game.move.select(Moves.TRICK_ROOM, 1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)?.getSourcePokemon()?.getBattlerIndex()).toBe(BattlerIndex.ENEMY); + game.scene.arena.removeTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER, true); + + // turn 2 + game.move.select(Moves.STICKY_WEB, 0); + game.move.select(Moves.TRICK_ROOM, 1); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)?.getSourcePokemon()?.getBattlerIndex()).toBe(BattlerIndex.ENEMY); + }); + + it("should not bounce back status moves that hit through semi-invulnerable states", async () => { + game.override.moveset([ Moves.TOXIC, Moves.CHARM ]); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + game.move.select(Moves.TOXIC); + await game.forceEnemyMove(Moves.FLY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.TOXIC); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); + + game.override.ability(Abilities.NO_GUARD); + game.move.select(Moves.CHARM); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-2); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0); + }); +}); + diff --git a/src/test/moves/magic_coat.test.ts b/src/test/moves/magic_coat.test.ts new file mode 100644 index 00000000000..7371c89d4ac --- /dev/null +++ b/src/test/moves/magic_coat.test.ts @@ -0,0 +1,286 @@ +import { BattlerIndex } from "#app/battle"; +import { ArenaTagSide } from "#app/data/arena-tag"; +import { allMoves } from "#app/data/move"; +import { ArenaTagType } from "#app/enums/arena-tag-type"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { Stat } from "#app/enums/stat"; +import { StatusEffect } from "#app/enums/status-effect"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Magic Coat", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.MAGIC_COAT); + }); + + it("should fail if the user goes last in the turn", async () => { + game.override.moveset([ Moves.PROTECT ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.PROTECT); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getEnemyPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should fail if called again in the same turn due to moves like instruct", async () => { + game.override.moveset([ Moves.INSTRUCT ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.INSTRUCT); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getEnemyPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should not reflect moves used on the next turn", async () => { + game.override.moveset([ Moves.GROWL, Moves.SPLASH ]); + game.override.enemyMoveset([ Moves.MAGIC_COAT, Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + // turn 1 + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.MAGIC_COAT); + await game.toNextTurn(); + + // turn 2 + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should reflect basic status moves", async () => { + game.override.moveset([ Moves.GROWL ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should individually bounce back multi-target moves when used by both targets in doubles", async () => { + game.override.battleType("double"); + game.override.moveset([ Moves.GROWL, Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]); + + game.move.select(Moves.GROWL, 0); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + + const user = game.scene.getPlayerField()[0]; + expect(user.getStatStage(Stat.ATK)).toBe(-2); + }); + + it("should bounce back a spread status move against both pokemon", async () => { + game.override.battleType("double"); + game.override.moveset([ Moves.GROWL, Moves.SPLASH ]); + game.override.enemyMoveset([ Moves.SPLASH, Moves.MAGIC_COAT ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]); + + game.move.select(Moves.GROWL, 0); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH); + await game.forceEnemyMove(Moves.MAGIC_COAT); + + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -1)).toBeTruthy(); + }); + + it("should still bounce back a move that would otherwise fail", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.scene.getEnemyPokemon()?.setStatStage(Stat.ATK, -6); + game.override.moveset([ Moves.GROWL ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should not bounce back a move that was just bounced", async () => { + game.override.battleType("double"); + game.override.ability(Abilities.MAGIC_BOUNCE); + game.override.moveset([ Moves.GROWL, Moves.MAGIC_COAT ]); + game.override.enemyMoveset([ Moves.SPLASH, Moves.MAGIC_COAT ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]); + + game.move.select(Moves.MAGIC_COAT, 0); + game.move.select(Moves.GROWL, 1); + await game.forceEnemyMove(Moves.MAGIC_COAT); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyField()[0].getStatStage(Stat.ATK)).toBe(0); + }); + + // todo while Mirror Armor is not implemented + it.todo("should receive the stat change after reflecting a move back to a mirror armor user", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should still bounce back a move from a mold breaker user", async () => { + game.override.ability(Abilities.MOLD_BREAKER); + game.override.moveset([ Moves.GROWL ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(0); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should only bounce spikes back once when both targets use magic coat in doubles", async () => { + game.override.battleType("double"); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.override.moveset([ Moves.SPIKES ]); + + game.move.select(Moves.SPIKES); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1); + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)).toBeUndefined(); + }); + + it("should not bounce back curse", async() => { + game.override.starterSpecies(Species.GASTLY); + await game.classicMode.startBattle([ Species.GASTLY ]); + game.override.moveset([ Moves.CURSE ]); + + game.move.select(Moves.CURSE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.getTag(BattlerTagType.CURSED)).toBeDefined(); + }); + + // TODO: encore is failing if the last move was virtual. + it.todo("should not cause the bounced move to count for encore", async () => { + game.override.moveset([ Moves.GROWL, Moves.ENCORE ]); + game.override.enemyMoveset([ Moves.MAGIC_COAT, Moves.TACKLE ]); + game.override.enemyAbility(Abilities.MAGIC_BOUNCE); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // turn 1 + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.MAGIC_COAT); + await game.toNextTurn(); + + // turn 2 + game.move.select(Moves.ENCORE); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE); + expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.TACKLE); + }); + + // TODO: stomping tantrum should consider moves that were bounced. + it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => { + game.override.battleType("single"); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.override.moveset([ Moves.STOMPING_TANTRUM, Moves.CHARM ]); + + const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM]; + vi.spyOn(stomping_tantrum, "calculateBattlePower"); + + game.move.select(Moves.CHARM); + await game.toNextTurn(); + + game.move.select(Moves.STOMPING_TANTRUM); + await game.phaseInterceptor.to("BerryPhase"); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150); + }); + + // TODO: stomping tantrum should consider moves that were bounced. + it.todo("should properly cause the enemy's stomping tantrum to be doubled in power after bouncing and failing", async () => { + game.override.enemyMoveset([ Moves.STOMPING_TANTRUM, Moves.SPLASH, Moves.CHARM ]); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM]; + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(stomping_tantrum, "calculateBattlePower"); + + game.move.select(Moves.SPORE); + await game.forceEnemyMove(Moves.CHARM); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemy.getLastXMoves(1)[0].result).toBe("success"); + + await game.phaseInterceptor.to("BerryPhase"); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); + + await game.toNextTurn(); + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); + }); + + it("should respect immunities when bouncing a move", async () => { + vi.spyOn(allMoves[Moves.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100); + game.override.moveset([ Moves.THUNDER_WAVE, Moves.GROWL ]); + game.override.ability(Abilities.SOUNDPROOF); + await game.classicMode.startBattle([ Species.PHANPY ]); + + // Turn 1 - thunder wave immunity test + game.move.select(Moves.THUNDER_WAVE); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); + + // Turn 2 - soundproof immunity test + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0); + }); + + it("should bounce back a move before the accuracy check", async () => { + game.override.moveset([ Moves.SPORE ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const attacker = game.scene.getPlayerPokemon()!; + + vi.spyOn(attacker, "getAccuracyMultiplier").mockReturnValue(0.0); + game.move.select(Moves.SPORE); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.SLEEP); + }); + + it("should take the accuracy of the magic bounce user into account", async () => { + game.override.moveset([ Moves.SPORE ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + const opponent = game.scene.getEnemyPokemon()!; + + vi.spyOn(opponent, "getAccuracyMultiplier").mockReturnValue(0); + game.move.select(Moves.SPORE); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); + }); +}); From b31d5fd23e2f8b5263d797ad560eb8506c57dd4a Mon Sep 17 00:00:00 2001 From: geeilhan <107366005+geeilhan@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:26:01 +0100 Subject: [PATCH 18/18] [Move] Spectral Thief Full Implementation (#4891) * fully implemented spectral thief * Update to structure of implementation * line commented target.scene.queueMessage since message does not exist yet * changed documentation * added move-trigger.json key * removed line comment since key was added to english locales * removed console.log messages used for debugging * refactored move-trigger key to race with @muscode13 * added more automated tests * github tests failed * removed line comment since key was added to english locales * refactored move-trigger key to race with @muscode13 * added more automated tests * github tests failed * solved conflicts * Update src/data/move.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * removed .partial() * corrected spectral thief name * changed target.scene to globalScene * changed comments --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> --- src/data/move.ts | 67 +++++++- src/field/pokemon.ts | 41 ++++- src/test/moves/spectral_thief.test.ts | 224 ++++++++++++++++++++++++++ 3 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 src/test/moves/spectral_thief.test.ts diff --git a/src/data/move.ts b/src/data/move.ts index 75908f86a14..1c768f20bb0 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4380,6 +4380,69 @@ export class CueNextRoundAttr extends MoveEffectAttr { } } +/** + * Attribute that changes stat stages before the damage is calculated + */ +export class StatChangeBeforeDmgCalcAttr extends MoveAttr { + /** + * Applies Stat Changes before damage is calculated + * + * @param user {@linkcode Pokemon} that called {@linkcode move} + * @param target {@linkcode Pokemon} that is the target of {@linkcode move} + * @param move {@linkcode Move} called by {@linkcode user} + * @param args N/A + * + * @returns true if stat stages where correctly applied + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + return false; + } +} + +/** + * Steals the postitive Stat stages of the target before damage calculation so stat changes + * apply to damage calculation (e.g. {@linkcode Moves.SPECTRAL_THIEF}) + * {@link https://bulbapedia.bulbagarden.net/wiki/Spectral_Thief_(move) | Spectral Thief} + */ +export class SpectralThiefAttr extends StatChangeBeforeDmgCalcAttr { + /** + * steals max amount of positive stats of the target while not exceeding the limit of max 6 stat stages + * + * @param user {@linkcode Pokemon} that called {@linkcode move} + * @param target {@linkcode Pokemon} that is the target of {@linkcode move} + * @param move {@linkcode Move} called by {@linkcode user} + * @param args N/A + * + * @returns true if stat stages where correctly stolen + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + /** + * Copy all positive stat stages to user and reduce copied stat stages on target. + */ + for (const s of BATTLE_STATS) { + const statStageValueTarget = target.getStatStage(s); + const statStageValueUser = user.getStatStage(s); + + if (statStageValueTarget > 0) { + /** + * Only value of up to 6 can be stolen (stat stages don't exceed 6) + */ + const availableToSteal = Math.min(statStageValueTarget, 6 - statStageValueUser); + + globalScene.unshiftPhase(new StatStageChangePhase(user.getBattlerIndex(), this.selfTarget, [ s ], availableToSteal)); + target.setStatStage(s, statStageValueTarget - availableToSteal); + } + } + + target.updateInfo(); + user.updateInfo(); + globalScene.queueMessage(i18next.t("moveTriggers:stealPositiveStats", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })); + + return true; + } + +} + export class VariableAtkAttr extends MoveAttr { constructor() { super(); @@ -10467,8 +10530,8 @@ export function initMoves() { new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7) .attr(RechargeAttr), new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7) - .ignoresSubstitute() - .partial(), // Does not steal stats + .attr(SpectralThiefAttr) + .ignoresSubstitute(), new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7) .ignoresAbilities(), new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 79d7192b4db..82674fb8b46 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -7,7 +7,40 @@ import { variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; import type Move from "#app/data/move"; -import { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr, VariableMoveTypeChartAttr, HpSplitAttr } from "#app/data/move"; +import { + HighCritAttr, + StatChangeBeforeDmgCalcAttr, + HitsTagAttr, + applyMoveAttrs, + FixedDamageAttr, + VariableAtkAttr, + allMoves, + MoveCategory, + TypelessAttr, + CritOnlyAttr, + getMoveTargets, + OneHitKOAttr, + VariableMoveTypeAttr, + VariableDefAttr, + AttackMove, + ModifiedDamageAttr, + VariableMoveTypeMultiplierAttr, + IgnoreOpponentStatStagesAttr, + SacrificialAttr, + VariableMoveCategoryAttr, + CounterDamageAttr, + StatStageChangeAttr, + RechargeAttr, + IgnoreWeatherTypeDebuffAttr, + BypassBurnDamageReductionAttr, + SacrificialAttrOnHit, + OneHitKOAccuracyAttr, + RespectAttackTypeImmunityAttr, + MoveTarget, + CombinedPledgeStabBoostAttr, + VariableMoveTypeChartAttr, + HpSplitAttr +} from "#app/data/move"; import type { PokemonSpeciesForm } from "#app/data/pokemon-species"; import { default as PokemonSpecies, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; @@ -2903,6 +2936,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { isCritical = false; } + /** + * Applies stat changes from {@linkcode move} and gives it to {@linkcode source} + * before damage calculation + */ + applyMoveAttrs(StatChangeBeforeDmgCalcAttr, source, this, move); + const { cancelled, result, damage: dmg } = this.getAttackDamage(source, move, false, false, isCritical, false); const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === source.getMoveType(move)) as TypeBoostTag; diff --git a/src/test/moves/spectral_thief.test.ts b/src/test/moves/spectral_thief.test.ts new file mode 100644 index 00000000000..8913b7f3683 --- /dev/null +++ b/src/test/moves/spectral_thief.test.ts @@ -0,0 +1,224 @@ +import { Abilities } from "#enums/abilities"; +import { BattlerIndex } from "#app/battle"; +import { Stat } from "#enums/stat"; +import { allMoves } from "#app/data/move"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Spectral Thief", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .enemySpecies(Species.SHUCKLE) + .enemyLevel(100) + .enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.BALL_FETCH) + .moveset([ Moves.SPECTRAL_THIEF, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .disableCrits; + }); + + it("should steal max possible positive stat changes and ignore negative ones.", async () => { + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 6); + enemy.setStatStage(Stat.DEF, -6); + enemy.setStatStage(Stat.SPATK, 6); + enemy.setStatStage(Stat.SPDEF, -6); + enemy.setStatStage(Stat.SPD, 3); + + player.setStatStage(Stat.ATK, 4); + player.setStatStage(Stat.DEF, 1); + player.setStatStage(Stat.SPATK, 0); + player.setStatStage(Stat.SPDEF, 0); + player.setStatStage(Stat.SPD, -2); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + /** + * enemy has +6 ATK and player +4 => player only steals +2 + * enemy has -6 DEF and player 1 => player should not steal + * enemy has +6 SPATK and player 0 => player only steals +6 + * enemy has -6 SPDEF and player 0 => player should not steal + * enemy has +3 SPD and player -2 => player only steals +3 + */ + expect(player.getStatStages()).toEqual([ 6, 1, 6, 0, 1, 0, 0 ]); + expect(enemy.getStatStages()).toEqual([ 4, -6, 0, -6, 0, 0, 0 ]); + }); + + it("should steal stat stages before dmg calculation", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .enemyLevel(50); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + const moveToCheck = allMoves[Moves.SPECTRAL_THIEF]; + const dmgBefore = enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage; + + enemy.setStatStage(Stat.ATK, 6); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(dmgBefore).toBeLessThan(enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage); + }); + + it("should steal stat stages as a negative value with Contrary.", async () => { + game.override + .ability(Abilities.CONTRARY); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 6); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(-6); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + }); + + it("should steal double the stat stages with Simple.", async () => { + game.override + .ability(Abilities.SIMPLE); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 3); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(6); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + }); + + it("should steal the stat stages through Clear Body.", async () => { + game.override + .enemyAbility(Abilities.CLEAR_BODY); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 3); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(3); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + }); + + it("should steal the stat stages through White Smoke.", async () => { + game.override + .enemyAbility(Abilities.WHITE_SMOKE); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 3); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(3); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + }); + + it("should steal the stat stages through Hyper Cutter.", async () => { + game.override + .enemyAbility(Abilities.HYPER_CUTTER); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 3); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(3); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + }); + + it("should bypass Substitute.", async () => { + game.override + .enemyMoveset(Moves.SUBSTITUTE); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 3); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(3); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + expect(enemy.hp).toBeLessThan(enemy.getMaxHp() - 1); + }); + + it("should get blocked by protect.", async () => { + game.override + .enemyMoveset(Moves.PROTECT); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 3); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(0); + expect(enemy.getStatStage(Stat.ATK)).toEqual(3); + expect(enemy.hp).toBe(enemy.getMaxHp()); + }); +});