diff --git a/eslint.config.js b/eslint.config.js index 1cea5563a78..0da9cc604bf 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/public/images/events/valentines2025event-de.png b/public/images/events/valentines2025event-de.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-de.png differ diff --git a/public/images/events/valentines2025event-en.png b/public/images/events/valentines2025event-en.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-en.png differ diff --git a/public/images/events/valentines2025event-es-ES.png b/public/images/events/valentines2025event-es-ES.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-es-ES.png differ diff --git a/public/images/events/valentines2025event-fr.png b/public/images/events/valentines2025event-fr.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-fr.png differ diff --git a/public/images/events/valentines2025event-it.png b/public/images/events/valentines2025event-it.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-it.png differ diff --git a/public/images/events/valentines2025event-ja.png b/public/images/events/valentines2025event-ja.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-ja.png differ diff --git a/public/images/events/valentines2025event-ko.png b/public/images/events/valentines2025event-ko.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-ko.png differ diff --git a/public/images/events/valentines2025event-pt-BR.png b/public/images/events/valentines2025event-pt-BR.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-pt-BR.png differ diff --git a/public/images/events/valentines2025event-zh-CN.png b/public/images/events/valentines2025event-zh-CN.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-zh-CN.png differ 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 ddeb0807922..76d84810eaa 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4622,7 +4622,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; } @@ -10968,8 +10969,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/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 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 6b0c4dd3fca..b3c7b127738 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -980,11 +980,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); @@ -998,7 +1001,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: @@ -2520,9 +2523,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); @@ -2549,7 +2553,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; @@ -4395,8 +4401,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/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/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/evolution.test.ts b/src/test/evolution.test.ts index d198049801c..8dc19a548ca 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); 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/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 }]); 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); }); 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 { 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);