From 8169760e1ebdbe22a9e6106ed0f0f9f6776615c9 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Sat, 2 Nov 2024 00:21:45 -0400 Subject: [PATCH 01/98] [Bug] Prevent wild mons fleeing with U-turn, Flip Turn, Volt Switch (#4643) * Wild mons can't flee with U-turn * Update src/data/move.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/move.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/data/move.ts b/src/data/move.ts index c5b14304fb2..837602ca71a 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5750,6 +5750,11 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return false; } + // Don't allow wild mons to flee with U-turn et al + if (this.selfSwitch && !user.isPlayer() && move.category !== MoveCategory.STATUS) { + return false; + } + if (switchOutTarget.hp > 0) { switchOutTarget.leaveField(false); user.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); From c2d24d6e93baa4f1f56bf449a2aa4aa47ff02b48 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Sat, 2 Nov 2024 16:55:22 +0100 Subject: [PATCH 02/98] [Bug] Take weight into account when getting the tier of a modifier (#4775) * disable timed events in tests * Take weight into account when getting the tier of modifiers * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> --- src/battle-scene.ts | 2 +- .../encounters/clowning-around-encounter.ts | 4 +-- .../utils/encounter-phase-utils.ts | 2 +- src/modifier/modifier-type.ts | 35 +++++++++++++++---- src/test/utils/gameWrapper.ts | 2 ++ src/test/utils/mocks/mockTimedEventManager.ts | 17 +++++++++ 6 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 src/test/utils/mocks/mockTimedEventManager.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 3cbf4d7b422..3c561206abe 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -323,6 +323,7 @@ export default class BattleScene extends SceneBase { this.conditionalQueue = []; this.phaseQueuePrependSpliceIndex = -1; this.nextCommandPhaseQueue = []; + this.eventManager = new TimedEventManager(); this.updateGameInfo(); } @@ -378,7 +379,6 @@ export default class BattleScene extends SceneBase { this.fieldSpritePipeline = new FieldSpritePipeline(this.game); (this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("FieldSprite", this.fieldSpritePipeline); - this.eventManager = new TimedEventManager(); this.launchBattle(); } diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index c4b03660bde..0a88c5a699c 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -1,7 +1,7 @@ import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config"; import { ModifierTier } from "#app/modifier/modifier-tier"; -import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import BattleScene from "#app/battle-scene"; @@ -280,7 +280,7 @@ export const ClowningAroundEncounter: MysteryEncounter = let numRogue = 0; items.filter(m => m.isTransferable && !(m instanceof BerryModifier)) .forEach(m => { - const type = m.type.withTierFromPool(); + const type = m.type.withTierFromPool(ModifierPoolType.PLAYER, party); const tier = type.tier ?? ModifierTier.ULTRA; if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { numRogue += m.stackCount; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 5cd2fbffd5f..66459c96ede 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -418,7 +418,7 @@ export function generateModifierType(scene: BattleScene, modifier: () => Modifie // Populates item id and tier (order matters) result = result .withIdFromFunc(modifierTypes[modifierId]) - .withTierFromPool(); + .withTierFromPool(ModifierPoolType.PLAYER, scene.getParty()); return result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index e68e9a06fae..dfa46ce3667 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -19,7 +19,7 @@ import { Unlockables } from "#app/system/unlockables"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import { getModifierTierTextTint } from "#app/ui/text"; -import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; +import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -121,18 +121,41 @@ export class ModifierType { * Populates item tier for ModifierType instance * Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use) * To find the tier, this function performs a reverse lookup of the item type in modifier pools + * It checks the weight of the item and will use the first tier for which the weight is greater than 0 + * This is to allow items to be in multiple item pools depending on the conditions, for example for events + * If all tiers have a weight of 0 for the item, the first tier where the item was found is used * @param poolType Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from + * @param party optional. Needed to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc}) + * if not provided or empty, the weight check will be ignored + * @param rerollCount Default `0`. Used to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc}) */ - withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER): ModifierType { + withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER, party?: PlayerPokemon[], rerollCount: number = 0): ModifierType { + let defaultTier: undefined | ModifierTier; for (const tier of Object.values(getModifierPoolForType(poolType))) { for (const modifier of tier) { if (this.id === modifier.modifierType.id) { - this.tier = modifier.modifierType.tier; - return this; + let weight: number; + if (modifier.weight instanceof Function) { + weight = party ? modifier.weight(party, rerollCount) : 0; + } else { + weight = modifier.weight; + } + if (weight > 0) { + this.tier = modifier.modifierType.tier; + return this; + } else if (isNullOrUndefined(defaultTier)) { + // If weight is 0, keep track of the first tier where the item was found + defaultTier = modifier.modifierType.tier; + } } } } + // Didn't find a pool with weight > 0, fallback to first tier where the item was found, if any + if (defaultTier) { + this.tier = defaultTier; + } + return this; } @@ -2117,7 +2140,7 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo // Populates item id and tier guaranteedMod = guaranteedMod .withIdFromFunc(modifierTypes[modifierId]) - .withTierFromPool(); + .withTierFromPool(ModifierPoolType.PLAYER, party); const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod; if (modType) { @@ -2186,7 +2209,7 @@ export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[], } if (modifierType) { - options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool(); + options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool(ModifierPoolType.PLAYER, party); } } } diff --git a/src/test/utils/gameWrapper.ts b/src/test/utils/gameWrapper.ts index 48c0007118b..22517502a05 100644 --- a/src/test/utils/gameWrapper.ts +++ b/src/test/utils/gameWrapper.ts @@ -24,6 +24,7 @@ import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin; import EventEmitter = Phaser.Events.EventEmitter; import UpdateList = Phaser.GameObjects.UpdateList; import { version } from "../../../package.json"; +import { MockTimedEventManager } from "./mocks/mockTimedEventManager"; Object.defineProperty(window, "localStorage", { value: mockLocalStorage(), @@ -232,6 +233,7 @@ export default class GameWrapper { this.scene.make = new MockGameObjectCreator(mockTextureManager); this.scene.time = new MockClock(this.scene); this.scene.remove = vi.fn(); // TODO: this should be stubbed differently + this.scene.eventManager = new MockTimedEventManager(); // Disable Timed Events } } diff --git a/src/test/utils/mocks/mockTimedEventManager.ts b/src/test/utils/mocks/mockTimedEventManager.ts new file mode 100644 index 00000000000..b44729996a7 --- /dev/null +++ b/src/test/utils/mocks/mockTimedEventManager.ts @@ -0,0 +1,17 @@ +import { TimedEventManager } from "#app/timed-event-manager"; + +/** Mock TimedEventManager so that ongoing events don't impact tests */ +export class MockTimedEventManager extends TimedEventManager { + override activeEvent() { + return undefined; + } + override isEventActive(): boolean { + return false; + } + override getFriendshipMultiplier(): number { + return 1; + } + override getShinyMultiplier(): number { + return 1; + } +} From 80a8c659eeb8dddefbde615a694b472000a1ee73 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sat, 2 Nov 2024 09:47:08 -0700 Subject: [PATCH 03/98] [P2] Add "no switch-in" fail condition for Shed Tail and Baton Pass (#4777) --- src/data/move.ts | 10 ++++++++-- src/test/moves/shed_tail.test.ts | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 837602ca71a..e6695d64c48 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5839,7 +5839,6 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } } - export class ChillyReceptionAttr extends ForceSwitchOutAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { user.scene.arena.trySetWeather(WeatherType.SNOW, true); @@ -7063,6 +7062,11 @@ const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined; +const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => { + const party: Pokemon[] = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField()); +}; + export type MoveAttrFilter = (attr: MoveAttr) => boolean; function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise { @@ -7972,6 +7976,7 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new SelfStatusMove(Moves.BATON_PASS, Type.NORMAL, -1, 40, -1, 0, 2) .attr(ForceSwitchOutAttr, true, SwitchType.BATON_PASS) + .condition(failIfLastInPartyCondition) .hidesUser(), new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) @@ -10112,7 +10117,8 @@ export function initMoves() { .makesContact(), new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9) .attr(AddSubstituteAttr, 0.5) - .attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL), + .attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL) + .condition(failIfLastInPartyCondition), new SelfStatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9) .attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) })) .attr(ChillyReceptionAttr, true), diff --git a/src/test/moves/shed_tail.test.ts b/src/test/moves/shed_tail.test.ts index c4df6c574cb..4d761a8af24 100644 --- a/src/test/moves/shed_tail.test.ts +++ b/src/test/moves/shed_tail.test.ts @@ -1,4 +1,5 @@ import { SubstituteTag } from "#app/data/battler-tags"; +import { MoveResult } from "#app/field/pokemon"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -53,4 +54,18 @@ describe("Moves - Shed Tail", () => { expect(substituteTag).toBeDefined(); expect(substituteTag?.hp).toBe(Math.floor(magikarp.getMaxHp() / 4)); }); + + it("should fail if no ally is available to switch in", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const magikarp = game.scene.getPlayerPokemon()!; + expect(game.scene.getParty().length).toBe(1); + + game.move.select(Moves.SHED_TAIL); + + await game.phaseInterceptor.to("TurnEndPhase", false); + + expect(magikarp.isOnField()).toBeTruthy(); + expect(magikarp.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); }); From 1659f5726221d30df08f650938fd992ca4019645 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sat, 2 Nov 2024 09:48:10 -0700 Subject: [PATCH 04/98] [P2] Camouflage now considers Terrains first (#4761) * the fix... will maybe write a test later * ughh * made a test * moved aorund function * Update src/test/moves/camouflage.test.ts Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> * lalal --------- Co-authored-by: frutescens Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> --- src/data/move.ts | 98 ++++++++++++++++++++++++++++++- src/field/arena.ts | 60 ------------------- src/test/moves/camouflage.test.ts | 49 ++++++++++++++++ 3 files changed, 144 insertions(+), 63 deletions(-) create mode 100644 src/test/moves/camouflage.test.ts diff --git a/src/data/move.ts b/src/data/move.ts index e6695d64c48..cbafb4e66a3 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5925,15 +5925,107 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr { return false; } - const biomeType = user.scene.arena.getTypeForBiome(); + const terrainType = user.scene.arena.getTerrainType(); + let typeChange: Type; + if (terrainType !== TerrainType.NONE) { + typeChange = this.getTypeForTerrain(user.scene.arena.getTerrainType()); + } else { + typeChange = this.getTypeForBiome(user.scene.arena.biomeType); + } - user.summonData.types = [ biomeType ]; + user.summonData.types = [ typeChange ]; user.updateInfo(); - user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[biomeType]}`) })); + user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[typeChange]}`) })); return true; } + + /** + * Retrieves a type from the current terrain + * @param terrainType {@linkcode TerrainType} + * @returns {@linkcode Type} + */ + private getTypeForTerrain(terrainType: TerrainType): Type { + switch (terrainType) { + case TerrainType.ELECTRIC: + return Type.ELECTRIC; + case TerrainType.MISTY: + return Type.FAIRY; + case TerrainType.GRASSY: + return Type.GRASS; + case TerrainType.PSYCHIC: + return Type.PSYCHIC; + case TerrainType.NONE: + default: + return Type.UNKNOWN; + } + } + + /** + * Retrieves a type from the current biome + * @param biomeType {@linkcode Biome} + * @returns {@linkcode Type} + */ + private getTypeForBiome(biomeType: Biome): Type { + switch (biomeType) { + case Biome.TOWN: + case Biome.PLAINS: + case Biome.METROPOLIS: + return Type.NORMAL; + case Biome.GRASS: + case Biome.TALL_GRASS: + return Type.GRASS; + case Biome.FOREST: + case Biome.JUNGLE: + return Type.BUG; + case Biome.SLUM: + case Biome.SWAMP: + return Type.POISON; + case Biome.SEA: + case Biome.BEACH: + case Biome.LAKE: + case Biome.SEABED: + return Type.WATER; + case Biome.MOUNTAIN: + return Type.FLYING; + case Biome.BADLANDS: + return Type.GROUND; + case Biome.CAVE: + case Biome.DESERT: + return Type.ROCK; + case Biome.ICE_CAVE: + case Biome.SNOWY_FOREST: + return Type.ICE; + case Biome.MEADOW: + case Biome.FAIRY_CAVE: + case Biome.ISLAND: + return Type.FAIRY; + case Biome.POWER_PLANT: + return Type.ELECTRIC; + case Biome.VOLCANO: + return Type.FIRE; + case Biome.GRAVEYARD: + case Biome.TEMPLE: + return Type.GHOST; + case Biome.DOJO: + case Biome.CONSTRUCTION_SITE: + return Type.FIGHTING; + case Biome.FACTORY: + case Biome.LABORATORY: + return Type.STEEL; + case Biome.RUINS: + case Biome.SPACE: + return Type.PSYCHIC; + case Biome.WASTELAND: + case Biome.END: + return Type.DRAGON; + case Biome.ABYSS: + return Type.DARK; + default: + return Type.UNKNOWN; + } + } } export class ChangeTypeAttr extends MoveEffectAttr { diff --git a/src/field/arena.ts b/src/field/arena.ts index b053a3d056a..abc2b89569c 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -224,66 +224,6 @@ export class Arena { return 0; } - getTypeForBiome() { - switch (this.biomeType) { - case Biome.TOWN: - case Biome.PLAINS: - case Biome.METROPOLIS: - return Type.NORMAL; - case Biome.GRASS: - case Biome.TALL_GRASS: - return Type.GRASS; - case Biome.FOREST: - case Biome.JUNGLE: - return Type.BUG; - case Biome.SLUM: - case Biome.SWAMP: - return Type.POISON; - case Biome.SEA: - case Biome.BEACH: - case Biome.LAKE: - case Biome.SEABED: - return Type.WATER; - case Biome.MOUNTAIN: - return Type.FLYING; - case Biome.BADLANDS: - return Type.GROUND; - case Biome.CAVE: - case Biome.DESERT: - return Type.ROCK; - case Biome.ICE_CAVE: - case Biome.SNOWY_FOREST: - return Type.ICE; - case Biome.MEADOW: - case Biome.FAIRY_CAVE: - case Biome.ISLAND: - return Type.FAIRY; - case Biome.POWER_PLANT: - return Type.ELECTRIC; - case Biome.VOLCANO: - return Type.FIRE; - case Biome.GRAVEYARD: - case Biome.TEMPLE: - return Type.GHOST; - case Biome.DOJO: - case Biome.CONSTRUCTION_SITE: - return Type.FIGHTING; - case Biome.FACTORY: - case Biome.LABORATORY: - return Type.STEEL; - case Biome.RUINS: - case Biome.SPACE: - return Type.PSYCHIC; - case Biome.WASTELAND: - case Biome.END: - return Type.DRAGON; - case Biome.ABYSS: - return Type.DARK; - default: - return Type.UNKNOWN; - } - } - getBgTerrainColorRatioForBiome(): number { switch (this.biomeType) { case Biome.SPACE: diff --git a/src/test/moves/camouflage.test.ts b/src/test/moves/camouflage.test.ts new file mode 100644 index 00000000000..acf37635c47 --- /dev/null +++ b/src/test/moves/camouflage.test.ts @@ -0,0 +1,49 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { TerrainType } from "#app/data/terrain"; +import { Type } from "#app/data/type"; +import { BattlerIndex } from "#app/battle"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Camouflage", () => { + 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.CAMOUFLAGE ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.REGIELEKI) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.PSYCHIC_TERRAIN); + }); + + it("Camouflage should look at terrain first when selecting a type to change into", async () => { + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.CAMOUFLAGE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTerrainType()).toBe(TerrainType.PSYCHIC); + const pokemonType = playerPokemon.getTypes()[0]; + expect(pokemonType).toBe(Type.PSYCHIC); + }); +}); From 1474f8cf147f947f2d30ca92d8b0c727cf463199 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sat, 2 Nov 2024 10:05:33 -0700 Subject: [PATCH 05/98] [Refactor] Add `options` param interface for `MoveEffectAttr` (#4710) * Optional parameter interfaces for `MoveEffectAttr` and `StatStageChangeAttr` * Update docs + Diamond Storm typo * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Make move effect trigger specification optional --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/move.ts | 247 ++++++++++++++++++---------- src/test/moves/secret_power.test.ts | 21 ++- 2 files changed, 171 insertions(+), 97 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index cbafb4e66a3..a8a9b6ab2f7 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1049,31 +1049,80 @@ export enum MoveEffectTrigger { POST_TARGET, } +interface MoveEffectAttrOptions { + /** + * Defines when this effect should trigger in the move's effect order + * @see {@linkcode MoveEffectPhase} + */ + trigger?: MoveEffectTrigger; + /** Should this effect only apply on the first hit? */ + firstHitOnly?: boolean; + /** Should this effect only apply on the last hit? */ + lastHitOnly?: boolean; + /** Should this effect only apply on the first target hit? */ + firstTargetOnly?: boolean; + /** Overrides the secondary effect chance for this attr if set. */ + effectChanceOverride?: number; +} + /** Base class defining all Move Effect Attributes * @extends MoveAttr * @see {@linkcode apply} */ export class MoveEffectAttr extends MoveAttr { - /** Defines when this effect should trigger in the move's effect order - * @see {@linkcode phases.MoveEffectPhase.start} + /** + * A container for this attribute's optional parameters + * @see {@linkcode MoveEffectAttrOptions} for supported params. */ - public trigger: MoveEffectTrigger; - /** Should this effect only apply on the first hit? */ - public firstHitOnly: boolean; - /** Should this effect only apply on the last hit? */ - public lastHitOnly: boolean; - /** Should this effect only apply on the first target hit? */ - public firstTargetOnly: boolean; - /** Overrides the secondary effect chance for this attr if set. */ - public effectChanceOverride?: number; + protected options?: MoveEffectAttrOptions; - constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false, firstTargetOnly: boolean = false, effectChanceOverride?: number) { + constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) { super(selfTarget); - this.trigger = trigger ?? MoveEffectTrigger.POST_APPLY; - this.firstHitOnly = firstHitOnly; - this.lastHitOnly = lastHitOnly; - this.firstTargetOnly = firstTargetOnly; - this.effectChanceOverride = effectChanceOverride; + this.options = options; + } + + /** + * Defines when this effect should trigger in the move's effect order. + * @default MoveEffectTrigger.POST_APPLY + * @see {@linkcode MoveEffectTrigger} + */ + public get trigger () { + return this.options?.trigger ?? MoveEffectTrigger.POST_APPLY; + } + + /** + * `true` if this effect should only trigger on the first hit of + * multi-hit moves. + * @default false + */ + public get firstHitOnly () { + return this.options?.firstHitOnly ?? false; + } + + /** + * `true` if this effect should only trigger on the last hit of + * multi-hit moves. + * @default false + */ + public get lastHitOnly () { + return this.options?.lastHitOnly ?? false; + } + + /** + * `true` if this effect should apply only upon hitting a target + * for the first time when targeting multiple {@linkcode Pokemon}. + * @default false + */ + public get firstTargetOnly () { + return this.options?.firstTargetOnly ?? false; + } + + /** + * If defined, overrides the move's base chance for this + * secondary effect to trigger. + */ + public get effectChanceOverride () { + return this.options?.effectChanceOverride; } /** @@ -1398,7 +1447,7 @@ export class RecoilAttr extends MoveEffectAttr { private unblockable: boolean; constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) { - super(true, MoveEffectTrigger.POST_APPLY, false, true); + super(true, { lastHitOnly: true }); this.useHp = useHp; this.damageRatio = damageRatio; @@ -1456,7 +1505,7 @@ export class RecoilAttr extends MoveEffectAttr { **/ export class SacrificialAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.POST_TARGET); + super(true, { trigger: MoveEffectTrigger.POST_TARGET }); } /** @@ -1489,7 +1538,7 @@ export class SacrificialAttr extends MoveEffectAttr { **/ export class SacrificialAttrOnHit extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); } /** @@ -1528,7 +1577,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr { */ export class HalfSacrificialAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.POST_TARGET); + super(true, { trigger: MoveEffectTrigger.POST_TARGET }); } /** @@ -1932,7 +1981,7 @@ export class HitHealAttr extends MoveEffectAttr { private healStat: EffectiveStat | null; constructor(healRatio?: number | null, healStat?: EffectiveStat) { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); this.healRatio = healRatio ?? 0.5; this.healStat = healStat ?? null; @@ -2141,7 +2190,7 @@ export class StatusEffectAttr extends MoveEffectAttr { public overrideStatus: boolean = false; constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) { - super(selfTarget, MoveEffectTrigger.HIT); + super(selfTarget, { trigger: MoveEffectTrigger.HIT }); this.effect = effect; this.turnsRemaining = turnsRemaining; @@ -2214,7 +2263,7 @@ export class MultiStatusEffectAttr extends StatusEffectAttr { export class PsychoShiftEffectAttr extends MoveEffectAttr { constructor() { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -2251,7 +2300,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { private chance: number; constructor(chance: number) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.chance = chance; } @@ -2312,7 +2361,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { private berriesOnly: boolean; constructor(berriesOnly: boolean) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.berriesOnly = berriesOnly; } @@ -2386,7 +2435,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { export class EatBerryAttr extends MoveEffectAttr { protected chosenBerry: BerryModifier | undefined; constructor() { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); } /** * Causes the target to eat a berry. @@ -2489,7 +2538,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr { * @param ...effects - List of status effects to cure */ constructor(selfTarget: boolean, ...effects: StatusEffect[]) { - super(selfTarget, MoveEffectTrigger.POST_APPLY, false, true); + super(selfTarget, { lastHitOnly: true }); this.effects = effects; } @@ -2819,35 +2868,67 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { } } +/** + * Set of optional parameters that may be applied to stat stage changing effects + * @extends MoveEffectAttrOptions + * @see {@linkcode StatStageChangeAttr} + */ +interface StatStageChangeAttrOptions extends MoveEffectAttrOptions { + /** If defined, needs to be met in order for the stat change to apply */ + condition?: MoveConditionFunc, + /** `true` to display a message */ + showMessage?: boolean +} + /** * Attribute used for moves that change stat stages * * @param stats {@linkcode BattleStat} Array of stat(s) to change * @param stages How many stages to change the stat(s) by, [-6, 6] * @param selfTarget `true` if the move is self-targetting - * @param condition {@linkcode MoveConditionFunc} Optional condition to be checked in order to apply the changes - * @param showMessage `true` to display a message; default `true` - * @param firstHitOnly `true` if only the first hit of a multi hit move should cause a stat stage change; default `false` - * @param moveEffectTrigger {@linkcode MoveEffectTrigger} When the stat change should trigger; default {@linkcode MoveEffectTrigger.HIT} - * @param firstTargetOnly `true` if a move that hits multiple pokemon should only trigger the stat change if it hits at least one pokemon, rather than once per hit pokemon; default `false` - * @param lastHitOnly `true` if the effect should only apply after the last hit of a multi hit move; default `false` - * @param effectChanceOverride Will override the move's normal secondary effect chance if specified + * @param options {@linkcode StatStageChangeAttrOptions} Container for any optional parameters for this attribute. * * @extends MoveEffectAttr * @see {@linkcode apply} */ export class StatStageChangeAttr extends MoveEffectAttr { public stats: BattleStat[]; - public stages: integer; - private condition?: MoveConditionFunc | null; - private showMessage: boolean; + public stages: number; + /** + * Container for optional parameters to this attribute. + * @see {@linkcode StatStageChangeAttrOptions} for available optional params + */ + protected override options?: StatStageChangeAttrOptions; - constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false, lastHitOnly: boolean = false, effectChanceOverride?: number) { - super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly, effectChanceOverride); + constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, options?: StatStageChangeAttrOptions) { + super(selfTarget, options); this.stats = stats; this.stages = stages; - this.condition = condition; - this.showMessage = showMessage; + this.options = options; + } + + /** + * The condition required for the stat stage change to apply. + * Defaults to `null` (i.e. no condition required). + */ + private get condition () { + return this.options?.condition ?? null; + } + + /** + * `true` to display a message for the stat change. + * @default true + */ + private get showMessage () { + return this.options?.showMessage ?? true; + } + + /** + * Indicates when the stat change should trigger + * @default MoveEffectTrigger.HIT + */ + public override get trigger () { + return this.options?.trigger ?? MoveEffectTrigger.HIT; } /** @@ -2932,20 +3013,6 @@ export class SecretPowerAttr extends MoveEffectAttr { super(false); } - /** - * Used to determine if the move should apply a secondary effect based on Secret Power's 30% chance - * @returns `true` if the move's secondary effect should apply - */ - override canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { - this.effectChanceOverride = move.chance; - const moveChance = this.getMoveChance(user, target, move, this.selfTarget); - if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) { - return true; - } else { - return false; - } - } - /** * Used to apply the secondary effect to the target Pokemon * @returns `true` if a secondary effect is successfully applied @@ -2962,8 +3029,6 @@ export class SecretPowerAttr extends MoveEffectAttr { const biome = user.scene.arena.biomeType; secondaryEffect = this.determineBiomeEffect(biome); } - // effectChanceOverride used in the application of the actual secondary effect - secondaryEffect.effectChanceOverride = 100; return secondaryEffect.apply(user, target, move, []); } @@ -3139,7 +3204,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr { private messageCallback: ((user: Pokemon) => void) | undefined; constructor(stat: BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) { - super(stat, levels, true, null, true); + super(stat, levels, true); this.cutRatio = cutRatio; this.messageCallback = messageCallback; @@ -4889,7 +4954,7 @@ export class BypassRedirectAttr extends MoveAttr { export class FrenzyAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.HIT, false, true); + super(true, { trigger: MoveEffectTrigger.HIT, lastHitOnly: true }); } canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) { @@ -4962,7 +5027,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { private failOnOverlap: boolean; constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false, cancelOnFail: boolean = false) { - super(selfTarget, MoveEffectTrigger.POST_APPLY, false, lastHitOnly); + super(selfTarget, { lastHitOnly: lastHitOnly }); this.tagType = tagType; this.turnCountMin = turnCountMin; @@ -5397,7 +5462,7 @@ export class AddArenaTagAttr extends MoveEffectAttr { public selfSideTarget: boolean; constructor(tagType: ArenaTagType, turnCount?: integer | null, failOnOverlap: boolean = false, selfSideTarget: boolean = false) { - super(true, MoveEffectTrigger.POST_APPLY); + super(true); this.tagType = tagType; this.turnCount = turnCount!; // TODO: is the bang correct? @@ -5435,7 +5500,7 @@ export class RemoveArenaTagsAttr extends MoveEffectAttr { public selfSideTarget: boolean; constructor(tagTypes: ArenaTagType[], selfSideTarget: boolean) { - super(true, MoveEffectTrigger.POST_APPLY); + super(true); this.tagTypes = tagTypes; this.selfSideTarget = selfSideTarget; @@ -5501,7 +5566,7 @@ export class RemoveArenaTrapAttr extends MoveEffectAttr { private targetBothSides: boolean; constructor(targetBothSides: boolean = false) { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); this.targetBothSides = targetBothSides; } @@ -5537,7 +5602,7 @@ export class RemoveScreensAttr extends MoveEffectAttr { private targetBothSides: boolean; constructor(targetBothSides: boolean = false) { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); this.targetBothSides = targetBothSides; } @@ -5575,7 +5640,7 @@ export class SwapArenaTagsAttr extends MoveEffectAttr { constructor(SwapTags: ArenaTagType[]) { - super(true, MoveEffectTrigger.POST_APPLY); + super(true); this.SwapTags = SwapTags; } @@ -5701,7 +5766,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { private selfSwitch: boolean = false, private switchType: SwitchType = SwitchType.SWITCH ) { - super(false, MoveEffectTrigger.POST_APPLY, false, true); + super(false, { lastHitOnly: true }); } isBatonPass() { @@ -5856,7 +5921,7 @@ export class RemoveTypeAttr extends MoveEffectAttr { private messageCallback: ((user: Pokemon) => void) | undefined; constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) { - super(true, MoveEffectTrigger.POST_TARGET); + super(true, { trigger: MoveEffectTrigger.POST_TARGET }); this.removedType = removedType; this.messageCallback = messageCallback; @@ -6032,7 +6097,7 @@ export class ChangeTypeAttr extends MoveEffectAttr { private type: Type; constructor(type: Type) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.type = type; } @@ -6055,7 +6120,7 @@ export class AddTypeAttr extends MoveEffectAttr { private type: Type; constructor(type: Type) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.type = type; } @@ -6582,7 +6647,7 @@ export class AbilityChangeAttr extends MoveEffectAttr { public ability: Abilities; constructor(ability: Abilities, selfTarget?: boolean) { - super(selfTarget, MoveEffectTrigger.HIT); + super(selfTarget, { trigger: MoveEffectTrigger.HIT }); this.ability = ability; } @@ -6611,7 +6676,7 @@ export class AbilityCopyAttr extends MoveEffectAttr { public copyToPartner: boolean; constructor(copyToPartner: boolean = false) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.copyToPartner = copyToPartner; } @@ -6650,7 +6715,7 @@ export class AbilityGiveAttr extends MoveEffectAttr { public copyToPartner: boolean; constructor() { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -6962,7 +7027,7 @@ export class DiscourageFrequentUseAttr extends MoveAttr { export class MoneyAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.HIT, true); + super(true, { trigger: MoveEffectTrigger.HIT, firstHitOnly: true }); } apply(user: Pokemon, target: Pokemon, move: Move): boolean { @@ -6979,7 +7044,7 @@ export class MoneyAttr extends MoveEffectAttr { */ export class DestinyBondAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); } /** @@ -7029,7 +7094,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr { public effect: StatusEffect; constructor(effect: StatusEffect) { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); this.effect = effect; } @@ -9087,7 +9152,7 @@ export function initMoves() { // If any fielded pokémon is grass-type and grounded. return [ ...user.scene.getEnemyParty(), ...user.scene.getParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded()); }) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded()), + .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), @@ -9124,7 +9189,7 @@ export function initMoves() { .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY }) .attr(ForceSwitchOutAttr, true) .soundBased(), new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) @@ -9139,7 +9204,7 @@ export function initMoves() { .condition(failIfLastCondition), new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6) .target(MoveTarget.ALL) - .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)), + .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, { condition: (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag) }), new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6) .attr(TerrainChangeAttr, TerrainType.GRASSY) .target(MoveTarget.BOTH_SIDES), @@ -9171,7 +9236,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .soundBased(), new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6) - .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, undefined, undefined, undefined, undefined, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true }) .makesContact(false) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.STEAM_ERUPTION, Type.WATER, MoveCategory.SPECIAL, 110, 95, 5, 30, 0, 6) @@ -9197,7 +9262,7 @@ export function initMoves() { new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2), new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC) + .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), new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) .ignoresSubstitute() @@ -9208,7 +9273,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) .ignoresVirtual(), new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false))) + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) }) .ignoresSubstitute() .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))), @@ -9428,7 +9493,7 @@ export function initMoves() { 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) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false))) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) }) .ignoresSubstitute() .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))), @@ -9485,7 +9550,7 @@ export function initMoves() { .ballBombMove() .makesContact(false), new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7) - .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { firstTargetOnly: true }) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.DRAGON_HAMMER, Type.DRAGON, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 7), @@ -9599,7 +9664,7 @@ export function initMoves() { .makesContact(false) .ignoresVirtual(), new AttackMove(Moves.CLANGOROUS_SOULBLAZE, Type.DRAGON, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, undefined, undefined, undefined, undefined, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, { firstTargetOnly: true }) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES) .edgeCase() // I assume it needs clanging scales and Kommo-O @@ -9837,8 +9902,8 @@ export function initMoves() { .attr(ClearTerrainAttr) .condition((user, target, move) => !!user.scene.arena.terrain), new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8) - .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, null, true, false, MoveEffectTrigger.HIT, false, true) - .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true) + .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, { lastHitOnly: true }) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { lastHitOnly: true }) .attr(MultiHitAttr) .makesContact(false), new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8) @@ -9972,7 +10037,7 @@ export function initMoves() { new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8) .makesContact(false) .attr(HighCritAttr) - .attr(StatStageChangeAttr, [ Stat.DEF ], -1, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 50) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, false, { effectChanceOverride: 50 }) .attr(FlinchAttr), new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8) .attr(StatusEffectAttr, StatusEffect.BURN) @@ -10108,7 +10173,7 @@ export function initMoves() { .attr(TeraMoveCategoryAttr) .attr(TeraBlastTypeAttr) .attr(TeraBlastPowerAttr) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR)) + .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} */ new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9) .attr(ProtectAttr, BattlerTagType.SILK_TRAP) @@ -10192,7 +10257,7 @@ export function initMoves() { .attr(RemoveScreensAttr), new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) .attr(MoneyAttr) - .attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, { firstTargetOnly: true }) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.PSYBLADE, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && user.isGrounded() ? 1.5 : 1) @@ -10215,7 +10280,7 @@ export function initMoves() { .attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) })) .attr(ChillyReceptionAttr, true), new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true) .attr(RemoveArenaTrapAttr, true) .attr(RemoveAllSubstitutesAttr), new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9) diff --git a/src/test/moves/secret_power.test.ts b/src/test/moves/secret_power.test.ts index ff0b5ae8c24..09fe5faa50b 100644 --- a/src/test/moves/secret_power.test.ts +++ b/src/test/moves/secret_power.test.ts @@ -2,7 +2,7 @@ import { Abilities } from "#enums/abilities"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Stat } from "#enums/stat"; -import { allMoves, SecretPowerAttr } from "#app/data/move"; +import { allMoves } from "#app/data/move"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; @@ -11,6 +11,7 @@ import { StatusEffect } from "#enums/status-effect"; import { BattlerIndex } from "#app/battle"; import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagSide } from "#app/data/arena-tag"; +import { allAbilities, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability"; describe("Moves - Secret Power", () => { let phaserGame: Phaser.Game; @@ -60,30 +61,38 @@ describe("Moves - Secret Power", () => { expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1); }); - it("the 'rainbow' effect of fire+water pledge does not double the chance of secret power's secondary effect", + it("Secret Power's effect chance is doubled by Serene Grace, but not by the 'rainbow' effect from Fire/Water Pledge", async () => { game.override .moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.SECRET_POWER, Moves.SPLASH ]) + .ability(Abilities.SERENE_GRACE) .enemyMoveset([ Moves.SPLASH ]) .battleType("double"); await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]); - const secretPowerAttr = allMoves[Moves.SECRET_POWER].getAttrs(SecretPowerAttr)[0]; - vi.spyOn(secretPowerAttr, "getMoveChance"); + const sereneGraceAttr = allAbilities[Abilities.SERENE_GRACE].getAttrs(MoveEffectChanceMultiplierAbAttr)[0]; + vi.spyOn(sereneGraceAttr, "apply"); game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY); game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2); await game.phaseInterceptor.to("TurnEndPhase"); - expect(game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER)).toBeDefined(); + let rainbowEffect = game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER); + expect(rainbowEffect).toBeDefined(); + + rainbowEffect = rainbowEffect!; + vi.spyOn(rainbowEffect, "apply"); game.move.select(Moves.SECRET_POWER, 0, BattlerIndex.ENEMY); game.move.select(Moves.SPLASH, 1); await game.phaseInterceptor.to("BerryPhase", false); - expect(secretPowerAttr.getMoveChance).toHaveLastReturnedWith(30); + expect(sereneGraceAttr.apply).toHaveBeenCalledOnce(); + expect(sereneGraceAttr.apply).toHaveLastReturnedWith(true); + + expect(rainbowEffect.apply).toHaveBeenCalledTimes(0); } ); }); From c31657d95206f0cf4bb398337aae01adc0975543 Mon Sep 17 00:00:00 2001 From: Mason S <132116525+ElizaAlex@users.noreply.github.com> Date: Sat, 2 Nov 2024 22:55:15 -0400 Subject: [PATCH 06/98] [Refactor] Reduced nesting in MoveEffectPhase (#3541) * Re-structured MoveEffectPhase.run() to reduce nesting and improve readability. * Re-structured MoveEffectPhase.run() to reduce nesting and improve readability. * Added Substitute Implementation * Fix indentation, tsdocs, add `public`/etc * Prevent ENEMY_SIDE targeted moves from occurring twice in double battles * Add `override` to `start()` and `end()` * Minor indentation fix --------- Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- public/locales | 2 +- src/phases/move-effect-phase.ts | 398 ++++++++++++++++-------- src/test/abilities/serene_grace.test.ts | 4 +- src/test/abilities/sheer_force.test.ts | 14 +- src/test/abilities/shield_dust.test.ts | 4 +- src/test/moves/dynamax_cannon.test.ts | 10 +- 6 files changed, 287 insertions(+), 145 deletions(-) diff --git a/public/locales b/public/locales index 3cf6d553541..fc4a1effd51 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 3cf6d553541d79ba165387bc73fb06544d00f1f9 +Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 2b898f7d66b..8b4b462380c 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -1,20 +1,62 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr, TypeImmunityAbAttr } from "#app/data/ability"; +import BattleScene from "#app/battle-scene"; +import { + AddSecondStrikeAbAttr, + AlwaysHitAbAttr, + applyPostAttackAbAttrs, + applyPostDefendAbAttrs, + applyPreAttackAbAttrs, + IgnoreMoveEffectsAbAttr, + MaxMultiHitAbAttr, + PostAttackAbAttr, + PostDefendAbAttr, + TypeImmunityAbAttr, +} from "#app/data/ability"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { MoveAnim } from "#app/data/battle-anims"; -import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; -import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move"; +import { + BattlerTagLapseType, + DamageProtectedTag, + ProtectedTag, + SemiInvulnerableTag, + SubstituteTag, +} from "#app/data/battler-tags"; +import { + applyFilteredMoveAttrs, + applyMoveAttrs, + AttackMove, + FixedDamageAttr, + HitsTagAttr, + MissEffectAttr, + MoveAttr, + MoveCategory, + MoveEffectAttr, + MoveEffectTrigger, + MoveFlags, + MoveTarget, + MultiHitAttr, + NoEffectAttr, + OneHitKOAttr, + OverrideMoveEffectAttr, + ToxicAccuracyAttr, + VariableTargetAttr, +} from "#app/data/move"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; -import { Moves } from "#app/enums/moves"; -import Pokemon, { PokemonMove, MoveResult, HitResult } from "#app/field/pokemon"; -import { getPokemonNameWithAffix } from "#app/messages"; -import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectChanceModifier, ContactHeldItemTransferChanceModifier, HitHealModifier } from "#app/modifier/modifier"; -import i18next from "i18next"; -import * as Utils from "#app/utils"; -import { PokemonPhase } from "./pokemon-phase"; import { Type } from "#app/data/type"; +import Pokemon, { HitResult, MoveResult, PokemonMove } from "#app/field/pokemon"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { + ContactHeldItemTransferChanceModifier, + EnemyAttackStatusEffectChanceModifier, + FlinchChanceModifier, + HitHealModifier, + PokemonMultiHitModifier, +} from "#app/modifier/modifier"; +import { BooleanHolder, executeIf, NumberHolder } from "#app/utils"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import i18next from "i18next"; +import { PokemonPhase } from "./pokemon-phase"; export class MoveEffectPhase extends PokemonPhase { public move: PokemonMove; @@ -35,7 +77,7 @@ export class MoveEffectPhase extends PokemonPhase { this.targets = targets; } - start() { + public override start(): void { super.start(); /** The Pokemon using this phase's invoked move */ @@ -52,12 +94,12 @@ export class MoveEffectPhase extends PokemonPhase { * Does an effect from this move override other effects on this turn? * e.g. Charging moves (Fly, etc.) on their first turn of use. */ - const overridden = new Utils.BooleanHolder(false); + const overridden = new BooleanHolder(false); /** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */ const move = this.move.getMove(); // Assume single target for override - applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget() ?? null, move, overridden, this.move.virtual).then(() => { + applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => { // If other effects were overriden, stop this phase before they can be applied if (overridden.value) { return this.end(); @@ -71,14 +113,14 @@ export class MoveEffectPhase extends PokemonPhase { * effects of the move itself, Parental Bond, and Multi-Lens to do so. */ if (user.turnData.hitsLeft === -1) { - const hitCount = new Utils.IntegerHolder(1); + const hitCount = new NumberHolder(1); // Assume single target for multi hit - applyMoveAttrs(MultiHitAttr, user, this.getTarget() ?? null, move, hitCount); + applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount); // If Parental Bond is applicable, double the hit count - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new Utils.IntegerHolder(0)); + applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new NumberHolder(0)); // If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) { - this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0)); + this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new NumberHolder(0)); } // Set the user's relevant turnData fields to reflect the final hit count user.turnData.hitCount = hitCount.value; @@ -100,8 +142,9 @@ export class MoveEffectPhase extends PokemonPhase { const hasActiveTargets = targets.some(t => t.isActive(true)); /** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */ - const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) - && !targets[0].getTag(SemiInvulnerableTag); + const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) + && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) + && !targets[0].getTag(SemiInvulnerableTag); /** * If no targets are left for the move to hit (FAIL), or the invoked move is single-target @@ -111,7 +154,7 @@ export class MoveEffectPhase extends PokemonPhase { if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { this.stopMultiHit(); if (hasActiveTargets) { - this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget() ? getPokemonNameWithAffix(this.getTarget()!) : "" })); + this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" })); moveHistoryEntry.result = MoveResult.MISS; applyMoveAttrs(MissEffectAttr, user, null, move); } else { @@ -127,30 +170,40 @@ export class MoveEffectPhase extends PokemonPhase { const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; // Move animation only needs one target - new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => { + new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, 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; + } /** 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)? */ - const hasConditionalProtectApplied = new Utils.BooleanHolder(false); + const hasConditionalProtectApplied = new BooleanHolder(false); /** Does the applied conditional protection bypass Protect-ignoring effects? */ - const bypassIgnoreProtect = new Utils.BooleanHolder(false); + const bypassIgnoreProtect = new BooleanHolder(false); /** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */ if (!this.move.getMove().isAllyTarget()) { this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); } /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ - const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) - && (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) - || (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); + const isProtected = ( + bypassIgnoreProtect.value + || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) + && (hasConditionalProtectApplied.value + || (!target.findTags(t => t instanceof DamageProtectedTag).length + && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) + || (this.move.getMove().category !== MoveCategory.STATUS + && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); /** 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); + const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) + && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) + && !target.getTag(SemiInvulnerableTag); /** * If the move missed a target, stop all future hits against that target @@ -218,7 +271,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** Does this phase represent the invoked move's last strike? */ - const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); + const lastHit = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()); /** * If the user can change forms by using the invoked move, @@ -234,85 +287,48 @@ export class MoveEffectPhase extends PokemonPhase { * These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger * type requires different conditions to be met with respect to the move's hit result. */ - applyAttrs.push(new Promise(resolve => { - // Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move) - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT, - user, target, move).then(() => { - // All other effects require the move to not have failed or have been cancelled to trigger - if (hitResult !== HitResult.FAIL) { - /** - * If the invoked move's effects are meant to trigger during the move's "charge turn," - * ignore all effects after this point. - * Otherwise, apply all self-targeted POST_APPLY effects. - */ - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY - && attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move).then(() => { - // All effects past this point require the move to have hit the target - if (hitResult !== HitResult.NO_EFFECT) { - // Apply all non-self-targeted POST_APPLY effects - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY - && !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => { - /** - * If the move hit, and the target doesn't have Shield Dust, - * apply the chance to flinch the target gained from King's Rock - */ - if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) { - const flinched = new Utils.BooleanHolder(false); - user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); - if (flinched.value) { - target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); - } - } - // If the move was not protected against, apply all HIT effects - Utils.executeIf(!isProtected, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT - && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => { - // Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them) - return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { - // Only apply the following effects if the move was not deflected by a substitute - if (move.hitsSubstitute(user, target)) { - return resolve(); - } + const k = new Promise((resolve) => { + //Start promise chain and apply PRE_APPLY move attributes + let promiseChain: Promise = applyFilteredMoveAttrs((attr: MoveAttr) => + attr instanceof MoveEffectAttr + && attr.trigger === MoveEffectTrigger.PRE_APPLY + && (!attr.firstHitOnly || firstHit) + && (!attr.lastHitOnly || lastHit) + && hitResult !== HitResult.NO_EFFECT, user, target, move); - // If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens - if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { - user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); - } - target.lapseTags(BattlerTagLapseType.AFTER_HIT); + /** Don't complete if the move failed */ + if (hitResult === HitResult.FAIL) { + return resolve(); + } - })).then(() => { - // Apply the user's post-attack ability effects - applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { - /** - * If the invoked move is an attack, apply the user's chance to - * steal an item from the target granted by Grip Claw - */ - if (this.move.getMove() instanceof AttackMove) { - this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); - } - resolve(); - }); - }); - }) - ).then(() => resolve()); - }); - } else { - applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve()); - } - }); - } else { - resolve(); - } - }); - })); + /** Apply Move/Ability Effects in correct order */ + promiseChain = promiseChain + .then(this.applySelfTargetEffects(user, target, firstHit, lastHit)); + + if (hitResult !== HitResult.NO_EFFECT) { + promiseChain + .then(this.applyPostApplyEffects(user, target, firstHit, lastHit)) + .then(this.applyHeldItemFlinchCheck(user, target, dealsDamage)) + .then(this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget)) + .then(() => resolve()); + } else { + promiseChain + .then(() => applyMoveAttrs(NoEffectAttr, user, null, move)) + .then(resolve); + } + }); + + applyAttrs.push(k); } + // 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.getTarget()?.isActive()) ? + const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ? applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : null; - if (!!postTarget) { + if (postTarget) { if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after - applyAttrs[applyAttrs.length - 1]?.then(() => postTarget); + applyAttrs[applyAttrs.length - 1].then(() => postTarget); } else { // Otherwise, push a new asynchronous move effect applyAttrs.push(postTarget); } @@ -327,7 +343,7 @@ export class MoveEffectPhase extends PokemonPhase { */ targets.forEach(target => { const substitute = target.getTag(SubstituteTag); - if (!!substitute && substitute.hp <= 0) { + if (substitute && substitute.hp <= 0) { target.lapseTag(BattlerTagType.SUBSTITUTE); } }); @@ -337,7 +353,7 @@ export class MoveEffectPhase extends PokemonPhase { }); } - end() { + public override end(): void { const user = this.getUserPokemon(); /** * If this phase isn't for the invoked move's last strike, @@ -347,7 +363,7 @@ export class MoveEffectPhase extends PokemonPhase { * to the user. */ if (user) { - if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { + if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) { this.scene.unshiftPhase(this.getNewHitPhase()); } else { // Queue message for number of hits made by multi-move @@ -367,11 +383,135 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Resolves whether this phase's invoked move hits or misses the given target - * @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move - * @returns `true` if the move does not miss the target; `false` otherwise - */ - hitCheck(target: Pokemon): boolean { + * Apply self-targeted effects that trigger `POST_APPLY` + * + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param firstHit - `true` if this is the first hit in a multi-hit attack + * @param lastHit - `true` if this is the last hit in a multi-hit attack + * @returns a function intended to be passed into a `then()` call. + */ + protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise { + return () => applyFilteredMoveAttrs((attr: MoveAttr) => + attr instanceof MoveEffectAttr + && attr.trigger === MoveEffectTrigger.POST_APPLY + && attr.selfTarget + && (!attr.firstHitOnly || firstHit) + && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()); + } + + /** + * Applies non-self-targeted effects that trigger `POST_APPLY` + * (i.e. Smelling Salts curing Paralysis, and the forced switch from U-Turn, Dragon Tail, etc) + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param firstHit - `true` if this is the first hit in a multi-hit attack + * @param lastHit - `true` if this is the last hit in a multi-hit attack + * @returns a function intended to be passed into a `then()` call. + */ + protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise { + return () => applyFilteredMoveAttrs((attr: MoveAttr) => + attr instanceof MoveEffectAttr + && attr.trigger === MoveEffectTrigger.POST_APPLY + && !attr.selfTarget + && (!attr.firstHitOnly || firstHit) + && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()); + } + + /** + * Applies effects that trigger on HIT + * (i.e. Final Gambit, Power-Up Punch, Drain Punch) + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param firstHit - `true` if this is the first hit in a multi-hit attack + * @param lastHit - `true` if this is the last hit in a multi-hit attack + * @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move} + * @returns a function intended to be passed into a `then()` call. + */ + protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): Promise { + return applyFilteredMoveAttrs((attr: MoveAttr) => + attr instanceof MoveEffectAttr + && attr.trigger === MoveEffectTrigger.HIT + && (!attr.firstHitOnly || firstHit) + && (!attr.lastHitOnly || lastHit) + && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()); + } + + /** + * Applies reactive effects that occur when a Pokémon is hit. + * (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast) + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param hitResult - The {@linkcode HitResult} of the attempted move + * @returns a `Promise` intended to be passed into a `then()` call. + */ + protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): Promise { + return executeIf(!target.isFainted() || target.canApplyAbility(), () => + applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult) + .then(() => { + + if (!this.move.getMove().hitsSubstitute(user, target)) { + if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { + user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); + } + + target.lapseTags(BattlerTagLapseType.AFTER_HIT); + } + + }) + ); + } + + /** + * Applies all effects and attributes that require a move to connect with a target, + * namely reactive effects like Weak Armor, on-hit effects like that of Power-Up Punch, and item stealing effects + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param firstHit - `true` if this is the first hit in a multi-hit attack + * @param lastHit - `true` if this is the last hit in a multi-hit attack + * @param isProtected - `true` if the target is protected by effects such as Protect + * @param hitResult - The {@linkcode HitResult} of the attempted move + * @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move} + * @returns a function intended to be passed into a `then()` call. + */ + protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, isProtected : boolean, hitResult: HitResult, firstTarget: boolean) : () => Promise { + return () => executeIf(!isProtected, () => + this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget).then(() => + this.applyOnGetHitAbEffects(user, target, hitResult)).then(() => + applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult)).then(() => { // Item Stealing Effects + + if (this.move.getMove() instanceof AttackMove) { + this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); + } + }) + ); + } + + /** + * Handles checking for and applying Flinches + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param dealsDamage - `true` if the attempted move successfully dealt damage + * @returns a function intended to be passed into a `then()` call. + */ + protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void { + return () => { + if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) { + const flinched = new BooleanHolder(false); + user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); + if (flinched.value) { + target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); + } + } + }; + } + + /** + * Resolves whether this phase's invoked move hits the given target + * @param target - The {@linkcode Pokemon} targeted by the invoked move + * @returns `true` if the move hits the target + */ + public hitCheck(target: Pokemon): boolean { // Moves targeting the user and entry hazards can't miss if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) { return true; @@ -425,29 +565,29 @@ export class MoveEffectPhase extends PokemonPhase { return rand < (moveAccuracy * accuracyMultiplier); } - /** Returns the {@linkcode Pokemon} using this phase's invoked move */ - getUserPokemon(): Pokemon | undefined { + /** @returns The {@linkcode Pokemon} using this phase's invoked move */ + public getUserPokemon(): Pokemon | undefined { if (this.battlerIndex > BattlerIndex.ENEMY_2) { return this.scene.getPokemonById(this.battlerIndex) ?? undefined; } return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; } - /** Returns an array of all {@linkcode Pokemon} targeted by this phase's invoked move */ - getTargets(): Pokemon[] { + /** @returns An array of all {@linkcode Pokemon} targeted by this phase's invoked move */ + public getTargets(): Pokemon[] { return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1); } - /** Returns the first target of this phase's invoked move */ - getTarget(): Pokemon | undefined { + /** @returns The first target of this phase's invoked move */ + public getFirstTarget(): Pokemon | undefined { return this.getTargets()[0]; } /** * Removes the given {@linkcode Pokemon} from this phase's target list - * @param target {@linkcode Pokemon} the Pokemon to be removed + * @param target - The {@linkcode Pokemon} to be removed */ - removeTarget(target: Pokemon): void { + protected removeTarget(target: Pokemon): void { const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex()); if (targetIndex !== -1) { this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1); @@ -459,23 +599,25 @@ export class MoveEffectPhase extends PokemonPhase { * @param target {@linkcode Pokemon} if defined, only stop subsequent * strikes against this Pokemon */ - stopMultiHit(target?: Pokemon): void { - /** If given a specific target, remove the target from subsequent strikes */ + public stopMultiHit(target?: Pokemon): void { + // If given a specific target, remove the target from subsequent strikes if (target) { this.removeTarget(target); } - /** - * If no target specified, or the specified target was the last of this move's - * targets, completely cancel all subsequent strikes. - */ + const user = this.getUserPokemon(); + if (!user) { + return; + } + // If no target specified, or the specified target was the last of this move's + // targets, completely cancel all subsequent strikes. if (!target || this.targets.length === 0 ) { - this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here? - this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here? + user.turnData.hitCount = 1; + user.turnData.hitsLeft = 1; } } - /** Returns a new MoveEffectPhase with the same properties as this phase */ - getNewHitPhase() { + /** @returns A new `MoveEffectPhase` with the same properties as this phase */ + protected getNewHitPhase(): MoveEffectPhase { return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move); } } diff --git a/src/test/abilities/serene_grace.test.ts b/src/test/abilities/serene_grace.test.ts index 3155594c81d..ddca87496e9 100644 --- a/src/test/abilities/serene_grace.test.ts +++ b/src/test/abilities/serene_grace.test.ts @@ -57,7 +57,7 @@ describe("Abilities - Serene Grace", () => { const chance = new Utils.IntegerHolder(move.chance); console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); expect(chance.value).toBe(30); }, 20000); @@ -83,7 +83,7 @@ describe("Abilities - Serene Grace", () => { expect(move.id).toBe(Moves.AIR_SLASH); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); expect(chance.value).toBe(60); }, 20000); diff --git a/src/test/abilities/sheer_force.test.ts b/src/test/abilities/sheer_force.test.ts index a2600476d6d..63c81e9aafc 100644 --- a/src/test/abilities/sheer_force.test.ts +++ b/src/test/abilities/sheer_force.test.ts @@ -60,8 +60,8 @@ describe("Abilities - Sheer Force", () => { const power = new Utils.IntegerHolder(move.power); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); expect(chance.value).toBe(0); expect(power.value).toBe(move.power * 5461 / 4096); @@ -93,8 +93,8 @@ describe("Abilities - Sheer Force", () => { const power = new Utils.IntegerHolder(move.power); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); expect(chance.value).toBe(-1); expect(power.value).toBe(move.power); @@ -126,8 +126,8 @@ describe("Abilities - Sheer Force", () => { const power = new Utils.IntegerHolder(move.power); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); expect(chance.value).toBe(-1); expect(power.value).toBe(move.power); @@ -161,7 +161,7 @@ describe("Abilities - Sheer Force", () => { const power = new Utils.IntegerHolder(move.power); const chance = new Utils.IntegerHolder(move.chance); const user = phase.getUserPokemon()!; - const target = phase.getTarget()!; + const target = phase.getFirstTarget()!; const opponentType = target.getTypes()[0]; applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false); diff --git a/src/test/abilities/shield_dust.test.ts b/src/test/abilities/shield_dust.test.ts index 0f831fcf3fa..ccedf6873d7 100644 --- a/src/test/abilities/shield_dust.test.ts +++ b/src/test/abilities/shield_dust.test.ts @@ -57,8 +57,8 @@ describe("Abilities - Shield Dust", () => { expect(move.id).toBe(Moves.AIR_SLASH); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getTarget()!, phase.getUserPokemon()!, null, null, false, chance); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance); expect(chance.value).toBe(0); }, 20000); diff --git a/src/test/moves/dynamax_cannon.test.ts b/src/test/moves/dynamax_cannon.test.ts index 9dd48d3c94c..001f986bd52 100644 --- a/src/test/moves/dynamax_cannon.test.ts +++ b/src/test/moves/dynamax_cannon.test.ts @@ -81,7 +81,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120); }, 20000); @@ -98,7 +98,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140); }, 20000); @@ -115,7 +115,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160); }, 20000); @@ -132,7 +132,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180); }, 20000); @@ -149,7 +149,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200); }, 20000); From 70ddc29e86328f103d0f49f7c0af496ed7e0b774 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sat, 2 Nov 2024 20:11:24 -0700 Subject: [PATCH 07/98] [Refactor] Refactor data/pokemon-species.ts (#4054) * Setting species-variables to read only * set generations to protected * formIndex set to protected * quick fix * typedocs fixes * get / set generation * wait * Use get/set for formIndex too * Fix misplaced line * Replace a couple `!!` with default params --------- Co-authored-by: frutescens Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> --- src/data/move.ts | 2 +- src/data/pokemon-species.ts | 143 ++++++++++++++++++++---------------- 2 files changed, 82 insertions(+), 63 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index a8a9b6ab2f7..6ab569e88c1 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4465,7 +4465,7 @@ export class FormChangeItemTypeAttr extends VariableMoveTypeAttr { } if ([ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.ARCEUS) || [ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.SILVALLY)) { - const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; // TODO: is this bang correct? + const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; moveType.value = Type[Type[form]]; return true; diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index a93c35829ea..107fa5073af 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -47,7 +47,7 @@ export function getPokemonSpecies(species: Species | Species[] | undefined): Pok return allSpecies[species - 1]; } -export function getPokemonSpeciesForm(species: Species, formIndex: integer): PokemonSpeciesForm { +export function getPokemonSpeciesForm(species: Species, formIndex: number): PokemonSpeciesForm { const retSpecies: PokemonSpecies = species >= 2000 ? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct? : allSpecies[species - 1]; @@ -129,26 +129,27 @@ export type PokemonSpeciesFilter = (species: PokemonSpecies) => boolean; export abstract class PokemonSpeciesForm { public speciesId: Species; - public formIndex: integer; - public generation: integer; - public type1: Type; - public type2: Type | null; - public height: number; - public weight: number; - public ability1: Abilities; - public ability2: Abilities; - public abilityHidden: Abilities; - public baseTotal: integer; - public baseStats: integer[]; - public catchRate: integer; - public baseFriendship: integer; - public baseExp: integer; - public genderDiffs: boolean; - public isStarterSelectable: boolean; + protected _formIndex: number; + protected _generation: number; + readonly type1: Type; + readonly type2: Type | null; + readonly height: number; + readonly weight: number; + readonly ability1: Abilities; + readonly ability2: Abilities; + readonly abilityHidden: Abilities; + readonly baseTotal: number; + readonly baseStats: number[]; + readonly catchRate: number; + readonly baseFriendship: number; + readonly baseExp: number; + readonly genderDiffs: boolean; + readonly isStarterSelectable: boolean; constructor(type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, - baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, - catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs: boolean, isStarterSelectable: boolean) { + baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number, + catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean, isStarterSelectable: boolean + ) { this.type1 = type1; this.type2 = type2; this.height = height; @@ -180,7 +181,23 @@ export abstract class PokemonSpeciesForm { return ret; } - isOfType(type: integer): boolean { + get generation(): number { + return this._generation; + } + + set generation(generation: number) { + this._generation = generation; + } + + get formIndex(): number { + return this._formIndex; + } + + set formIndex(formIndex: number) { + this._formIndex = formIndex; + } + + isOfType(type: number): boolean { return this.type1 === type || (this.type2 !== null && this.type2 === type); } @@ -188,7 +205,7 @@ export abstract class PokemonSpeciesForm { * Method to get the total number of abilities a Pokemon species has. * @returns Number of abilities */ - getAbilityCount(): integer { + getAbilityCount(): number { return this.abilityHidden !== Abilities.NONE ? 3 : 2; } @@ -197,7 +214,7 @@ export abstract class PokemonSpeciesForm { * @param abilityIndex Which ability to get (should only be 0-2) * @returns The id of the Ability */ - getAbility(abilityIndex: integer): Abilities { + getAbility(abilityIndex: number): Abilities { let ret: Abilities; if (abilityIndex === 0) { ret = this.ability1; @@ -277,12 +294,12 @@ export abstract class PokemonSpeciesForm { return ret; } - getSpriteAtlasPath(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { + getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string { const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/"); return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`; } - getSpriteId(female: boolean, formIndex?: integer, shiny?: boolean, variant: integer = 0, back?: boolean): string { + getSpriteId(female: boolean, formIndex?: number, shiny?: boolean, variant: number = 0, back?: boolean): string { if (formIndex === undefined || this instanceof PokemonForm) { formIndex = this.formIndex; } @@ -299,11 +316,11 @@ export abstract class PokemonSpeciesForm { return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`; } - getSpriteKey(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { + getSpriteKey(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string { return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`; } - abstract getFormSpriteKey(formIndex?: integer): string; + abstract getFormSpriteKey(formIndex?: number): string; /** @@ -311,9 +328,9 @@ export abstract class PokemonSpeciesForm { * @param formIndex optional form index for pokemon with different forms * @returns species id if no additional forms, index with formkey if a pokemon with a form */ - getVariantDataIndex(formIndex?: integer) { + getVariantDataIndex(formIndex?: number) { let formkey: string | null = null; - let variantDataIndex: integer | string = this.speciesId; + let variantDataIndex: number | string = this.speciesId; const species = getPokemonSpecies(this.speciesId); if (species.forms.length > 0 && formIndex !== undefined) { formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex); @@ -324,13 +341,13 @@ export abstract class PokemonSpeciesForm { return variantDataIndex; } - getIconAtlasKey(formIndex?: integer, shiny?: boolean, variant?: integer): string { + getIconAtlasKey(formIndex?: number, shiny?: boolean, variant?: number): string { const variantDataIndex = this.getVariantDataIndex(formIndex); const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]); return `pokemon_icons_${this.generation}${isVariant ? "v" : ""}`; } - getIconId(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { + getIconId(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string { if (formIndex === undefined) { formIndex = this.formIndex; } @@ -379,7 +396,7 @@ export abstract class PokemonSpeciesForm { return ret; } - getCryKey(formIndex?: integer): string { + getCryKey(formIndex?: number): string { let speciesId = this.speciesId; if (this.speciesId > 2000) { switch (this.speciesId) { @@ -446,7 +463,7 @@ export abstract class PokemonSpeciesForm { return ret; } - validateStarterMoveset(moveset: StarterMoveset, eggMoves: integer): boolean { + validateStarterMoveset(moveset: StarterMoveset, eggMoves: number): boolean { const rootSpeciesId = this.getRootSpeciesId(); for (const moveId of moveset) { if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) { @@ -467,7 +484,7 @@ export abstract class PokemonSpeciesForm { return true; } - loadAssets(scene: BattleScene, female: boolean, formIndex?: integer, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise { + loadAssets(scene: BattleScene, female: boolean, formIndex?: number, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise { return new Promise(resolve => { const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant); scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant)); @@ -536,7 +553,7 @@ export abstract class PokemonSpeciesForm { return cry; } - generateCandyColors(scene: BattleScene): integer[][] { + generateCandyColors(scene: BattleScene): number[][] { const sourceTexture = scene.textures.get(this.getSpriteKey(false)); const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame]; @@ -544,7 +561,7 @@ export abstract class PokemonSpeciesForm { const canvas = document.createElement("canvas"); - const spriteColors: integer[][] = []; + const spriteColors: number[][] = []; const context = canvas.getContext("2d"); const frame = sourceFrame; @@ -567,7 +584,7 @@ export abstract class PokemonSpeciesForm { } for (let i = 0; i < pixelData.length; i += 4) { - const total = pixelData.slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0); + const total = pixelData.slice(i, i + 3).reduce((total: number, value: number) => total + value, 0); if (!total) { continue; } @@ -586,27 +603,28 @@ export abstract class PokemonSpeciesForm { Math.random = originalRandom; - return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as integer[]); + return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as number[]); } } export default class PokemonSpecies extends PokemonSpeciesForm implements Localizable { public name: string; - public subLegendary: boolean; - public legendary: boolean; - public mythical: boolean; - public species: string; - public growthRate: GrowthRate; - public malePercent: number | null; - public genderDiffs: boolean; - public canChangeForm: boolean; - public forms: PokemonForm[]; + readonly subLegendary: boolean; + readonly legendary: boolean; + readonly mythical: boolean; + readonly species: string; + readonly growthRate: GrowthRate; + readonly malePercent: number | null; + readonly genderDiffs: boolean; + readonly canChangeForm: boolean; + readonly forms: PokemonForm[]; - constructor(id: Species, generation: integer, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string, + constructor(id: Species, generation: number, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, - baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, - catchRate: integer, baseFriendship: integer, baseExp: integer, growthRate: GrowthRate, malePercent: number | null, - genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]) { + baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number, + catchRate: number, baseFriendship: number, baseExp: number, growthRate: GrowthRate, malePercent: number | null, + genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[] + ) { super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd, catchRate, baseFriendship, baseExp, genderDiffs, false); this.speciesId = id; @@ -631,7 +649,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali }); } - getName(formIndex?: integer): string { + getName(formIndex?: number): string { if (formIndex !== undefined && this.forms.length) { const form = this.forms[formIndex]; let key: string | null; @@ -662,11 +680,11 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali this.name = i18next.t(`pokemon:${Species[this.speciesId].toLowerCase()}`); } - getWildSpeciesForLevel(level: integer, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species { + getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species { return this.getSpeciesForLevel(level, allowEvolving, false, (isBoss ? PartyMemberStrength.WEAKER : PartyMemberStrength.AVERAGE) + (gameMode?.isEndless ? 1 : 0)); } - getTrainerSpeciesForLevel(level: integer, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species { + getTrainerSpeciesForLevel(level: number, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species { return this.getSpeciesForLevel(level, allowEvolving, true, strength, currentWave); } @@ -688,7 +706,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali * @param strength {@linkcode PartyMemberStrength} The strength of the party member in question * @returns {@linkcode integer} The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance. */ - private getStrengthLevelDiff(strength: PartyMemberStrength): integer { + private getStrengthLevelDiff(strength: PartyMemberStrength): number { switch (Math.min(strength, PartyMemberStrength.STRONGER)) { case PartyMemberStrength.WEAKEST: return 60; @@ -705,7 +723,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali } } - getSpeciesForLevel(level: integer, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species { + getSpeciesForLevel(level: number, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species { const prevolutionLevels = this.getPrevolutionLevels(); if (prevolutionLevels.length) { @@ -847,7 +865,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali } // This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon - getSimulatedEvolutionChain(currentLevel: integer, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] { + getSimulatedEvolutionChain(currentLevel: number, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] { const ret: EvolutionLevel[] = []; if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) { const prevolutionLevels = this.getPrevolutionLevels().reverse(); @@ -899,7 +917,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId); } - getFormSpriteKey(formIndex?: integer) { + getFormSpriteKey(formIndex?: number) { if (this.forms.length && (formIndex !== undefined && formIndex >= this.forms.length)) { console.warn(`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`); formIndex = Math.min(formIndex, this.forms.length - 1); @@ -919,16 +937,17 @@ export class PokemonForm extends PokemonSpeciesForm { private starterSelectableKeys: string[] = [ "10", "50", "10-pc", "50-pc", "red", "orange", "yellow", "green", "blue", "indigo", "violet" ]; constructor(formName: string, formKey: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, - baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, - catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs?: boolean, formSpriteKey?: string | null, isStarterSelectable?: boolean, ) { + baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number, + catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean = false, formSpriteKey: string | null = null, isStarterSelectable: boolean = false + ) { super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd, - catchRate, baseFriendship, baseExp, !!genderDiffs, (!!isStarterSelectable || !formKey)); + catchRate, baseFriendship, baseExp, genderDiffs, (isStarterSelectable || !formKey)); this.formName = formName; this.formKey = formKey; - this.formSpriteKey = formSpriteKey !== undefined ? formSpriteKey : null; + this.formSpriteKey = formSpriteKey; } - getFormSpriteKey(_formIndex?: integer) { + getFormSpriteKey(_formIndex?: number) { return this.formSpriteKey !== null ? this.formSpriteKey : this.formKey; } } From f16309fd069169e20a9e5a82cac1040e111397eb Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sat, 2 Nov 2024 22:22:40 -0700 Subject: [PATCH 08/98] [P2] Plus, Minus, Beads/Sword of Ruin not ignorable. Shield Dust and Armoa Veil ignorable (#4778) Co-authored-by: frutescens --- src/data/ability.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 3c73bb47c47..88ba14050b5 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -5062,7 +5062,8 @@ export function initAbilities() { .attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1) .ignorable(), new Ability(Abilities.SHIELD_DUST, 3) - .attr(IgnoreMoveEffectsAbAttr), + .attr(IgnoreMoveEffectsAbAttr) + .ignorable(), new Ability(Abilities.OWN_TEMPO, 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) .attr(IntimidateImmunityAbAttr) @@ -5180,11 +5181,9 @@ export function initAbilities() { new Ability(Abilities.CUTE_CHARM, 3) .attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED), new Ability(Abilities.PLUS, 3) - .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5) - .ignorable(), + .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5), new Ability(Abilities.MINUS, 3) - .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5) - .ignorable(), + .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5), new Ability(Abilities.FORECAST, 3) .attr(UncopiableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) @@ -5522,7 +5521,8 @@ export function initAbilities() { .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(MoveAbilityBypassAbAttr), new Ability(Abilities.AROMA_VEIL, 6) - .attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]), + .attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]) + .ignorable(), new Ability(Abilities.FLOWER_VEIL, 6) .ignorable() .unimplemented(), @@ -5973,16 +5973,14 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.SWORD_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75) - .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })) - .ignorable(), + .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })), new Ability(Abilities.TABLETS_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })) .ignorable(), new Ability(Abilities.BEADS_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75) - .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })) - .ignorable(), + .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })), new Ability(Abilities.ORICHALCUM_PULSE, 9) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY) From b17b1d6e7ea78349485ea34e2773c934774c8a06 Mon Sep 17 00:00:00 2001 From: podar <1999688+podarsmarty@users.noreply.github.com> Date: Sun, 3 Nov 2024 00:33:48 -0500 Subject: [PATCH 09/98] [P1] Fixing crash that can occur when failing to loading variant sprites. (#4315) --- src/field/pokemon.ts | 69 ++++++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 321532fffa7..eec5997989e 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -428,38 +428,26 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { resolve(); }; if (this.shiny) { - const populateVariantColors = (key: string, back: boolean = false): Promise => { + const populateVariantColors = (isBackSprite: boolean = false): Promise => { return new Promise(resolve => { - const battleSpritePath = this.getBattleSpriteAtlasPath(back, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, ""); + const battleSpritePath = this.getBattleSpriteAtlasPath(isBackSprite, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, ""); let config = variantData; - const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(back, ignoreOverride)); + const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(isBackSprite, ignoreOverride)); battleSpritePath.split("/").map(p => config ? config = config[p] : null); const variantSet: VariantSet = config as VariantSet; if (variantSet && variantSet[this.variant] === 1) { - if (variantColorCache.hasOwnProperty(key)) { - return resolve(); + const cacheKey = this.getBattleSpriteKey(isBackSprite); + if (!variantColorCache.hasOwnProperty(cacheKey)) { + this.populateVariantColorCache(cacheKey, useExpSprite, battleSpritePath); } - this.scene.cachedFetch(`./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`). - then(res => { - // Prevent the JSON from processing if it failed to load - if (!res.ok) { - console.error(`Could not load ${res.url}!`); - return; - } - return res.json(); - }).then(c => { - variantColorCache[key] = c; - resolve(); - }); - } else { - resolve(); } + resolve(); }); }; if (this.isPlayer()) { - Promise.all([ populateVariantColors(this.getBattleSpriteKey(false)), populateVariantColors(this.getBattleSpriteKey(true), true) ]).then(() => updateFusionPaletteAndResolve()); + Promise.all([ populateVariantColors(false), populateVariantColors(true) ]).then(() => updateFusionPaletteAndResolve()); } else { - populateVariantColors(this.getBattleSpriteKey(false)).then(() => updateFusionPaletteAndResolve()); + populateVariantColors(false).then(() => updateFusionPaletteAndResolve()); } } else { updateFusionPaletteAndResolve(); @@ -472,6 +460,45 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }); } + /** + * Gracefully handle errors loading a variant sprite. Log if it fails and attempt to fall back on + * non-experimental sprites before giving up. + * + * @param cacheKey the cache key for the variant color sprite + * @param attemptedSpritePath the sprite path that failed to load + * @param useExpSprite was the attempted sprite experimental + * @param battleSpritePath the filename of the sprite + * @param optionalParams any additional params to log + */ + fallbackVariantColor(cacheKey: string, attemptedSpritePath: string, useExpSprite: boolean, battleSpritePath: string, ...optionalParams: any[]) { + console.warn(`Could not load ${attemptedSpritePath}!`, ...optionalParams); + if (useExpSprite) { + this.populateVariantColorCache(cacheKey, false, battleSpritePath); + } + } + + /** + * Attempt to process variant sprite. + * + * @param cacheKey the cache key for the variant color sprite + * @param useExpSprite should the experimental sprite be used + * @param battleSpritePath the filename of the sprite + */ + populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) { + const spritePath = `./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`; + this.scene.cachedFetch(spritePath).then(res => { + // Prevent the JSON from processing if it failed to load + if (!res.ok) { + return this.fallbackVariantColor(cacheKey, res.url, useExpSprite, battleSpritePath, res.status, res.statusText); + } + return res.json(); + }).catch(error => { + this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error); + }).then(c => { + variantColorCache[cacheKey] = c; + }); + } + getFormKey(): string { if (!this.species.forms.length || this.species.forms.length <= this.formIndex) { return ""; From f0ae36de6c3e7a29f86f8f3e4be33efc2466194b Mon Sep 17 00:00:00 2001 From: muscode Date: Sun, 3 Nov 2024 01:04:20 -0500 Subject: [PATCH 10/98] [Ability] Implement Wimp Out and Emergency Exit (#4701) * implement Wimp Out/Emergency Exit * fixed test * fixed weather bug * Added nightmare interaction to Wimp Out following bug fix * refactored and added postdamageattr * bug fixes * added confusion test back (skipped) * updated applyPostDamageAbAttrs to applyPostDamage * fix external func name * fixed syntax inconsistency * updated PostDamageForceSwitchAttr -> PostDamageForceSwitchAbAttr * Modify `wimp_out.test.ts` * remove extra comment Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * remove extra comment Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update tsdocs Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * remove comment Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * remove comment Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * fix tsdocs Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * fix tsdocs Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * fix tsdocs Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * fix tsdocs Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * fix whitespace Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * make getFailedText public Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * make switchOutLogic public Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * make getSwitchOutCondition public Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * make getFailedText public Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * make applyPostDamage public Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * simplify if statement Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * add public override to applyPostDamage Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * fixed nested if issue, remove trapped tag removal * add fix for multi hit move * added multi-lens logic * moved applyPostDamageAbAttrs to pokemon.damage, added check for multi lens in pokemon.apply * added source to damageAndUpdate and applyPostDamageAbAttrs, added Parental Bond logic + test, put applyPostDamageAbAttrs back in damageAndUpdate * simplify multi hit check Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Minor formatting changes * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * moved and renamed shouldPreventSwitchOut, rewrote tests to account for U-turn changes, fix syntax error * Move comment slightly --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/data/ability.ts | 260 ++++++++- src/data/move.ts | 44 +- src/field/pokemon.ts | 19 +- src/phases/post-turn-status-effect-phase.ts | 3 +- src/test/abilities/wimp_out.test.ts | 614 ++++++++++++++++++++ 5 files changed, 921 insertions(+), 19 deletions(-) create mode 100644 src/test/abilities/wimp_out.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 88ba14050b5..6c503bb4100 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1,4 +1,4 @@ -import Pokemon, { HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; +import Pokemon, { EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; import { Type } from "./type"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; @@ -9,7 +9,7 @@ import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, g import { Gender } from "./gender"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; -import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; +import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { TerrainType } from "./terrain"; import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms"; import i18next from "i18next"; @@ -17,7 +17,7 @@ import { Localizable } from "#app/interfaces/locales"; import { Command } from "../ui/command-ui-handler"; import { BerryModifierType } from "#app/modifier/modifier-type"; import { getPokeballName } from "./pokeball"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex, BattleType } from "#app/battle"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -29,6 +29,12 @@ import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import BattleScene from "#app/battle-scene"; +import { SwitchType } from "#app/enums/switch-type"; +import { SwitchPhase } from "#app/phases/switch-phase"; +import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; +import { BattleEndPhase } from "#app/phases/battle-end-phase"; +import { NewBattlePhase } from "#app/phases/new-battle-phase"; +import { MoveEndPhase } from "#app/phases/move-end-phase"; export class Ability implements Localizable { public id: Abilities; @@ -3092,7 +3098,7 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr { /** * Condition function to applied to abilities related to Sheer Force. * Checks if last move used against target was affected by a Sheer Force user and: - * Disables: Color Change, Pickpocket, Wimp Out, Emergency Exit, Berserk, Anger Shell + * Disables: Color Change, Pickpocket, Berserk, Anger Shell * @returns {AbAttrCondition} If false disables the ability which the condition is applied to. */ function getSheerForceHitDisableAbCondition(): AbAttrCondition { @@ -4833,6 +4839,239 @@ async function applyAbAttrsInternal( } } +class ForceSwitchOutHelper { + constructor(private switchType: SwitchType) {} + + /** + * Handles the logic for switching out a Pokémon based on battle conditions, HP, and the switch type. + * + * @param pokemon The {@linkcode Pokemon} attempting to switch out. + * @returns `true` if the switch is successful + */ + public switchOutLogic(pokemon: Pokemon): boolean { + const switchOutTarget = pokemon; + /** + * If the switch-out target is a player-controlled Pokémon, the function checks: + * - Whether there are available party members to switch in. + * - If the Pokémon is still alive (hp > 0), and if so, it leaves the field and a new SwitchPhase is initiated. + */ + if (switchOutTarget instanceof PlayerPokemon) { + if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { + return false; + } + + if (switchOutTarget.hp > 0) { + switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); + pokemon.scene.prependToPhase(new SwitchPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase); + return true; + } + /** + * For non-wild battles, it checks if the opposing party has any available Pokémon to switch in. + * If yes, the Pokémon leaves the field and a new SwitchSummonPhase is initiated. + */ + } else if (pokemon.scene.currentBattle.battleType !== BattleType.WILD) { + if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { + return false; + } + if (switchOutTarget.hp > 0) { + switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); + pokemon.scene.prependToPhase(new SwitchSummonPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), + (pokemon.scene.currentBattle.trainer ? pokemon.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), + false, false), MoveEndPhase); + return true; + } + /** + * For wild Pokémon battles, the Pokémon will flee if the conditions are met (waveIndex and double battles). + */ + } else { + if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) { + return false; + } + + if (switchOutTarget.hp > 0) { + switchOutTarget.leaveField(false); + pokemon.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); + + if (switchOutTarget.scene.currentBattle.double) { + const allyPokemon = switchOutTarget.getAlly(); + switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon); + } + } + + if (!switchOutTarget.getAlly()?.isActive(true)) { + pokemon.scene.clearEnemyHeldItemModifiers(); + + if (switchOutTarget.hp) { + pokemon.scene.pushPhase(new BattleEndPhase(pokemon.scene)); + pokemon.scene.pushPhase(new NewBattlePhase(pokemon.scene)); + } + } + } + return false; + } + + /** + * Determines if a Pokémon can switch out based on its status, the opponent's status, and battle conditions. + * + * @param pokemon The Pokémon attempting to switch out. + * @param opponent The opponent Pokémon. + * @returns `true` if the switch-out condition is met + */ + public getSwitchOutCondition(pokemon: Pokemon, opponent: Pokemon): boolean { + const switchOutTarget = pokemon; + const player = switchOutTarget instanceof PlayerPokemon; + + if (player) { + const blockedByAbility = new Utils.BooleanHolder(false); + applyAbAttrs(ForceSwitchOutImmunityAbAttr, opponent, blockedByAbility); + return !blockedByAbility.value; + } + + if (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD) { + if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) { + return false; + } + } + + if (!player && pokemon.scene.currentBattle.isBattleMysteryEncounter() && !pokemon.scene.currentBattle.mysteryEncounter?.fleeAllowed) { + return false; + } + + const party = player ? pokemon.scene.getParty() : pokemon.scene.getEnemyParty(); + return (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD) + || party.filter(p => p.isAllowedInBattle() + && (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > pokemon.scene.currentBattle.getBattlerCount(); + } + + /** + * Returns a message if the switch-out attempt fails due to ability effects. + * + * @param target The target Pokémon. + * @returns The failure message, or `null` if no failure. + */ + public getFailedText(target: Pokemon): string | null { + const blockedByAbility = new Utils.BooleanHolder(false); + applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility); + return blockedByAbility.value ? i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }) : null; + } +} + +/** + * Calculates the amount of recovery from the Shell Bell item. + * + * If the Pokémon is holding a Shell Bell, this function computes the amount of health + * recovered based on the damage dealt in the current turn. The recovery is multiplied by the + * Shell Bell's modifier (if any). + * + * @param pokemon - The Pokémon whose Shell Bell recovery is being calculated. + * @returns The amount of health recovered by Shell Bell. + */ +function calculateShellBellRecovery(pokemon: Pokemon): number { + const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier); + if (shellBellModifier) { + return Utils.toDmgValue(pokemon.turnData.damageDealt / 8) * shellBellModifier.stackCount; + } + return 0; +} + +/** + * Triggers after the Pokemon takes any damage + * @extends AbAttr + */ +export class PostDamageAbAttr extends AbAttr { + public applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise { + return false; + } +} + +/** + * Ability attribute for forcing a Pokémon to switch out after its health drops below half. + * This attribute checks various conditions related to the damage received, the moves used by the Pokémon + * and its opponents, and determines whether a forced switch-out should occur. + * + * Used by Wimp Out and Emergency Exit + * + * @extends PostDamageAbAttr + * @see {@linkcode applyPostDamage} + */ +export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr { + private helper: ForceSwitchOutHelper = new ForceSwitchOutHelper(SwitchType.SWITCH); + private hpRatio: number; + + constructor(hpRatio: number = 0.5) { + super(); + this.hpRatio = hpRatio; + } + + /** + * Applies the switch-out logic after the Pokémon takes damage. + * Checks various conditions based on the moves used by the Pokémon, the opponents' moves, and + * the Pokémon's health after damage to determine whether the switch-out should occur. + * + * @param pokemon The Pokémon that took damage. + * @param damage The amount of damage taken by the Pokémon. + * @param passive N/A + * @param simulated Whether the ability is being simulated. + * @param args N/A + * @param source The Pokemon that dealt damage + * @returns `true` if the switch-out logic was successfully applied + */ + public override applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise { + const moveHistory = pokemon.getMoveHistory(); + // Will not activate when the Pokémon's HP is lowered by cutting its own HP + const fordbiddenAttackingMoves = [ Moves.BELLY_DRUM, Moves.SUBSTITUTE, Moves.CURSE, Moves.PAIN_SPLIT ]; + if (moveHistory.length > 0) { + const lastMoveUsed = moveHistory[moveHistory.length - 1]; + if (fordbiddenAttackingMoves.includes(lastMoveUsed.move)) { + return false; + } + } + + // Dragon Tail and Circle Throw switch out Pokémon before the Ability activates. + const fordbiddenDefendingMoves = [ Moves.DRAGON_TAIL, Moves.CIRCLE_THROW ]; + if (source) { + const enemyMoveHistory = source.getMoveHistory(); + if (enemyMoveHistory.length > 0) { + const enemyLastMoveUsed = enemyMoveHistory[enemyMoveHistory.length - 1]; + // Will not activate if the Pokémon's HP falls below half while it is in the air during Sky Drop. + if (fordbiddenDefendingMoves.includes(enemyLastMoveUsed.move) || enemyLastMoveUsed.move === Moves.SKY_DROP && enemyLastMoveUsed.result === MoveResult.OTHER) { + return false; + // Will not activate if the Pokémon's HP falls below half by a move affected by Sheer Force. + } else if (allMoves[enemyLastMoveUsed.move].chance >= 0 && source.hasAbility(Abilities.SHEER_FORCE)) { + return false; + // Activate only after the last hit of multistrike moves + } else if (source.turnData.hitsLeft > 1) { + return false; + } + if (source.turnData.hitCount > 1) { + damage = pokemon.turnData.damageTaken; + } + } + } + + if (pokemon.hp + damage >= pokemon.getMaxHp() * this.hpRatio) { + // Activates if it falls below half and recovers back above half from a Shell Bell + const shellBellHeal = calculateShellBellRecovery(pokemon); + if (pokemon.hp - shellBellHeal < pokemon.getMaxHp() * this.hpRatio) { + for (const opponent of pokemon.getOpponents()) { + if (!this.helper.getSwitchOutCondition(pokemon, opponent)) { + return false; + } + } + return this.helper.switchOutLogic(pokemon); + } else { + return false; + } + } else { + return false; + } + } + public getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { + return this.helper.getFailedText(target); + } +} + + export function applyAbAttrs(attrType: Constructor, pokemon: Pokemon, cancelled: Utils.BooleanHolder | null, simulated: boolean = false, ...args: any[]): Promise { return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), args, false, simulated); } @@ -4866,6 +5105,11 @@ export function applyPostSetStatusAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated); } +export function applyPostDamageAbAttrs(attrType: Constructor, + pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean = false, args: any[], source?: Pokemon): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostDamage(pokemon, damage, passive, simulated, args, source), args); +} + /** * Applies a field Stat multiplier attribute * @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being @@ -5607,11 +5851,11 @@ export function initAbilities() { new Ability(Abilities.STAMINA, 7) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1), new Ability(Abilities.WIMP_OUT, 7) - .condition(getSheerForceHitDisableAbCondition()) - .unimplemented(), + .attr(PostDamageForceSwitchAbAttr) + .edgeCase(), // Should not trigger when hurting itself in confusion new Ability(Abilities.EMERGENCY_EXIT, 7) - .condition(getSheerForceHitDisableAbCondition()) - .unimplemented(), + .attr(PostDamageForceSwitchAbAttr) + .edgeCase(), // Should not trigger when hurting itself in confusion new Ability(Abilities.WATER_COMPACTION, 7) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2), new Ability(Abilities.MERCILESS, 7) diff --git a/src/data/move.ts b/src/data/move.ts index 6ab569e88c1..db49f86021f 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; +import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; import { TerrainType } from "./terrain"; @@ -5761,6 +5761,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr { } } + export class ForceSwitchOutAttr extends MoveEffectAttr { constructor( private selfSwitch: boolean = false, @@ -5779,12 +5780,19 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return false; } - /** - * Move the switch out logic inside the conditional block - * This ensures that the switch out only happens when the conditions are met - */ const switchOutTarget = this.selfSwitch ? user : target; if (switchOutTarget instanceof PlayerPokemon) { + /** + * Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch + * If it did, the user of U-turn or Volt Switch will not be switched out. + */ + if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) && + (move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH || move.id === Moves.FLIP_TURN) + ) { + if (this.hpDroppedBelowHalf(target)) { + return false; + } + } // Switch out logic for the player's Pokemon if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { return false; @@ -5810,6 +5818,17 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { false, false), MoveEndPhase); } } else { + /** + * Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch + * If it did, the user of U-turn or Volt Switch will not be switched out. + */ + if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) && + (move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH) || move.id === Moves.FLIP_TURN) { + if (this.hpDroppedBelowHalf(target)) { + return false; + } + } + // Switch out logic for everything else (eg: WILD battles) if (user.scene.currentBattle.waveIndex % 10 === 0) { return false; @@ -5902,6 +5921,21 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } return ret; } + + /** + * Helper function to check if the Pokémon's health is below half after taking damage. + * Used for an edge case interaction with Wimp Out/Emergency Exit. + * If the Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out. + */ + hpDroppedBelowHalf(target: Pokemon): boolean { + const pokemonHealth = target.hp; + const maxPokemonHealth = target.getMaxHp(); + const damageTaken = target.turnData.damageTaken; + const initialHealth = pokemonHealth + damageTaken; + + // Check if the Pokémon's health has dropped below half after the damage + return initialHealth >= maxPokemonHealth / 2 && pokemonHealth < maxPokemonHealth / 2; + } } export class ChillyReceptionAttr extends ForceSwitchOutAttr { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index eec5997989e..c77aeed617b 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -12,7 +12,7 @@ import * as Utils from "#app/utils"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type"; import { getLevelTotalExp } from "#app/data/exp"; import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; -import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier } from "#app/modifier/modifier"; +import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier } from "#app/modifier/modifier"; import { PokeballType } from "#app/data/pokeball"; import { Gender } from "#app/data/gender"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; @@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags"; import { WeatherType } from "#app/data/weather"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; -import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr } from "#app/data/ability"; +import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -2818,7 +2818,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * We explicitly require to ignore the faint phase here, as we want to show the messages * about the critical hit and the super effective/not very effective messages before the faint phase. */ - const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true); + const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true, source); if (damage > 0) { if (source.isPlayer()) { @@ -2831,6 +2831,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { source.turnData.currDamageDealt = damage; this.turnData.damageTaken += damage; this.battleData.hitCount++; + + // Multi-Lens and Parental Bond check for Wimp Out/Emergency Exit + if (this.hasAbilityWithAttr(PostDamageForceSwitchAbAttr)) { + const multiHitModifier = source.getHeldItems().find(m => m instanceof PokemonMultiHitModifier); + if (multiHitModifier || source.hasAbilityWithAttr(AddSecondStrikeAbAttr)) { + applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source); + } + } + const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; this.turnData.attacksReceived.unshift(attackResult); if (source.isPlayer() && !this.isPlayer()) { @@ -2918,7 +2927,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.destroySubstitute(); this.resetSummonData(); } - return damage; } @@ -2932,12 +2940,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() * @returns integer of damage done */ - damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer { + damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): integer { const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical); this.scene.unshiftPhase(damagePhase); damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase); // Damage amount may have changed, but needed to be queued before calling damage function damagePhase.updateAmount(damage); + applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source); return damage; } diff --git a/src/phases/post-turn-status-effect-phase.ts b/src/phases/post-turn-status-effect-phase.ts index 2efd992a2b5..08e4d7cb952 100644 --- a/src/phases/post-turn-status-effect-phase.ts +++ b/src/phases/post-turn-status-effect-phase.ts @@ -1,6 +1,6 @@ import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { applyAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability"; +import { applyAbAttrs, applyPostDamageAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, PostDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability"; import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims"; import { getStatusEffectActivationText } from "#app/data/status-effect"; import { BattleSpec } from "#app/enums/battle-spec"; @@ -41,6 +41,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); pokemon.updateInfo(); + applyPostDamageAbAttrs(PostDamageAbAttr, pokemon, damage.value, pokemon.hasPassive(), false, []); } new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end()); } else { diff --git a/src/test/abilities/wimp_out.test.ts b/src/test/abilities/wimp_out.test.ts new file mode 100644 index 00000000000..ee943befc16 --- /dev/null +++ b/src/test/abilities/wimp_out.test.ts @@ -0,0 +1,614 @@ +import { BattlerIndex } from "#app/battle"; +import { ArenaTagSide } from "#app/data/arena-tag"; +import { allMoves } from "#app/data/move"; +import GameManager from "#app/test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Wimp Out", () => { + 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") + .ability(Abilities.WIMP_OUT) + .enemySpecies(Species.NINJASK) + .enemyPassiveAbility(Abilities.NO_GUARD) + .startingLevel(90) + .enemyLevel(70) + .moveset([ Moves.SPLASH, Moves.FALSE_SWIPE, Moves.ENDURE ]) + .enemyMoveset(Moves.FALSE_SWIPE) + .disableCrits(); + }); + + function confirmSwitch(): void { + const [ pokemon1, pokemon2 ] = game.scene.getParty(); + + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + + expect(pokemon1.species.speciesId).not.toBe(Species.WIMPOD); + + expect(pokemon2.species.speciesId).toBe(Species.WIMPOD); + expect(pokemon2.isFainted()).toBe(false); + expect(pokemon2.getHpRatio()).toBeLessThan(0.5); + } + + function confirmNoSwitch(): void { + const [ pokemon1, pokemon2 ] = game.scene.getParty(); + + expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); + + expect(pokemon2.species.speciesId).not.toBe(Species.WIMPOD); + + expect(pokemon1.species.speciesId).toBe(Species.WIMPOD); + expect(pokemon1.isFainted()).toBe(false); + expect(pokemon1.getHpRatio()).toBeLessThan(0.5); + } + + it("triggers regenerator passive single time when switching out with wimp out", async () => { + game.override + .passiveAbility(Abilities.REGENERATOR) + .startingLevel(5) + .enemyLevel(100); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + const wimpod = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(wimpod.hp).toEqual(Math.floor(wimpod.getMaxHp() * 0.33 + 1)); + confirmSwitch(); + }); + + it("It makes wild pokemon flee if triggered", async () => { + game.override.enemyAbility(Abilities.WIMP_OUT); + await game.classicMode.startBattle([ + Species.GOLISOPOD, + Species.TYRUNT + ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + enemyPokemon.hp *= 0.52; + + game.move.select(Moves.FALSE_SWIPE); + await game.phaseInterceptor.to("BerryPhase"); + + const isVisible = enemyPokemon.visible; + const hasFled = enemyPokemon.switchOutStatus; + expect(!isVisible && hasFled).toBe(true); + }); + + it("Does not trigger when HP already below half", async () => { + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + const wimpod = game.scene.getPlayerPokemon()!; + wimpod.hp = 5; + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(wimpod.hp).toEqual(1); + confirmNoSwitch(); + }); + + it("Trapping moves do not prevent Wimp Out from activating.", async () => { + game.override + .enemyMoveset([ Moves.SPIRIT_SHACKLE ]) + .startingLevel(53) + .enemyLevel(45); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()!.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); + expect(game.scene.getParty()[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined(); + confirmSwitch(); + }); + + it("If this Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.", async () => { + game.override + .startingLevel(95) + .enemyMoveset([ Moves.U_TURN ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const hasFled = enemyPokemon.switchOutStatus; + expect(hasFled).toBe(false); + confirmSwitch(); + }); + + it("If this Ability does not activate due to being hit by U-turn or Volt Switch, the user of that move will be switched out.", async () => { + game.override + .startingLevel(190) + .startingWave(8) + .enemyMoveset([ Moves.U_TURN ]); + await game.classicMode.startBattle([ + Species.GOLISOPOD, + Species.TYRUNT + ]); + const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id; + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1); + }); + + it("Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.", async () => { + game.override + .startingLevel(69) + .enemyMoveset([ Moves.DRAGON_TAIL ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + const wimpod = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("SwitchSummonPhase", false); + + expect(wimpod.summonData.abilitiesApplied).not.toContain(Abilities.WIMP_OUT); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD); + }); + + it("triggers when recoil damage is taken", async () => { + game.override + .moveset([ Moves.HEAD_SMASH ]) + .enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.HEAD_SMASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmSwitch(); + }); + + it("It does not activate when the Pokémon cuts its own HP", async () => { + game.override + .moveset([ Moves.SUBSTITUTE ]) + .enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + const wimpod = game.scene.getPlayerPokemon()!; + wimpod.hp *= 0.52; + + game.move.select(Moves.SUBSTITUTE); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmNoSwitch(); + }); + + it("Does not trigger when neutralized", async () => { + game.override + .enemyAbility(Abilities.NEUTRALIZING_GAS) + .startingLevel(5); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmNoSwitch(); + }); + + it("If it falls below half and recovers back above half from a Shell Bell, Wimp Out will activate even after the Shell Bell recovery", async () => { + game.override + .moveset([ Moves.DOUBLE_EDGE ]) + .enemyMoveset([ Moves.SPLASH ]) + .startingHeldItems([ + { name: "SHELL_BELL", count: 3 }, + { name: "HEALING_CHARM", count: 5 }, + ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.75; + + game.move.select(Moves.DOUBLE_EDGE); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getParty()[1].getHpRatio()).toBeGreaterThan(0.5); + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT); + }); + + it("Wimp Out will activate due to weather damage", async () => { + game.override + .weather(WeatherType.HAIL) + .enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmSwitch(); + }); + + it("Does not trigger when enemy has sheer force", async () => { + game.override + .enemyAbility(Abilities.SHEER_FORCE) + .enemyMoveset(Moves.SLUDGE_BOMB) + .startingLevel(95); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmNoSwitch(); + }); + + it("Wimp Out will activate due to post turn status damage", async () => { + game.override + .statusEffect(StatusEffect.POISON) + .enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to bad dreams", async () => { + game.override + .statusEffect(StatusEffect.SLEEP) + .enemyAbility(Abilities.BAD_DREAMS); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.52; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to leech seed", async () => { + game.override + .enemyMoveset([ Moves.LEECH_SEED ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.52; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to curse damage", async () => { + game.override + .enemySpecies(Species.DUSKNOIR) + .enemyMoveset([ Moves.CURSE ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.52; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to salt cure damage", async () => { + game.override + .enemySpecies(Species.NACLI) + .enemyMoveset([ Moves.SALT_CURE ]) + .enemyLevel(1); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.70; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to damaging trap damage", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .enemyMoveset([ Moves.WHIRLPOOL ]) + .enemyLevel(1); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.55; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Magic Guard passive should not allow indirect damage to trigger Wimp Out", async () => { + game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY); + game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); + game.override + .passiveAbility(Abilities.MAGIC_GUARD) + .enemyMoveset([ Moves.LEECH_SEED ]) + .weather(WeatherType.HAIL) + .statusEffect(StatusEffect.POISON); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getParty()[0].getHpRatio()).toEqual(0.51); + expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD); + }); + + it("Wimp Out activating should not cancel a double battle", async () => { + game.override + .battleType("double") + .enemyAbility(Abilities.WIMP_OUT) + .enemyMoveset([ Moves.SPLASH ]) + .enemyLevel(1); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + const enemyLeadPokemon = game.scene.getEnemyParty()[0]; + const enemySecPokemon = game.scene.getEnemyParty()[1]; + + game.move.select(Moves.FALSE_SWIPE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + + await game.phaseInterceptor.to("BerryPhase"); + + const isVisibleLead = enemyLeadPokemon.visible; + const hasFledLead = enemyLeadPokemon.switchOutStatus; + const isVisibleSec = enemySecPokemon.visible; + const hasFledSec = enemySecPokemon.switchOutStatus; + expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true); + expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp()); + expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp()); + }); + + it("Wimp Out will activate due to aftermath", async () => { + game.override + .moveset([ Moves.THUNDER_PUNCH ]) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.AFTERMATH) + .enemyMoveset([ Moves.SPLASH ]) + .enemyLevel(1); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.THUNDER_PUNCH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmSwitch(); + }); + + it("Activates due to entry hazards", async () => { + game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY); + game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); + game.override + .enemySpecies(Species.CENTISKORCH) + .enemyAbility(Abilities.WIMP_OUT) + .startingWave(4); + await game.classicMode.startBattle([ + Species.TYRUNT + ]); + + expect(game.phaseInterceptor.log).not.toContain("MovePhase"); + expect(game.phaseInterceptor.log).toContain("BattleEndPhase"); + }); + + it("Wimp Out will activate due to Nightmare", async () => { + game.override + .enemyMoveset([ Moves.NIGHTMARE ]) + .statusEffect(StatusEffect.SLEEP); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.65; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("triggers status on the wimp out user before a new pokemon is switched in", async () => { + game.override + .enemyMoveset(Moves.SLUDGE_BOMB) + .startingLevel(80); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + vi.spyOn(allMoves[Moves.SLUDGE_BOMB], "chance", "get").mockReturnValue(100); + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getParty()[1].status?.effect).toEqual(StatusEffect.POISON); + confirmSwitch(); + }); + + it("triggers after last hit of multi hit move", async () => { + game.override + .enemyMoveset(Moves.BULLET_SEED) + .enemyAbility(Abilities.SKILL_LINK); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.turnData.hitsLeft).toBe(0); + expect(enemyPokemon.turnData.hitCount).toBe(5); + confirmSwitch(); + }); + + it("triggers after last hit of multi hit move (multi lens)", async () => { + game.override + .enemyMoveset(Moves.TACKLE) + .enemyHeldItems([{ name: "MULTI_LENS", count: 1 }]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.turnData.hitsLeft).toBe(0); + expect(enemyPokemon.turnData.hitCount).toBe(2); + confirmSwitch(); + }); + it("triggers after last hit of Parental Bond", async () => { + game.override + .enemyMoveset(Moves.TACKLE) + .enemyAbility(Abilities.PARENTAL_BOND); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.turnData.hitsLeft).toBe(0); + expect(enemyPokemon.turnData.hitCount).toBe(2); + confirmSwitch(); + }); + + // TODO: This interaction is not implemented yet + it.todo("Wimp Out will not activate if the Pokémon's HP falls below half due to hurting itself in confusion", async () => { + game.override + .moveset([ Moves.SWORDS_DANCE ]) + .enemyMoveset([ Moves.SWAGGER ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.hp *= 0.51; + playerPokemon.setStatStage(Stat.ATK, 6); + playerPokemon.addTag(BattlerTagType.CONFUSED); + + // TODO: add helper function to force confusion self-hits + + while (playerPokemon.getHpRatio() > 0.49) { + game.move.select(Moves.SWORDS_DANCE); + await game.phaseInterceptor.to("TurnEndPhase"); + } + + confirmNoSwitch(); + }); +}); From 5e2dfa975a5f7729ae4fab0340ee4ea67e883fd8 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 3 Nov 2024 14:09:28 -0800 Subject: [PATCH 11/98] Rename `PokemonTurnData`'s `damageDealt` and `currDamageDealt` (#4784) --- src/data/ability.ts | 2 +- src/data/move.ts | 6 +++--- src/field/pokemon.ts | 9 ++++----- src/modifier/modifier.ts | 4 ++-- src/test/moves/focus_punch.test.ts | 4 ++-- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 6c503bb4100..87f9e89e534 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4969,7 +4969,7 @@ class ForceSwitchOutHelper { function calculateShellBellRecovery(pokemon: Pokemon): number { const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier); if (shellBellModifier) { - return Utils.toDmgValue(pokemon.turnData.damageDealt / 8) * shellBellModifier.stackCount; + return Utils.toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellModifier.stackCount; } return 0; } diff --git a/src/data/move.ts b/src/data/move.ts index db49f86021f..cd206755238 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1474,8 +1474,8 @@ export class RecoilAttr extends MoveEffectAttr { return false; } - const damageValue = (!this.useHp ? user.turnData.damageDealt : user.getMaxHp()) * this.damageRatio; - const minValue = user.turnData.damageDealt ? 1 : 0; + const damageValue = (!this.useHp ? user.turnData.totalDamageDealt : user.getMaxHp()) * this.damageRatio; + const minValue = user.turnData.totalDamageDealt ? 1 : 0; const recoilDamage = Utils.toDmgValue(damageValue, minValue); if (!recoilDamage) { return false; @@ -2006,7 +2006,7 @@ export class HitHealAttr extends MoveEffectAttr { message = i18next.t("battle:drainMessage", { pokemonName: getPokemonNameWithAffix(target) }); } else { // Default healing formula used by draining moves like Absorb, Draining Kiss, Bitter Blade, etc. - healAmount = Utils.toDmgValue(user.turnData.currDamageDealt * this.healRatio); + healAmount = Utils.toDmgValue(user.turnData.singleHitDamageDealt * this.healRatio); message = i18next.t("battle:regainHealth", { pokemonName: getPokemonNameWithAffix(user) }); } if (reverseDrain) { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index c77aeed617b..6c328fa25cc 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2827,8 +2827,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.gameData.gameStats.highestDamage = damage; } } - source.turnData.damageDealt += damage; - source.turnData.currDamageDealt = damage; + source.turnData.totalDamageDealt += damage; + source.turnData.singleHitDamageDealt = damage; this.turnData.damageTaken += damage; this.battleData.hitCount++; @@ -5130,7 +5130,6 @@ export class PokemonSummonData { public tags: BattlerTag[] = []; public abilitySuppressed: boolean = false; public abilitiesApplied: Abilities[] = []; - public speciesForm: PokemonSpeciesForm | null; public fusionSpeciesForm: PokemonSpeciesForm; public ability: Abilities = Abilities.NONE; @@ -5170,8 +5169,8 @@ export class PokemonTurnData { * - `0` = Move is finished */ public hitsLeft: number = -1; - public damageDealt: number = 0; - public currDamageDealt: number = 0; + public totalDamageDealt: number = 0; + public singleHitDamageDealt: number = 0; public damageTaken: number = 0; public attacksReceived: AttackMoveResult[] = []; public order: number; diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 36f94b99b20..d34804ba040 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1767,10 +1767,10 @@ export class HitHealModifier extends PokemonHeldItemModifier { * @returns `true` if the {@linkcode Pokemon} was healed */ override apply(pokemon: Pokemon): boolean { - if (pokemon.turnData.damageDealt && !pokemon.isFullHp()) { + if (pokemon.turnData.totalDamageDealt && !pokemon.isFullHp()) { const scene = pokemon.scene; scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(), - toDmgValue(pokemon.turnData.damageDealt / 8) * this.stackCount, i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true)); + toDmgValue(pokemon.turnData.totalDamageDealt / 8) * this.stackCount, i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true)); } return true; diff --git a/src/test/moves/focus_punch.test.ts b/src/test/moves/focus_punch.test.ts index 386eb2537ee..352e3b60aa4 100644 --- a/src/test/moves/focus_punch.test.ts +++ b/src/test/moves/focus_punch.test.ts @@ -59,7 +59,7 @@ describe("Moves - Focus Punch", () => { expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); expect(leadPokemon.getMoveHistory().length).toBe(1); - expect(leadPokemon.turnData.damageDealt).toBe(enemyStartingHp - enemyPokemon.hp); + expect(leadPokemon.turnData.totalDamageDealt).toBe(enemyStartingHp - enemyPokemon.hp); } ); @@ -86,7 +86,7 @@ describe("Moves - Focus Punch", () => { expect(enemyPokemon.hp).toBe(enemyStartingHp); expect(leadPokemon.getMoveHistory().length).toBe(1); - expect(leadPokemon.turnData.damageDealt).toBe(0); + expect(leadPokemon.turnData.totalDamageDealt).toBe(0); } ); From 87b7122b601fcfe40b82401e6ee33bda05bffb1e Mon Sep 17 00:00:00 2001 From: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> Date: Sun, 3 Nov 2024 21:47:07 -0500 Subject: [PATCH 12/98] [P2] Illuminate ignores evasion (#4781) --- src/data/ability.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 87f9e89e534..5be87fc2397 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1956,6 +1956,10 @@ export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr { } } +/** + * Ability attribute for ignoring the opponent's stat changes + * @param stats the stats that should be ignored + */ export class IgnoreOpponentStatStagesAbAttr extends AbAttr { private stats: readonly BattleStat[]; @@ -1965,6 +1969,15 @@ export class IgnoreOpponentStatStagesAbAttr extends AbAttr { this.stats = stats ?? BATTLE_STATS; } + /** + * Modifies a BooleanHolder and returns the result to see if a stat is ignored or not + * @param _pokemon n/a + * @param _passive n/a + * @param simulated n/a + * @param _cancelled n/a + * @param args A BooleanHolder that represents whether or not to ignore a stat's stat changes + * @returns true if the stat is ignored, false otherwise + */ apply(_pokemon: Pokemon, _passive: boolean, simulated: boolean, _cancelled: Utils.BooleanHolder, args: any[]) { if (this.stats.includes(args[0])) { (args[1] as Utils.BooleanHolder).value = true; @@ -5361,6 +5374,7 @@ export function initAbilities() { new Ability(Abilities.ILLUMINATE, 3) .attr(ProtectStatAbAttr, Stat.ACC) .attr(DoubleBattleChanceAbAttr) + .attr(IgnoreOpponentStatStagesAbAttr, [ Stat.EVA ]) .ignorable(), new Ability(Abilities.TRACE, 3) .attr(PostSummonCopyAbilityAbAttr) @@ -5580,7 +5594,7 @@ export function initAbilities() { new Ability(Abilities.FOREWARN, 4) .attr(ForewarnAbAttr), new Ability(Abilities.UNAWARE, 4) - .attr(IgnoreOpponentStatStagesAbAttr) + .attr(IgnoreOpponentStatStagesAbAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA ]) .ignorable(), new Ability(Abilities.TINTED_LENS, 4) .attr(DamageBoostAbAttr, 2, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) <= 0.5), From af473ba4ff3228f66f9c5cd154acac471e6c9804 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 3 Nov 2024 18:53:52 -0800 Subject: [PATCH 13/98] [Refactor] Clean up various methods in `battle-scene.ts` and `pokemon.ts` (#4412) * Update `battle-scene.ts` and `data/field/pokemon.ts` `battle-scene.ts` changes: - `getParty()` renamed to `getPlayerParty()` for clarity - `getNonSwitchedXPokemon()` consolidated into `getXPokemon()` - Some tsdocs were added/updated for `getXParty()`, `getXField()` and `getXPokemon()`; and those functions were explicitly marked as `public` - Helper function `getPokemonAllowedInBattle()` added `pokemon.ts` changes: - `isAllowed()` renamed to `isAllowedInChallenge()` for clarity - A redundant check for an active scene is removed in `isActive()` - Some tsdocs were added/updated for `isFainted()`, `isAllowedInChallenge()`, `isAllowedInBattle()` and `isActive()`; and those functions were explicitly marked as `public` - `isFainted()` now checks for `hp <= 0` instead of `!hp` Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * Backport eslint change to reduce merge conflicts * Fix merge issues --------- Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> --- src/battle-scene.ts | 78 ++++--- src/data/ability.ts | 4 +- src/data/balance/pokemon-evolutions.ts | 4 +- src/data/battle-anims.ts | 4 +- src/data/move.ts | 22 +- .../encounters/absolute-avarice-encounter.ts | 6 +- .../encounters/berries-abound-encounter.ts | 6 +- .../encounters/bug-type-superfan-encounter.ts | 2 +- .../encounters/clowning-around-encounter.ts | 4 +- .../encounters/dancing-lessons-encounter.ts | 42 ++-- .../encounters/delibirdy-encounter.ts | 30 +-- .../encounters/field-trip-encounter.ts | 2 +- .../encounters/fiery-fallout-encounter.ts | 4 +- .../encounters/fight-or-flight-encounter.ts | 6 +- .../encounters/fun-and-games-encounter.ts | 2 +- .../global-trade-system-encounter.ts | 14 +- .../encounters/lost-at-sea-encounter.ts | 2 +- .../encounters/mysterious-chest-encounter.ts | 24 +-- .../shady-vitamin-dealer-encounter.ts | 2 +- .../teleporting-hijinks-encounter.ts | 4 +- .../the-expert-pokemon-breeder-encounter.ts | 8 +- .../encounters/the-strong-stuff-encounter.ts | 2 +- .../the-winstrate-challenge-encounter.ts | 2 +- .../encounters/training-session-encounter.ts | 6 +- .../encounters/trash-to-treasure-encounter.ts | 2 +- .../encounters/uncommon-breed-encounter.ts | 2 +- .../encounters/weird-dream-encounter.ts | 12 +- .../mystery-encounter-option.ts | 10 +- .../mystery-encounter-requirements.ts | 54 ++--- .../mystery-encounters/mystery-encounter.ts | 14 +- .../can-learn-move-requirement.ts | 2 +- .../utils/encounter-phase-utils.ts | 14 +- .../utils/encounter-pokemon-utils.ts | 40 ++-- src/field/pokemon.ts | 52 +++-- src/modifier/modifier.ts | 12 +- src/phases/attempt-capture-phase.ts | 23 +- src/phases/battle-end-phase.ts | 4 +- src/phases/check-switch-phase.ts | 2 +- src/phases/encounter-phase.ts | 68 +++--- src/phases/evolution-phase.ts | 2 +- src/phases/faint-phase.ts | 16 +- src/phases/game-over-phase.ts | 10 +- src/phases/mystery-encounter-phases.ts | 50 ++--- src/phases/new-biome-encounter-phase.ts | 4 +- src/phases/next-encounter-phase.ts | 2 +- src/phases/party-heal-phase.ts | 2 +- src/phases/party-member-pokemon-phase.ts | 2 +- src/phases/select-modifier-phase.ts | 4 +- src/phases/select-starter-phase.ts | 13 +- src/phases/switch-phase.ts | 10 +- src/phases/switch-summon-phase.ts | 2 +- src/phases/title-phase.ts | 18 +- src/phases/toggle-double-position-phase.ts | 4 +- src/phases/turn-init-phase.ts | 10 +- src/system/achv.ts | 2 +- src/system/game-data.ts | 4 +- src/system/modifier-data.ts | 2 +- src/test/abilities/aroma_veil.test.ts | 4 +- src/test/abilities/battle_bond.test.ts | 2 +- src/test/abilities/disguise.test.ts | 2 +- src/test/abilities/forecast.test.ts | 2 +- src/test/abilities/ice_face.test.ts | 2 +- src/test/abilities/mimicry.test.ts | 2 +- src/test/abilities/pastel_veil.test.ts | 2 +- src/test/abilities/power_construct.test.ts | 4 +- src/test/abilities/schooling.test.ts | 2 +- src/test/abilities/serene_grace.test.ts | 4 +- src/test/abilities/sheer_force.test.ts | 56 ++--- src/test/abilities/shield_dust.test.ts | 22 +- src/test/abilities/shields_down.test.ts | 2 +- src/test/abilities/synchronize.test.ts | 38 ++-- src/test/abilities/wimp_out.test.ts | 12 +- src/test/abilities/zen_mode.test.ts | 203 ++++++++---------- src/test/abilities/zero_to_hero.test.ts | 4 +- src/test/battle/battle.test.ts | 6 +- src/test/battle/double_battle.test.ts | 2 +- src/test/daily_mode.test.ts | 2 +- src/test/evolution.test.ts | 12 +- src/test/field/pokemon.test.ts | 4 +- src/test/items/eviolite.test.ts | 62 ++---- src/test/items/leek.test.ts | 4 +- src/test/items/light_ball.test.ts | 14 +- src/test/items/metal_powder.test.ts | 14 +- src/test/items/quick_powder.test.ts | 14 +- src/test/items/thick_club.test.ts | 18 +- src/test/moves/aromatherapy.test.ts | 4 +- src/test/moves/baton_pass.test.ts | 2 +- src/test/moves/beat_up.test.ts | 2 +- src/test/moves/dragon_rage.test.ts | 2 +- src/test/moves/dragon_tail.test.ts | 6 +- src/test/moves/fissure.test.ts | 2 +- src/test/moves/fusion_flare_bolt.test.ts | 4 +- src/test/moves/heal_bell.test.ts | 4 +- src/test/moves/magnet_rise.test.ts | 10 +- src/test/moves/multi_target.test.ts | 2 +- src/test/moves/parting_shot.test.ts | 10 +- src/test/moves/rollout.test.ts | 2 +- src/test/moves/shed_tail.test.ts | 2 +- src/test/moves/sparkly_swirl.test.ts | 2 +- src/test/moves/spikes.test.ts | 2 +- src/test/moves/tackle.test.ts | 2 +- src/test/moves/tera_starstorm.test.ts | 4 +- src/test/moves/u_turn.test.ts | 33 +-- .../absolute-avarice-encounter.test.ts | 8 +- ...an-offer-you-cant-refuse-encounter.test.ts | 10 +- .../berries-abound-encounter.test.ts | 6 +- .../clowning-around-encounter.test.ts | 34 +-- .../dancing-lessons-encounter.test.ts | 24 +-- .../encounters/delibirdy-encounter.test.ts | 22 +- .../fiery-fallout-encounter.test.ts | 6 +- .../fight-or-flight-encounter.test.ts | 4 +- .../global-trade-system-encounter.test.ts | 20 +- .../encounters/lost-at-sea-encounter.test.ts | 6 +- .../encounters/part-timer-encounter.test.ts | 22 +- .../the-expert-breeder-encounter.test.ts | 6 +- .../the-pokemon-salesman-encounter.test.ts | 6 +- .../the-strong-stuff-encounter.test.ts | 4 +- .../uncommon-breed-encounter.test.ts | 8 +- .../encounters/weird-dream-encounter.test.ts | 12 +- .../mystery-encounter-utils.test.ts | 26 +-- src/test/phases/select-modifier-phase.test.ts | 6 +- src/test/ui/starter-select.test.ts | 60 +++--- src/test/utils/gameManager.ts | 37 ++-- src/test/utils/gameManagerUtils.ts | 2 +- src/ui/battle-message-ui-handler.ts | 2 +- src/ui/party-ui-handler.ts | 24 +-- src/ui/summary-ui-handler.ts | 2 +- 127 files changed, 833 insertions(+), 882 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 3c561206abe..c731677775d 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -764,57 +764,65 @@ export default class BattleScene extends SceneBase { return true; } - getParty(): PlayerPokemon[] { + public getPlayerParty(): PlayerPokemon[] { return this.party; } - getPlayerPokemon(): PlayerPokemon | undefined { - return this.getPlayerField().find(p => p.isActive()); - } - /** - * Finds the first {@linkcode Pokemon.isActive() | active PlayerPokemon} that isn't also currently switching out - * @returns Either the first {@linkcode PlayerPokemon} satisfying, or undefined if no player pokemon on the field satisfy + * @returns An array of {@linkcode PlayerPokemon} filtered from the player's party + * that are {@linkcode PlayerPokemon.isAllowedInBattle | allowed in battle}. */ - getNonSwitchedPlayerPokemon(): PlayerPokemon | undefined { - return this.getPlayerField().find(p => p.isActive() && p.switchOutStatus === false); + public getPokemonAllowedInBattle(): PlayerPokemon[] { + return this.getPlayerParty().filter(p => p.isAllowedInBattle()); } /** - * Returns an array of PlayerPokemon of length 1 or 2 depending on if double battles or not + * @returns The first {@linkcode PlayerPokemon} that is {@linkcode getPlayerField on the field} + * and {@linkcode PlayerPokemon.isActive is active} + * (aka {@linkcode PlayerPokemon.isAllowedInBattle is allowed in battle}), + * or `undefined` if there are no valid pokemon + * @param includeSwitching Whether a pokemon that is currently switching out is valid, default `true` + */ + public getPlayerPokemon(includeSwitching: boolean = true): PlayerPokemon | undefined { + return this.getPlayerField().find(p => p.isActive() && (includeSwitching || p.switchOutStatus === false)); + } + + /** + * Returns an array of PlayerPokemon of length 1 or 2 depending on if in a double battle or not. + * Does not actually check if the pokemon are on the field or not. * @returns array of {@linkcode PlayerPokemon} */ - getPlayerField(): PlayerPokemon[] { - const party = this.getParty(); + public getPlayerField(): PlayerPokemon[] { + const party = this.getPlayerParty(); return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)); } - getEnemyParty(): EnemyPokemon[] { + public getEnemyParty(): EnemyPokemon[] { return this.currentBattle?.enemyParty ?? []; } - getEnemyPokemon(): EnemyPokemon | undefined { - return this.getEnemyField().find(p => p.isActive()); - } - /** - * Finds the first {@linkcode Pokemon.isActive() | active EnemyPokemon} pokemon from the enemy that isn't also currently switching out - * @returns Either the first {@linkcode EnemyPokemon} satisfying, or undefined if no player pokemon on the field satisfy + * @returns The first {@linkcode EnemyPokemon} that is {@linkcode getEnemyField on the field} + * and {@linkcode EnemyPokemon.isActive is active} + * (aka {@linkcode EnemyPokemon.isAllowedInBattle is allowed in battle}), + * or `undefined` if there are no valid pokemon + * @param includeSwitching Whether a pokemon that is currently switching out is valid, default `true` */ - getNonSwitchedEnemyPokemon(): EnemyPokemon | undefined { - return this.getEnemyField().find(p => p.isActive() && p.switchOutStatus === false); + public getEnemyPokemon(includeSwitching: boolean = true): EnemyPokemon | undefined { + return this.getEnemyField().find(p => p.isActive() && (includeSwitching || p.switchOutStatus === false)); } /** - * Returns an array of EnemyPokemon of length 1 or 2 depending on if double battles or not + * Returns an array of EnemyPokemon of length 1 or 2 depending on if in a double battle or not. + * Does not actually check if the pokemon are on the field or not. * @returns array of {@linkcode EnemyPokemon} */ - getEnemyField(): EnemyPokemon[] { + public getEnemyField(): EnemyPokemon[] { const party = this.getEnemyParty(); return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)); } - getField(activeOnly: boolean = false): Pokemon[] { + public getField(activeOnly: boolean = false): Pokemon[] { const ret = new Array(4).fill(null); const playerField = this.getPlayerField(); const enemyField = this.getEnemyField(); @@ -867,7 +875,7 @@ export default class BattleScene extends SceneBase { getPokemonById(pokemonId: integer): Pokemon | null { const findInParty = (party: Pokemon[]) => party.find(p => p.id === pokemonId); - return (findInParty(this.getParty()) || findInParty(this.getEnemyParty())) ?? null; + return (findInParty(this.getPlayerParty()) || findInParty(this.getEnemyParty())) ?? null; } addPlayerPokemon(species: PokemonSpecies, level: integer, abilityIndex?: integer, formIndex?: integer, gender?: Gender, shiny?: boolean, variant?: Variant, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void): PlayerPokemon { @@ -1062,7 +1070,7 @@ export default class BattleScene extends SceneBase { this.modifierBar.removeAll(true); this.enemyModifierBar.removeAll(true); - for (const p of this.getParty()) { + for (const p of this.getPlayerParty()) { p.destroy(); } this.party = []; @@ -1275,7 +1283,7 @@ export default class BattleScene extends SceneBase { } }); - for (const pokemon of this.getParty()) { + for (const pokemon of this.getPlayerParty()) { pokemon.resetBattleData(); applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); } @@ -1285,7 +1293,7 @@ export default class BattleScene extends SceneBase { } } - for (const pokemon of this.getParty()) { + for (const pokemon of this.getPlayerParty()) { this.triggerPokemonFormChange(pokemon, SpeciesFormChangeTimeOfDayTrigger); } @@ -1480,7 +1488,7 @@ export default class BattleScene extends SceneBase { } trySpreadPokerus(): void { - const party = this.getParty(); + const party = this.getPlayerParty(); const infectedIndexes: integer[] = []; const spread = (index: number, spreadTo: number) => { const partyMember = party[index + spreadTo]; @@ -1677,7 +1685,7 @@ export default class BattleScene extends SceneBase { updateAndShowText(duration: number): void { const labels = [ this.luckLabelText, this.luckText ]; labels.forEach(t => t.setAlpha(0)); - const luckValue = getPartyLuckValue(this.getParty()); + const luckValue = getPartyLuckValue(this.getPlayerParty()); this.luckText.setText(getLuckString(luckValue)); if (luckValue < 14) { this.luckText.setTint(getLuckTextTint(luckValue)); @@ -2615,7 +2623,7 @@ export default class BattleScene extends SceneBase { removePartyMemberModifiers(partyMemberIndex: integer): Promise { return new Promise(resolve => { - const pokemonId = this.getParty()[partyMemberIndex].id; + const pokemonId = this.getPlayerParty()[partyMemberIndex].id; const modifiersToRemove = this.modifiers.filter(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === pokemonId); for (const m of modifiersToRemove) { this.modifiers.splice(this.modifiers.indexOf(m), 1); @@ -2742,7 +2750,7 @@ export default class BattleScene extends SceneBase { } } - this.updatePartyForModifiers(player ? this.getParty() : this.getEnemyParty(), instant).then(() => { + this.updatePartyForModifiers(player ? this.getPlayerParty() : this.getEnemyParty(), instant).then(() => { (player ? this.modifierBar : this.enemyModifierBar).updateModifiers(modifiers); if (!player) { this.updateUIPositions(); @@ -2980,7 +2988,7 @@ export default class BattleScene extends SceneBase { */ getActiveKeys(): string[] { const keys: string[] = []; - const playerParty = this.getParty(); + const playerParty = this.getPlayerParty(); playerParty.forEach(p => { keys.push(p.getSpriteKey(true)); keys.push(p.getBattleSpriteKey(true, true)); @@ -3016,7 +3024,7 @@ export default class BattleScene extends SceneBase { this.setFieldScale(0.75); this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false); this.currentBattle.double = true; - const availablePartyMembers = this.getParty().filter((p) => p.isAllowedInBattle()); + const availablePartyMembers = this.getPlayerParty().filter((p) => p.isAllowedInBattle()); if (availablePartyMembers.length > 1) { this.pushPhase(new ToggleDoublePositionPhase(this, true)); if (!availablePartyMembers[1].isOnField()) { @@ -3041,7 +3049,7 @@ export default class BattleScene extends SceneBase { */ applyPartyExp(expValue: number, pokemonDefeated: boolean, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set): void { const participantIds = pokemonParticipantIds ?? this.currentBattle.playerParticipantIds; - const party = this.getParty(); + const party = this.getPlayerParty(); const expShareModifier = this.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; const expBalanceModifier = this.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; const multipleParticipantExpBonusModifier = this.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier; diff --git a/src/data/ability.ts b/src/data/ability.ts index 5be87fc2397..8ec0f296068 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4869,7 +4869,7 @@ class ForceSwitchOutHelper { * - If the Pokémon is still alive (hp > 0), and if so, it leaves the field and a new SwitchPhase is initiated. */ if (switchOutTarget instanceof PlayerPokemon) { - if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { + if (switchOutTarget.scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { return false; } @@ -4950,7 +4950,7 @@ class ForceSwitchOutHelper { return false; } - const party = player ? pokemon.scene.getParty() : pokemon.scene.getEnemyParty(); + const party = player ? pokemon.scene.getPlayerParty() : pokemon.scene.getEnemyParty(); return (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD) || party.filter(p => p.isAllowedInBattle() && (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > pokemon.scene.currentBattle.getBattlerCount(); diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index c0b213e4def..8f22b288f45 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -478,7 +478,7 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [Species.NINCADA]: [ new SpeciesEvolution(Species.NINJASK, 20, null, null), - new SpeciesEvolution(Species.SHEDINJA, 20, null, new SpeciesEvolutionCondition(p => p.scene.getParty().length < 6 && p.scene.pokeballCounts[PokeballType.POKEBALL] > 0)) + new SpeciesEvolution(Species.SHEDINJA, 20, null, new SpeciesEvolutionCondition(p => p.scene.getPlayerParty().length < 6 && p.scene.pokeballCounts[PokeballType.POKEBALL] > 0)) ], [Species.WHISMUR]: [ new SpeciesEvolution(Species.LOUDRED, 20, null, null) @@ -890,7 +890,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.GOGOAT, 32, null, null) ], [Species.PANCHAM]: [ - new SpeciesEvolution(Species.PANGORO, 32, null, new SpeciesEvolutionCondition(p => !!p.scene.getParty().find(p => p.getTypes(false, false, true).indexOf(Type.DARK) > -1)), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(Species.PANGORO, 32, null, new SpeciesEvolutionCondition(p => !!p.scene.getPlayerParty().find(p => p.getTypes(false, false, true).indexOf(Type.DARK) > -1)), SpeciesWildEvolutionDelay.MEDIUM) ], [Species.ESPURR]: [ new SpeciesFormEvolution(Species.MEOWSTIC, "", "female", 25, null, new SpeciesEvolutionCondition(p => p.gender === Gender.FEMALE, p => p.gender = Gender.FEMALE)), diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 37900a3ab5a..5928a65b4e3 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -428,7 +428,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { moveAnim.bgSprite.setScale(1.25); moveAnim.bgSprite.setAlpha(this.opacity / 255); scene.field.add(moveAnim.bgSprite); - const fieldPokemon = scene.getNonSwitchedEnemyPokemon() || scene.getNonSwitchedPlayerPokemon(); + const fieldPokemon = scene.getEnemyPokemon(false) ?? scene.getPlayerPokemon(false); if (!isNullOrUndefined(priority)) { scene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority); } else if (fieldPokemon?.isOnField()) { @@ -999,7 +999,7 @@ export abstract class BattleAnim { const setSpritePriority = (priority: integer) => { switch (priority) { case 0: - scene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, scene.getNonSwitchedEnemyPokemon() || scene.getNonSwitchedPlayerPokemon()!); // This bang assumes that if (the EnemyPokemon is undefined, then the PlayerPokemon function must return an object), correct assumption? + scene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, scene.getEnemyPokemon(false) ?? scene.getPlayerPokemon(false)!); // TODO: is this bang correct? break; case 1: scene.field.moveTo(moveSprite, scene.field.getAll().length - 1); diff --git a/src/data/move.ts b/src/data/move.ts index cd206755238..2fc34d13df5 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1743,7 +1743,7 @@ export class PartyStatusCureAttr extends MoveEffectAttr { if (!this.canApply(user, target, move, args)) { return false; } - const partyPokemon = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + const partyPokemon = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); partyPokemon.forEach(p => this.cureStatus(p, user.id)); if (this.message) { @@ -1815,7 +1815,7 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { } // We don't know which party member will be chosen, so pick the highest max HP in the party - const maxPartyMemberHp = user.scene.getParty().map(p => p.getMaxHp()).reduce((maxHp: integer, hp: integer) => Math.max(hp, maxHp), 0); + const maxPartyMemberHp = user.scene.getPlayerParty().map(p => p.getMaxHp()).reduce((maxHp: integer, hp: integer) => Math.max(hp, maxHp), 0); user.scene.pushPhase(new PokemonHealPhase(user.scene, user.getBattlerIndex(), maxPartyMemberHp, i18next.t("moveTriggers:sacrificialFullRestore", { pokemonName: getPokemonNameWithAffix(user) }), true, false, false, true), true); @@ -1828,7 +1828,7 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { } getCondition(): MoveConditionFunc { - return (user, target, move) => user.scene.getParty().filter(p => p.isActive()).length > user.scene.currentBattle.getBattlerCount(); + return (user, _target, _move) => user.scene.getPlayerParty().filter(p => p.isActive()).length > user.scene.currentBattle.getBattlerCount(); } } @@ -2158,7 +2158,7 @@ export class MultiHitAttr extends MoveAttr { case MultiHitType._10: return 10; case MultiHitType.BEAT_UP: - const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + const party = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); // No status means the ally pokemon can contribute to Beat Up return party.reduce((total, pokemon) => { return total + (pokemon.id === user.id ? 1 : pokemon?.status && pokemon.status.effect !== StatusEffect.NONE ? 0 : 1); @@ -3467,7 +3467,7 @@ export class MovePowerMultiplierAttr extends VariablePowerAttr { * @returns The base power of the Beat Up hit. */ const beatUpFunc = (user: Pokemon, allyIndex: number): number => { - const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + const party = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); for (let i = allyIndex; i < party.length; i++) { const pokemon = party[i]; @@ -3495,7 +3495,7 @@ export class BeatUpAttr extends VariablePowerAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const power = args[0] as Utils.NumberHolder; - const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + const party = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); const allyCount = party.filter(pokemon => { return pokemon.id === user.id || !pokemon.status?.effect; }).length; @@ -5720,7 +5720,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr { return new Promise(resolve => { // If user is player, checks if the user has fainted pokemon if (user instanceof PlayerPokemon - && user.scene.getParty().findIndex(p => p.isFainted()) > -1) { + && user.scene.getPlayerParty().findIndex(p => p.isFainted()) > -1) { (user as PlayerPokemon).revivalBlessing().then(() => { resolve(true); }); @@ -5794,7 +5794,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } } // Switch out logic for the player's Pokemon - if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { + if (switchOutTarget.scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { return false; } @@ -5903,7 +5903,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } } - const party = player ? user.scene.getParty() : user.scene.getEnemyParty(); + const party = player ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); return (!player && !user.scene.currentBattle.battleType) || party.filter(p => p.isAllowedInBattle() && (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > user.scene.currentBattle.getBattlerCount(); @@ -7254,7 +7254,7 @@ const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined; const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => { - const party: Pokemon[] = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + const party: Pokemon[] = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField()); }; @@ -9184,7 +9184,7 @@ export function initMoves() { .target(MoveTarget.ALL) .condition((user, target, move) => { // If any fielded pokémon is grass-type and grounded. - return [ ...user.scene.getEnemyParty(), ...user.scene.getParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded()); + return [ ...user.scene.getEnemyParty(), ...user.scene.getPlayerParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded()); }) .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) diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index c53b802bb22..fa8e1aed1c7 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -181,7 +181,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = // Sort berries by party member ID to more easily re-add later if necessary const berryItemsMap = new Map(); - scene.getParty().forEach(pokemon => { + scene.getPlayerParty().forEach(pokemon => { const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id); if (pokemonBerries?.length > 0) { berryItemsMap.set(pokemon.id, pokemonBerries); @@ -267,7 +267,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED); encounter.setDialogueToken("foodReward", revSeed?.name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name")); const givePartyPokemonReviverSeeds = () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party.forEach(p => { const heldItems = p.getHeldItems(); if (revSeed && !heldItems.some(item => item instanceof PokemonInstantReviveModifier)) { @@ -308,7 +308,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = const berryMap = encounter.misc.berryItemsMap; // Returns 2/5 of the berries stolen to each Pokemon - const party = scene.getParty(); + const party = scene.getPlayerParty(); party.forEach(pokemon => { const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id); const berryTypesAsArray: BerryType[] = []; diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index 095f8a8473b..5524511c67b 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -58,7 +58,7 @@ export const BerriesAboundEncounter: MysteryEncounter = // Calculate boss mon const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); - const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); const config: EnemyPartyConfig = { @@ -77,7 +77,7 @@ export const BerriesAboundEncounter: MysteryEncounter = scene.currentBattle.waveIndex > 160 ? 7 : scene.currentBattle.waveIndex > 120 ? 5 : scene.currentBattle.waveIndex > 40 ? 4 : 2; - regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + regenerateModifierPoolThresholds(scene.getPlayerParty(), ModifierPoolType.PLAYER, 0); encounter.misc = { numBerries }; const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon); @@ -253,7 +253,7 @@ function tryGiveBerry(scene: BattleScene, prioritizedPokemon?: PlayerPokemon) { const berryType = randSeedInt(Object.keys(BerryType).filter(s => !isNaN(Number(s))).length) as BerryType; const berry = generateModifierType(scene, modifierTypes.BERRY, [ berryType ]) as BerryModifierType; - const party = scene.getParty(); + const party = scene.getPlayerParty(); // Will try to apply to prioritized pokemon first, then do normal application method if it fails if (prioritizedPokemon) { diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index e24eadb56c7..f2605795955 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -331,7 +331,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; // Player gets different rewards depending on the number of bug types they have - const numBugTypes = scene.getParty().filter(p => p.isOfType(Type.BUG, true)).length; + const numBugTypes = scene.getPlayerParty().filter(p => p.isOfType(Type.BUG, true)).length; const numBugTypesText = i18next.t(`${namespace}:numBugTypes`, { count: numBugTypes }); encounter.setDialogueToken("numBugTypes", numBugTypesText); diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 0a88c5a699c..5502cc7b53a 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -245,7 +245,7 @@ export const ClowningAroundEncounter: MysteryEncounter = // So Vitamins, form change items, etc. are not included const encounter = scene.currentBattle.mysteryEncounter!; - const party = scene.getParty(); + const party = scene.getPlayerParty(); let mostHeldItemsPokemon = party[0]; let count = mostHeldItemsPokemon.getHeldItems() .filter(m => m.isTransferable && !(m instanceof BerryModifier)) @@ -328,7 +328,7 @@ export const ClowningAroundEncounter: MysteryEncounter = .withPreOptionPhase(async (scene: BattleScene) => { // Randomize the second type of all player's pokemon // If the pokemon does not normally have a second type, it will gain 1 - for (const pokemon of scene.getParty()) { + for (const pokemon of scene.getPlayerParty()) { const originalTypes = pokemon.getTypes(false, false, true); // If the Pokemon has non-status moves that don't match the Pokemon's type, prioritizes those as the new type diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index 55d7ce0e92d..bae5a8790e9 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -1,32 +1,32 @@ -import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { Species } from "#enums/species"; +import { BattlerIndex } from "#app/battle"; import BattleScene from "#app/battle-scene"; +import { EncounterBattleAnim } from "#app/data/battle-anims"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; -import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; -import { Moves } from "#enums/moves"; -import { TrainerSlot } from "#app/data/trainer-config"; -import PokemonData from "#app/system/pokemon-data"; -import { Biome } from "#enums/biome"; -import { EncounterBattleAnim } from "#app/data/battle-anims"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { DANCING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; -import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; -import { BattlerIndex } from "#app/battle"; +import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { catchPokemon, getEncounterPokemonLevelForWave, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; -import { PokeballType } from "#enums/pokeball"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { TrainerSlot } from "#app/data/trainer-config"; +import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { modifierTypes } from "#app/modifier/modifier-type"; import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; -import { Stat } from "#enums/stat"; +import PokemonData from "#app/system/pokemon-data"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Biome } from "#enums/biome"; import { EncounterAnim } from "#enums/encounter-anims"; -import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Moves } from "#enums/moves"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { PokeballType } from "#enums/pokeball"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import i18next from "i18next"; /** the i18n namespace for this encounter */ @@ -92,7 +92,7 @@ export const DancingLessonsEncounter: MysteryEncounter = .withCatchAllowed(true) .withFleeAllowed(false) .withOnVisualsStart((scene: BattleScene) => { - const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getParty()[0]); + const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon()!); danceAnim.play(scene); return true; @@ -217,7 +217,7 @@ export const DancingLessonsEncounter: MysteryEncounter = const onPokemonSelected = (pokemon: PlayerPokemon) => { encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); - scene.unshiftPhase(new LearnMovePhase(scene, scene.getParty().indexOf(pokemon), Moves.REVELATION_DANCE)); + scene.unshiftPhase(new LearnMovePhase(scene, scene.getPlayerParty().indexOf(pokemon), Moves.REVELATION_DANCE)); // Play animation again to "learn" the dance const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon()); diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index d5f9388b56c..d5a938b9cef 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -1,22 +1,22 @@ -import { generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; -import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; -import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { BerryModifier, HealingBoosterModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, PokemonHeldItemModifier, PreserveBerryModifier } from "#app/modifier/modifier"; -import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; -import i18next from "#app/plugins/i18n"; -import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { getPokemonSpecies } from "#app/data/pokemon-species"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { BerryModifier, HealingBoosterModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, PokemonHeldItemModifier, PreserveBerryModifier } from "#app/modifier/modifier"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import i18next from "#app/plugins/i18n"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/delibirdy"; @@ -133,7 +133,7 @@ export const DelibirdyEncounter: MysteryEncounter = if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { // At max stacks, give the first party pokemon a Shell Bell instead const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + await applyModifierTypeToPlayerPokemon(scene, scene.getPlayerPokemon()!, shellBell); scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { @@ -207,7 +207,7 @@ export const DelibirdyEncounter: MysteryEncounter = if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { // At max stacks, give the first party pokemon a Shell Bell instead const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + await applyModifierTypeToPlayerPokemon(scene, scene.getPlayerPokemon()!, shellBell); scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { @@ -220,7 +220,7 @@ export const DelibirdyEncounter: MysteryEncounter = if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { // At max stacks, give the first party pokemon a Shell Bell instead const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + await applyModifierTypeToPlayerPokemon(scene, scene.getPlayerPokemon()!, shellBell); scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { @@ -299,7 +299,7 @@ export const DelibirdyEncounter: MysteryEncounter = if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { // At max stacks, give the first party pokemon a Shell Bell instead const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + await applyModifierTypeToPlayerPokemon(scene, scene.getPlayerParty()[0], shellBell); scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index bf5fb28163b..1c26df0cf71 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -214,7 +214,7 @@ function pokemonAndMoveChosen(scene: BattleScene, pokemon: PlayerPokemon, move: text: `${namespace}:incorrect_exp`, }, ]; - setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); + setEncounterExp(scene, scene.getPlayerParty().map((p) => p.id), 50); } else { encounter.selectedOption!.dialogue!.selected = [ { diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 5c16e5d8564..5794277ffe1 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -184,7 +184,7 @@ export const FieryFalloutEncounter: MysteryEncounter = async (scene: BattleScene) => { // Damage non-fire types and burn 1 random non-fire type member + give it Heatproof const encounter = scene.currentBattle.mysteryEncounter!; - const nonFireTypes = scene.getParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE)); + const nonFireTypes = scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE)); for (const pkm of nonFireTypes) { const percentage = DAMAGE_PERCENTAGE / 100; @@ -257,7 +257,7 @@ export const FieryFalloutEncounter: MysteryEncounter = function giveLeadPokemonAttackTypeBoostItem(scene: BattleScene) { // Give first party pokemon attack type boost item for free at end of battle - const leadPokemon = scene.getParty()?.[0]; + const leadPokemon = scene.getPlayerParty()?.[0]; if (leadPokemon) { // Generate type booster held item, default to Charcoal if item fails to generate let boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType; diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index 889868519d2..3f9030dc3b2 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -56,7 +56,7 @@ export const FightOrFlightEncounter: MysteryEncounter = // Calculate boss mon const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); - const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", bossPokemon.getNameToRender()); const config: EnemyPartyConfig = { @@ -86,11 +86,11 @@ export const FightOrFlightEncounter: MysteryEncounter = : scene.currentBattle.waveIndex > 40 ? ModifierTier.ULTRA : ModifierTier.GREAT; - regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + regenerateModifierPoolThresholds(scene.getPlayerParty(), ModifierPoolType.PLAYER, 0); let item: ModifierTypeOption | null = null; // TMs and Candy Jar excluded from possible rewards as they're too swingy in value for a singular item reward while (!item || item.type.id.includes("TM_") || item.type.id === "CANDY_JAR") { - item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; + item = getPlayerModifierTypeOptions(1, scene.getPlayerParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; } encounter.setDialogueToken("itemName", item.type.name); encounter.misc = item; 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 b843a929c08..7bf48aa5926 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -165,7 +165,7 @@ async function summonPlayerPokemon(scene: BattleScene) { const playerPokemon = encounter.misc.playerPokemon; // Swaps the chosen Pokemon and the first player's lead Pokemon in the party - const party = scene.getParty(); + const party = scene.getPlayerParty(); const chosenIndex = party.indexOf(playerPokemon); if (chosenIndex !== 0) { const leadPokemon = party[0]; diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 376bdf0c95d..1d65fd6c9a6 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -191,7 +191,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = receivedPokemonData.pokeball = randInt(4) as PokeballType; const dataSource = new PokemonData(receivedPokemonData); const newPlayerPokemon = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource); - scene.getParty().push(newPlayerPokemon); + scene.getPlayerParty().push(newPlayerPokemon); await newPlayerPokemon.loadAssets(); for (const mod of modifiers) { @@ -224,7 +224,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Randomly generate a Wonder Trade pokemon - const randomTradeOption = generateTradeOption(scene.getParty().map(p => p.species)); + const randomTradeOption = generateTradeOption(scene.getPlayerParty().map(p => p.species)); const tradePokemon = new EnemyPokemon(scene, randomTradeOption, pokemon.level, TrainerSlot.NONE, false); // Extra shiny roll at 1/128 odds (boosted by events and charms) if (!tradePokemon.shiny) { @@ -299,7 +299,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = receivedPokemonData.pokeball = randInt(4) as PokeballType; const dataSource = new PokemonData(receivedPokemonData); const newPlayerPokemon = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource); - scene.getParty().push(newPlayerPokemon); + scene.getPlayerParty().push(newPlayerPokemon); await newPlayerPokemon.loadAssets(); for (const mod of modifiers) { @@ -384,11 +384,11 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = tier++; } - regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + regenerateModifierPoolThresholds(scene.getPlayerParty(), ModifierPoolType.PLAYER, 0); let item: ModifierTypeOption | null = null; // TMs excluded from possible rewards while (!item || item.type.id.includes("TM_")) { - item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; + item = getPlayerModifierTypeOptions(1, scene.getPlayerParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; } encounter.setDialogueToken("itemName", item.type.name); @@ -430,9 +430,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = function getPokemonTradeOptions(scene: BattleScene): Map { const tradeOptionsMap: Map = new Map(); // Starts by filtering out any current party members as valid resulting species - const alreadyUsedSpecies: PokemonSpecies[] = scene.getParty().map(p => p.species); + const alreadyUsedSpecies: PokemonSpecies[] = scene.getPlayerParty().map(p => p.species); - scene.getParty().forEach(pokemon => { + scene.getPlayerParty().forEach(pokemon => { // If the party member is legendary/mythical, the only trade options available are always pulled from generation-specific legendary trade pools if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) { const generation = pokemon.species.generation; diff --git a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts index 8e7ea52a967..a8cb076bbe9 100644 --- a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -104,7 +104,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with ], }, async (scene: BattleScene) => { - const allowedPokemon = scene.getParty().filter((p) => p.isAllowedInBattle()); + const allowedPokemon = scene.getPlayerParty().filter((p) => p.isAllowedInBattle()); for (const pkm of allowedPokemon) { const percentage = DAMAGE_PERCENTAGE / 100; diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 693d935ae17..ab6517e97af 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -1,19 +1,19 @@ -import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { getHighestLevelPlayerPokemon, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { randSeedInt } from "#app/utils"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; -import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getHighestLevelPlayerPokemon, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getPokemonSpecies } from "#app/data/pokemon-species"; -import { Species } from "#enums/species"; -import { Moves } from "#enums/moves"; -import { GameOverPhase } from "#app/phases/game-over-phase"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { randSeedInt } from "#app/utils"; +import { Moves } from "#enums/moves"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; /** i18n namespace for encounter */ const namespace = "mysteryEncounters/mysteriousChest"; @@ -177,7 +177,7 @@ export const MysteriousChestEncounter: MysteryEncounter = await showEncounterText(scene, `${namespace}:option.1.bad`); // Handle game over edge case - const allowedPokemon = scene.getParty().filter(p => p.isAllowedInBattle()); + const allowedPokemon = scene.getPokemonAllowedInBattle(); if (allowedPokemon.length === 0) { // If there are no longer any legal pokemon in the party, game over. scene.clearPhaseQueue(); diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 8dd730492b1..798e85868f6 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -100,7 +100,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = // Only Pokemon that can gain benefits are above half HP with no status const selectableFilter = (pokemon: Pokemon) => { // If pokemon meets primary pokemon reqs, it can be selected - if (!pokemon.isAllowed()) { + if (!pokemon.isAllowedInChallenge()) { return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null; } if (!encounter.pokemonMeetsPrimaryRequirements(scene, pokemon)) { diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index e8f11f02e18..faeba95f358 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -134,7 +134,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter = // Init enemy const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); - const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); const config: EnemyPartyConfig = { @@ -170,7 +170,7 @@ async function doBiomeTransitionDialogueAndBattleInit(scene: BattleScene) { // Init enemy const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); - const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index 610209f8aad..7c904bd047a 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -126,7 +126,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = ]; // Determine the 3 pokemon the player can battle with - let partyCopy = scene.getParty().slice(0); + let partyCopy = scene.getPlayerParty().slice(0); partyCopy = partyCopy .filter(p => p.isAllowedInBattle()) .sort((a, b) => a.friendship - b.friendship); @@ -508,11 +508,11 @@ function getEggOptions(scene: BattleScene, commonEggs: number, rareEggs: number) } function removePokemonFromPartyAndStoreHeldItems(scene: BattleScene, encounter: MysteryEncounter, chosenPokemon: PlayerPokemon) { - const party = scene.getParty(); + const party = scene.getPlayerParty(); const chosenIndex = party.indexOf(chosenPokemon); party[chosenIndex] = party[0]; party[0] = chosenPokemon; - encounter.misc.originalParty = scene.getParty().slice(1); + encounter.misc.originalParty = scene.getPlayerParty().slice(1); encounter.misc.originalPartyHeldItems = encounter.misc.originalParty .map(p => p.getHeldItems()); scene["party"] = [ @@ -529,7 +529,7 @@ function checkAchievement(scene: BattleScene) { function restorePartyAndHeldItems(scene: BattleScene) { const encounter = scene.currentBattle.mysteryEncounter!; // Restore original party - scene.getParty().push(...encounter.misc.originalParty); + scene.getPlayerParty().push(...encounter.misc.originalParty); // Restore held items const originalHeldItems = encounter.misc.originalPartyHeldItems; diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index 397d2af9522..b1ef6cedb21 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -140,7 +140,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = // -15 to all base stats of highest BST (halved for HP), +10 to all base stats of rest of party (halved for HP) // Sort party by bst - const sortedParty = scene.getParty().slice(0) + const sortedParty = scene.getPlayerParty().slice(0) .sort((pokemon1, pokemon2) => { const pokemon1Bst = pokemon1.calculateBaseStats().reduce((a, b) => a + b, 0); const pokemon2Bst = pokemon2.calculateBaseStats().reduce((a, b) => a + b, 0); diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index bf322802f81..7d9b531c9ab 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -189,7 +189,7 @@ function endTrainerBattleAndShowDialogue(scene: BattleScene): Promise { const playerField = scene.getPlayerField(); playerField.forEach((_, p) => scene.unshiftPhase(new ReturnPhase(scene, p))); - for (const pokemon of scene.getParty()) { + for (const pokemon of scene.getPlayerParty()) { // Only trigger form change when Eiscue is in Noice form // Hardcoded Eiscue for now in case it is fused with another pokemon if (pokemon.species.speciesId === Species.EISCUE && pokemon.hasAbility(Abilities.ICE_FACE) && pokemon.formIndex === 1) { diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 03341a713f2..8814eb0d8cf 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -152,7 +152,7 @@ export const TrainingSessionEncounter: MysteryEncounter = } // Add pokemon and mods back - scene.getParty().push(playerPokemon); + scene.getPlayerParty().push(playerPokemon); for (const mod of modifiers.value) { mod.pokemonId = playerPokemon.id; scene.addModifier(mod, true, false, false, true); @@ -229,7 +229,7 @@ export const TrainingSessionEncounter: MysteryEncounter = scene.gameData.setPokemonCaught(playerPokemon, false); // Add pokemon and modifiers back - scene.getParty().push(playerPokemon); + scene.getPlayerParty().push(playerPokemon); for (const mod of modifiers.value) { mod.pokemonId = playerPokemon.id; scene.addModifier(mod, true, false, false, true); @@ -342,7 +342,7 @@ export const TrainingSessionEncounter: MysteryEncounter = scene.gameData.setPokemonCaught(playerPokemon, false); // Add pokemon and mods back - scene.getParty().push(playerPokemon); + scene.getPlayerParty().push(playerPokemon); for (const mod of modifiers.value) { mod.pokemonId = playerPokemon.id; scene.addModifier(mod, true, false, false, true); diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index d3c16ce2122..fba3a6ca95e 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -164,7 +164,7 @@ async function tryApplyDigRewardItems(scene: BattleScene) { const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; const leftovers = generateModifierType(scene, modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType; - const party = scene.getParty(); + const party = scene.getPlayerParty(); // Iterate over the party until an item was successfully given // First leftovers diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index b7ce3bab48c..a2c32c6af40 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -51,7 +51,7 @@ export const UncommonBreedEncounter: MysteryEncounter = // Calculate boss mon // Level equal to 2 below highest party member const level = getHighestLevelPlayerPokemon(scene, false, true).level - 2; - const species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); const pokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, true); // Pokemon will always have one of its egg moves in its moveset diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 2ecba6ce658..c05e707ed4d 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -176,7 +176,7 @@ export const WeirdDreamEncounter: MysteryEncounter = for (const transformation of scene.currentBattle.mysteryEncounter!.misc.teamTransformations) { scene.removePokemonFromPlayerParty(transformation.previousPokemon, false); - scene.getParty().push(transformation.newPokemon); + scene.getPlayerParty().push(transformation.newPokemon); } }) .withOptionPhase(async (scene: BattleScene) => { @@ -280,7 +280,7 @@ export const WeirdDreamEncounter: MysteryEncounter = const onBeforeRewards = () => { // Before battle rewards, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently) // One random pokemon will get its passive unlocked - const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive); + const passiveDisabledPokemon = scene.getPlayerParty().filter(p => !p.passive); if (passiveDisabledPokemon?.length > 0) { const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; enablePassiveMon.passive = true; @@ -306,7 +306,7 @@ export const WeirdDreamEncounter: MysteryEncounter = }, async (scene: BattleScene) => { // Leave, reduce party levels by 10% - for (const pokemon of scene.getParty()) { + for (const pokemon of scene.getPlayerParty()) { pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1); pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate); pokemon.levelExp = 0; @@ -329,7 +329,7 @@ interface PokemonTransformation { } function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { - const party = scene.getParty(); + const party = scene.getPlayerParty(); // Removes all pokemon from the party const alreadyUsedSpecies: PokemonSpecies[] = party.map(p => p.species); const pokemonTransformations: PokemonTransformation[] = party.map(p => { @@ -404,7 +404,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon if (shouldGetOldGateau(newPokemon)) { const stats = getOldGateauBoostedStats(newPokemon); const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU() - .generateType(scene.getParty(), [ OLD_GATEAU_STATS_UP, stats ]) + .generateType(scene.getPlayerParty(), [ OLD_GATEAU_STATS_UP, stats ]) ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU); const modifier = modType?.newModifier(newPokemon); if (modifier) { @@ -417,7 +417,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon } // One random pokemon will get its passive unlocked - const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive); + const passiveDisabledPokemon = scene.getPlayerParty().filter(p => !p.passive); if (passiveDisabledPokemon?.length > 0) { const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; enablePassiveMon.passive = true; diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index ffae71b9555..c674ebdc46e 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -88,7 +88,7 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption { * @param pokemon */ pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean { - return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); + return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getPlayerParty()).map(p => p.id).includes(pokemon.id)); } /** @@ -102,10 +102,10 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption { if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { return true; } - let qualified: PlayerPokemon[] = scene.getParty(); + let qualified: PlayerPokemon[] = scene.getPlayerParty(); for (const req of this.primaryPokemonRequirements) { if (req.meetsRequirement(scene)) { - const queryParty = req.queryParty(scene.getParty()); + const queryParty = req.queryParty(scene.getPlayerParty()); qualified = qualified.filter(pkmn => queryParty.includes(pkmn)); } else { this.primaryPokemon = undefined; @@ -162,10 +162,10 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption { return true; } - let qualified: PlayerPokemon[] = scene.getParty(); + let qualified: PlayerPokemon[] = scene.getPlayerParty(); for (const req of this.secondaryPokemonRequirements) { if (req.meetsRequirement(scene)) { - const queryParty = req.queryParty(scene.getParty()); + const queryParty = req.queryParty(scene.getPlayerParty()); qualified = qualified.filter(pkmn => queryParty.includes(pkmn)); } else { this.secondaryPokemon = []; diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 91ea0c5be19..1358c465d17 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -1,21 +1,21 @@ -import { PlayerPokemon } from "#app/field/pokemon"; import BattleScene from "#app/battle-scene"; -import { isNullOrUndefined } from "#app/utils"; -import { Abilities } from "#enums/abilities"; -import { Moves } from "#enums/moves"; -import { Species } from "#enums/species"; -import { TimeOfDay } from "#enums/time-of-day"; -import { Nature } from "#app/data/nature"; +import { allAbilities } from "#app/data/ability"; import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; +import { Nature } from "#app/data/nature"; import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; import { StatusEffect } from "#app/data/status-effect"; import { Type } from "#app/data/type"; import { WeatherType } from "#app/data/weather"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { PlayerPokemon } from "#app/field/pokemon"; import { AttackTypeBoosterModifier } from "#app/modifier/modifier"; import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; +import { isNullOrUndefined } from "#app/utils"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; import { SpeciesFormKey } from "#enums/species-form-key"; -import { allAbilities } from "#app/data/ability"; +import { TimeOfDay } from "#enums/time-of-day"; export interface EncounterRequirement { meetsRequirement(scene: BattleScene): boolean; // Boolean to see if a requirement is met @@ -333,7 +333,7 @@ export class PartySizeRequirement extends EncounterSceneRequirement { override meetsRequirement(scene: BattleScene): boolean { if (!isNullOrUndefined(this.partySizeRange) && this.partySizeRange[0] <= this.partySizeRange[1]) { - const partySize = this.excludeDisallowedPokemon ? scene.getParty().filter(p => p.isAllowedInBattle()).length : scene.getParty().length; + const partySize = this.excludeDisallowedPokemon ? scene.getPokemonAllowedInBattle().length : scene.getPlayerParty().length; if (partySize >= 0 && (this.partySizeRange[0] >= 0 && this.partySizeRange[0] > partySize) || (this.partySizeRange[1] >= 0 && this.partySizeRange[1] < partySize)) { return false; } @@ -343,7 +343,7 @@ export class PartySizeRequirement extends EncounterSceneRequirement { } override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - return [ "partySize", scene.getParty().length.toString() ]; + return [ "partySize", scene.getPlayerParty().length.toString() ]; } } @@ -358,7 +358,7 @@ export class PersistentModifierRequirement extends EncounterSceneRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) { return false; } @@ -421,7 +421,7 @@ export class SpeciesRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredSpecies?.length < 0) { return false; } @@ -459,7 +459,7 @@ export class NatureRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredNature?.length < 0) { return false; } @@ -498,7 +498,7 @@ export class TypeRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - let partyPokemon = scene.getParty(); + let partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon)) { return false; @@ -545,7 +545,7 @@ export class MoveRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { return false; } @@ -594,7 +594,7 @@ export class CompatibleMoveRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { return false; } @@ -635,7 +635,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredAbilities?.length < 0) { return false; } @@ -677,7 +677,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredStatusEffect?.length < 0) { return false; } @@ -746,7 +746,7 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredFormChangeItem?.length < 0) { return false; } @@ -798,7 +798,7 @@ export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredEvolutionItem?.length < 0) { return false; } @@ -849,7 +849,7 @@ export class HeldItemRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon)) { return false; } @@ -900,7 +900,7 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon)) { return false; } @@ -957,7 +957,7 @@ export class LevelRequirement extends EncounterPokemonRequirement { override meetsRequirement(scene: BattleScene): boolean { // Party Pokemon inside required level range if (!isNullOrUndefined(this.requiredLevelRange) && this.requiredLevelRange[0] <= this.requiredLevelRange[1]) { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { return false; @@ -995,7 +995,7 @@ export class FriendshipRequirement extends EncounterPokemonRequirement { override meetsRequirement(scene: BattleScene): boolean { // Party Pokemon inside required friendship range if (!isNullOrUndefined(this.requiredFriendshipRange) && this.requiredFriendshipRange[0] <= this.requiredFriendshipRange[1]) { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { return false; @@ -1038,7 +1038,7 @@ export class HealthRatioRequirement extends EncounterPokemonRequirement { override meetsRequirement(scene: BattleScene): boolean { // Party Pokemon's health inside required health range if (!isNullOrUndefined(this.requiredHealthRange) && this.requiredHealthRange[0] <= this.requiredHealthRange[1]) { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { return false; @@ -1082,7 +1082,7 @@ export class WeightRequirement extends EncounterPokemonRequirement { override meetsRequirement(scene: BattleScene): boolean { // Party Pokemon's weight inside required weight range if (!isNullOrUndefined(this.requiredWeightRange) && this.requiredWeightRange[0] <= this.requiredWeightRange[1]) { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { return false; diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index c045ee51bd7..badbfcf7a56 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -314,7 +314,7 @@ export default class MysteryEncounter implements IMysteryEncounter { * @param pokemon */ pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean { - return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); + return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getPlayerParty()).map(p => p.id).includes(pokemon.id)); } /** @@ -326,18 +326,18 @@ export default class MysteryEncounter implements IMysteryEncounter { */ private meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean { if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { - const activeMon = scene.getParty().filter(p => p.isActive(true)); + const activeMon = scene.getPlayerParty().filter(p => p.isActive(true)); if (activeMon.length > 0) { this.primaryPokemon = activeMon[0]; } else { - this.primaryPokemon = scene.getParty().filter(p => p.isAllowedInBattle())[0]; + this.primaryPokemon = scene.getPlayerParty().filter(p => p.isAllowedInBattle())[0]; } return true; } - let qualified: PlayerPokemon[] = scene.getParty(); + let qualified: PlayerPokemon[] = scene.getPlayerParty(); for (const req of this.primaryPokemonRequirements) { if (req.meetsRequirement(scene)) { - qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn)); + qualified = qualified.filter(pkmn => req.queryParty(scene.getPlayerParty()).includes(pkmn)); } else { this.primaryPokemon = undefined; return false; @@ -394,10 +394,10 @@ export default class MysteryEncounter implements IMysteryEncounter { return true; } - let qualified: PlayerPokemon[] = scene.getParty(); + let qualified: PlayerPokemon[] = scene.getPlayerParty(); for (const req of this.secondaryPokemonRequirements) { if (req.meetsRequirement(scene)) { - qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn)); + qualified = qualified.filter(pkmn => req.queryParty(scene.getPlayerParty()).includes(pkmn)); } else { this.secondaryPokemon = []; return false; diff --git a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts index f34d383dbbc..a2c08938fbe 100644 --- a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts +++ b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts @@ -39,7 +39,7 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty().filter((pkm) => (this.includeFainted ? pkm.isAllowed() : pkm.isAllowedInBattle())); + const partyPokemon = scene.getPlayerParty().filter((pkm) => (this.includeFainted ? pkm.isAllowedInChallenge() : pkm.isAllowedInBattle())); if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { return false; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 66459c96ede..429b6bfa292 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -418,9 +418,9 @@ export function generateModifierType(scene: BattleScene, modifier: () => Modifie // Populates item id and tier (order matters) result = result .withIdFromFunc(modifierTypes[modifierId]) - .withTierFromPool(ModifierPoolType.PLAYER, scene.getParty()); + .withTierFromPool(ModifierPoolType.PLAYER, scene.getPlayerParty()); - return result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; + return result instanceof ModifierTypeGenerator ? result.generateType(scene.getPlayerParty(), pregenArgs) : result; } /** @@ -451,9 +451,9 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p // Open party screen to choose pokemon scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => { - if (slotIndex < scene.getParty().length) { + if (slotIndex < scene.getPlayerParty().length) { scene.ui.setMode(modeToSetOnExit).then(() => { - const pokemon = scene.getParty()[slotIndex]; + const pokemon = scene.getPlayerParty()[slotIndex]; const secondaryOptions = onPokemonSelected(pokemon); if (!secondaryOptions) { scene.currentBattle.mysteryEncounter!.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); @@ -563,7 +563,7 @@ export function selectOptionThenPokemon(scene: BattleScene, options: OptionSelec const selectPokemonAfterOption = (selectedOptionIndex: number) => { // Open party screen to choose a Pokemon scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => { - if (slotIndex < scene.getParty().length) { + if (slotIndex < scene.getPlayerParty().length) { // Pokemon and option selected scene.ui.setMode(modeToSetOnExit).then(() => { const result: PokemonAndOptionSelected = { selectedPokemonIndex: slotIndex, selectedOptionIndex: selectedOptionIndex }; @@ -713,7 +713,7 @@ export function leaveEncounterWithoutBattle(scene: BattleScene, addHealPhase: bo * @param doNotContinue - default `false`. If set to true, will not end the battle and continue to next wave */ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: boolean = false, doNotContinue: boolean = false) { - const allowedPkm = scene.getParty().filter((pkm) => pkm.isAllowedInBattle()); + const allowedPkm = scene.getPlayerParty().filter((pkm) => pkm.isAllowedInBattle()); if (allowedPkm.length === 0) { scene.clearPhaseQueue(); @@ -750,7 +750,7 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: * @param addHealPhase */ export function handleMysteryEncounterBattleFailed(scene: BattleScene, addHealPhase: boolean = false, doNotContinue: boolean = false) { - const allowedPkm = scene.getParty().filter((pkm) => pkm.isAllowedInBattle()); + const allowedPkm = scene.getPlayerParty().filter((pkm) => pkm.isAllowedInBattle()); if (allowedPkm.length === 0) { scene.clearPhaseQueue(); diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index b1adc478ab0..dd42cc9ce55 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -53,24 +53,24 @@ export function getSpriteKeysFromPokemon(pokemon: Pokemon): { spriteKey: string, } /** - * Will never remove the player's last non-fainted Pokemon (if they only have 1) + * Will never remove the player's last non-fainted Pokemon (if they only have 1). * Otherwise, picks a Pokemon completely at random and removes from the party * @param scene - * @param isAllowed Default false. If true, only picks from legal mons. If no legal mons are found (or there is 1, with `doNotReturnLastAllowedMon = true), will return a mon that is not allowed. - * @param isFainted Default false. If true, includes fainted mons. - * @param doNotReturnLastAllowedMon Default false. If true, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted) + * @param isAllowed Default `false`. If `true`, only picks from legal mons. If no legal mons are found (or there is 1, with `doNotReturnLastAllowedMon = true`), will return a mon that is not allowed. + * @param isFainted Default `false`. If `true`, includes fainted mons. + * @param doNotReturnLastAllowedMon Default `false`. If `true`, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted) * @returns */ export function getRandomPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false, doNotReturnLastAllowedMon: boolean = false): PlayerPokemon { - const party = scene.getParty(); + const party = scene.getPlayerParty(); let chosenIndex: number; let chosenPokemon: PlayerPokemon | null = null; - const fullyLegalMons = party.filter(p => (!isAllowed || p.isAllowed()) && (isFainted || !p.isFainted())); - const allowedOnlyMons = party.filter(p => p.isAllowed()); + const fullyLegalMons = party.filter(p => (!isAllowed || p.isAllowedInChallenge()) && (isFainted || !p.isFainted())); + const allowedOnlyMons = party.filter(p => p.isAllowedInChallenge()); if (doNotReturnLastAllowedMon && fullyLegalMons.length === 1) { // If there is only 1 legal/unfainted mon left, select from fainted legal mons - const faintedLegalMons = party.filter(p => (!isAllowed || p.isAllowed()) && p.isFainted()); + const faintedLegalMons = party.filter(p => (!isAllowed || p.isAllowedInChallenge()) && p.isFainted()); if (faintedLegalMons.length > 0) { chosenIndex = randSeedInt(faintedLegalMons.length); chosenPokemon = faintedLegalMons[chosenIndex]; @@ -101,11 +101,11 @@ export function getRandomPlayerPokemon(scene: BattleScene, isAllowed: boolean = * @returns */ export function getHighestLevelPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { - const party = scene.getParty(); + const party = scene.getPlayerParty(); let pokemon: PlayerPokemon | null = null; for (const p of party) { - if (isAllowed && !p.isAllowed()) { + if (isAllowed && !p.isAllowedInChallenge()) { continue; } if (!isFainted && p.isFainted()) { @@ -127,11 +127,11 @@ export function getHighestLevelPlayerPokemon(scene: BattleScene, isAllowed: bool * @returns */ export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentStat, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { - const party = scene.getParty(); + const party = scene.getPlayerParty(); let pokemon: PlayerPokemon | null = null; for (const p of party) { - if (isAllowed && !p.isAllowed()) { + if (isAllowed && !p.isAllowedInChallenge()) { continue; } if (!isFainted && p.isFainted()) { @@ -152,11 +152,11 @@ export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentS * @returns */ export function getLowestLevelPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { - const party = scene.getParty(); + const party = scene.getPlayerParty(); let pokemon: PlayerPokemon | null = null; for (const p of party) { - if (isAllowed && !p.isAllowed()) { + if (isAllowed && !p.isAllowedInChallenge()) { continue; } if (!isFainted && p.isFainted()) { @@ -177,11 +177,11 @@ export function getLowestLevelPlayerPokemon(scene: BattleScene, isAllowed: boole * @returns */ export function getHighestStatTotalPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { - const party = scene.getParty(); + const party = scene.getPlayerParty(); let pokemon: PlayerPokemon | null = null; for (const p of party) { - if (isAllowed && !p.isAllowed()) { + if (isAllowed && !p.isAllowedInChallenge()) { continue; } if (!isFainted && p.isFainted()) { @@ -315,7 +315,7 @@ export function applyHealToPokemon(scene: BattleScene, pokemon: PlayerPokemon, h */ export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: number) { const modType = modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE() - .generateType(pokemon.scene.getParty(), [ value ]) + .generateType(pokemon.scene.getPlayerParty(), [ value ]) ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE); const modifier = modType?.newModifier(pokemon); if (modifier) { @@ -591,7 +591,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po const addToParty = (slotIndex?: number) => { const newPokemon = pokemon.addToParty(pokeballType, slotIndex); const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); - if (scene.getParty().filter(p => p.isShiny()).length === 6) { + if (scene.getPlayerParty().filter(p => p.isShiny()).length === 6) { scene.validateAchv(achvs.SHINY_PARTY); } Promise.all(modifiers.map(m => scene.addModifier(m, true))).then(() => { @@ -605,7 +605,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po }); }; Promise.all([ pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon) ]).then(() => { - if (scene.getParty().length === 6) { + if (scene.getPlayerParty().length === 6) { const promptRelease = () => { scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true); @@ -826,7 +826,7 @@ export async function addPokemonDataToDexAndValidateAchievements(scene: BattleSc * @param invalidSelectionKey */ export function isPokemonValidForEncounterOptionSelection(pokemon: Pokemon, scene: BattleScene, invalidSelectionKey: string): string | null { - if (!pokemon.isAllowed()) { + if (!pokemon.isAllowedInChallenge()) { return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null; } if (!pokemon.isAllowedInBattle()) { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 6c328fa25cc..2dcef4b3965 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -325,35 +325,45 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.scene.field.getIndex(this) > -1; } - isFainted(checkStatus?: boolean): boolean { - return !this.hp && (!checkStatus || this.status?.effect === StatusEffect.FAINT); + /** + * Checks if a pokemon is fainted (ie: its `hp <= 0`). + * It's usually better to call {@linkcode isAllowedInBattle()} + * @param checkStatus `true` to also check that the pokemon's status is {@linkcode StatusEffect.FAINT} + * @returns `true` if the pokemon is fainted + */ + public isFainted(checkStatus: boolean = false): boolean { + return this.hp <= 0 && (!checkStatus || this.status?.effect === StatusEffect.FAINT); } /** - * Check if this pokemon is both not fainted (or a fled wild pokemon) and allowed to be in battle. - * This is frequently a better alternative to {@link isFainted} - * @returns {boolean} True if pokemon is allowed in battle + * Check if this pokemon is both not fainted and allowed to be in battle based on currently active challenges. + * @returns {boolean} `true` if pokemon is allowed in battle */ - isAllowedInBattle(): boolean { - return !this.isFainted() && this.isAllowed(); + public isAllowedInBattle(): boolean { + return !this.isFainted() && this.isAllowedInChallenge(); } /** - * Check if this pokemon is allowed (no challenge exclusion) - * This is frequently a better alternative to {@link isFainted} - * @returns {boolean} True if pokemon is allowed in battle + * Check if this pokemon is allowed based on any active challenges. + * It's usually better to call {@linkcode isAllowedInBattle()} + * @returns {boolean} `true` if pokemon is allowed in battle */ - isAllowed(): boolean { + public isAllowedInChallenge(): boolean { const challengeAllowed = new Utils.BooleanHolder(true); applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, this, challengeAllowed); return challengeAllowed.value; } - isActive(onField?: boolean): boolean { + /** + * Checks if the pokemon is allowed in battle (ie: not fainted, and allowed under any active challenges). + * @param onField `true` to also check if the pokemon is currently on the field, defaults to `false` + * @returns `true` if the pokemon is "active". Returns `false` if there is no active {@linkcode BattleScene} + */ + public isActive(onField: boolean = false): boolean { if (!this.scene) { return false; } - return this.isAllowedInBattle() && !!this.scene && (!onField || this.isOnField()); + return this.isAllowedInBattle() && (!onField || this.isOnField()); } getDexAttr(): bigint { @@ -4198,7 +4208,7 @@ export class PlayerPokemon extends Pokemon { return new Promise(resolve => { this.scene.ui.setMode(Mode.PARTY, PartyUiMode.REVIVAL_BLESSING, this.getFieldIndex(), (slotIndex:integer, option: PartyOption) => { if (slotIndex >= 0 && slotIndex < 6) { - const pokemon = this.scene.getParty()[slotIndex]; + const pokemon = this.scene.getPlayerParty()[slotIndex]; if (!pokemon || !pokemon.isFainted()) { resolve(); } @@ -4208,7 +4218,7 @@ export class PlayerPokemon extends Pokemon { pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp())); this.scene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: pokemon.name }), 0, true); - if (this.scene.currentBattle.double && this.scene.getParty().length > 1) { + if (this.scene.currentBattle.double && this.scene.getPlayerParty().length > 1) { const allyPokemon = this.getAlly(); if (slotIndex <= 1) { // Revived ally pokemon @@ -4350,7 +4360,7 @@ export class PlayerPokemon extends Pokemon { newPokemon.fusionLuck = this.fusionLuck; newPokemon.usedTMs = this.usedTMs; - this.scene.getParty().push(newPokemon); + this.scene.getPlayerParty().push(newPokemon); newPokemon.evolve((!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution)), evoSpecies); const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id, true) as PokemonHeldItemModifier[]; @@ -4466,8 +4476,8 @@ export class PlayerPokemon extends Pokemon { this.generateCompatibleTms(); this.updateInfo(true); - const fusedPartyMemberIndex = this.scene.getParty().indexOf(pokemon); - let partyMemberIndex = this.scene.getParty().indexOf(this); + const fusedPartyMemberIndex = this.scene.getPlayerParty().indexOf(pokemon); + let partyMemberIndex = this.scene.getPlayerParty().indexOf(this); if (partyMemberIndex > fusedPartyMemberIndex) { partyMemberIndex--; } @@ -4480,8 +4490,8 @@ export class PlayerPokemon extends Pokemon { Promise.allSettled(transferModifiers).then(() => { this.scene.updateModifiers(true, true).then(() => { this.scene.removePartyMemberModifiers(fusedPartyMemberIndex); - this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0]; - const newPartyMemberIndex = this.scene.getParty().indexOf(this); + this.scene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0]; + const newPartyMemberIndex = this.scene.getPlayerParty().indexOf(this); pokemon.getMoveset(true).map((m: PokemonMove) => this.scene.unshiftPhase(new LearnMovePhase(this.scene, newPartyMemberIndex, m.getMove().id))); pokemon.destroy(); this.updateFusionPalette(); @@ -5072,7 +5082,7 @@ export class EnemyPokemon extends Pokemon { * @returns the pokemon that was added or null if the pokemon could not be added */ addToParty(pokeballType: PokeballType, slotIndex: number = -1) { - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); let ret: PlayerPokemon | null = null; if (party.length < PLAYER_PARTY_MAX_SIZE) { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index d34804ba040..0891262649c 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -746,7 +746,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { return 0; } if (pokemon.isPlayer() && forThreshold) { - return scene.getParty().map(p => this.getMaxHeldItemCount(p)).reduce((stackCount: number, maxStackCount: number) => Math.max(stackCount, maxStackCount), 0); + return scene.getPlayerParty().map(p => this.getMaxHeldItemCount(p)).reduce((stackCount: number, maxStackCount: number) => Math.max(stackCount, maxStackCount), 0); } return this.getMaxHeldItemCount(pokemon); } @@ -2022,7 +2022,7 @@ export abstract class ConsumablePokemonModifier extends ConsumableModifier { abstract override apply(playerPokemon: PlayerPokemon, ...args: unknown[]): boolean | Promise; getPokemon(scene: BattleScene) { - return scene.getParty().find(p => p.id === this.pokemonId); + return scene.getPlayerParty().find(p => p.id === this.pokemonId); } } @@ -2224,7 +2224,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier { playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY); - playerPokemon.scene.unshiftPhase(new LevelUpPhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.level - levelCount.value, playerPokemon.level)); + playerPokemon.scene.unshiftPhase(new LevelUpPhase(playerPokemon.scene, playerPokemon.scene.getPlayerParty().indexOf(playerPokemon), playerPokemon.level - levelCount.value, playerPokemon.level)); return true; } @@ -2244,7 +2244,7 @@ export class TmModifier extends ConsumablePokemonModifier { */ override apply(playerPokemon: PlayerPokemon): boolean { - playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), this.type.moveId, LearnMoveType.TM)); + playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getPlayerParty().indexOf(playerPokemon), this.type.moveId, LearnMoveType.TM)); return true; } @@ -2266,7 +2266,7 @@ export class RememberMoveModifier extends ConsumablePokemonModifier { */ override apply(playerPokemon: PlayerPokemon, cost?: number): boolean { - playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex], LearnMoveType.MEMORY, cost)); + playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getPlayerParty().indexOf(playerPokemon), playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex], LearnMoveType.MEMORY, cost)); return true; } @@ -2783,7 +2783,7 @@ export class MoneyRewardModifier extends ConsumableModifier { battleScene.addMoney(moneyAmount.value); - battleScene.getParty().map(p => { + battleScene.getPlayerParty().map(p => { if (p.species?.speciesId === Species.GIMMIGHOUL || p.fusionSpecies?.speciesId === Species.GIMMIGHOUL) { p.evoCounter ? p.evoCounter++ : p.evoCounter = 1; const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier(p) as EvoTrackerModifier; diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index 3e46fc792f0..483e6eac943 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -1,21 +1,22 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { getPokeballCatchMultiplier, getPokeballAtlasKey, getPokeballTintColor, doPokeballBounceAnim } from "#app/data/pokeball"; +import BattleScene from "#app/battle-scene"; +import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; +import { SubstituteTag } from "#app/data/battler-tags"; +import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor } from "#app/data/pokeball"; import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; -import { PokeballType } from "#app/enums/pokeball"; -import { StatusEffect } from "#app/enums/status-effect"; -import { addPokeballOpenParticles, addPokeballCaptureStars } from "#app/field/anims"; +import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; import { EnemyPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; +import { VictoryPhase } from "#app/phases/victory-phase"; import { achvs } from "#app/system/achv"; -import { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; +import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import { SummaryUiMode } from "#app/ui/summary-ui-handler"; import { Mode } from "#app/ui/ui"; +import { PokeballType } from "#enums/pokeball"; +import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; -import { PokemonPhase } from "./pokemon-phase"; -import { VictoryPhase } from "./victory-phase"; -import { SubstituteTag } from "#app/data/battler-tags"; export class AttemptCapturePhase extends PokemonPhase { private pokeballType: PokeballType; @@ -235,7 +236,7 @@ export class AttemptCapturePhase extends PokemonPhase { const addToParty = (slotIndex?: number) => { const newPokemon = pokemon.addToParty(this.pokeballType, slotIndex); const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); - if (this.scene.getParty().filter(p => p.isShiny()).length === 6) { + if (this.scene.getPlayerParty().filter(p => p.isShiny()).length === PLAYER_PARTY_MAX_SIZE) { this.scene.validateAchv(achvs.SHINY_PARTY); } Promise.all(modifiers.map(m => this.scene.addModifier(m, true))).then(() => { @@ -249,7 +250,7 @@ export class AttemptCapturePhase extends PokemonPhase { }); }; Promise.all([ pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon) ]).then(() => { - if (this.scene.getParty().length === 6) { + if (this.scene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) { const promptRelease = () => { this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { this.scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true); diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index bae61aa2288..3b9ca012ef7 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -1,8 +1,8 @@ +import BattleScene from "#app/battle-scene"; import { applyPostBattleAbAttrs, PostBattleAbAttr } from "#app/data/ability"; import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier"; import { BattlePhase } from "./battle-phase"; import { GameOverPhase } from "./game-over-phase"; -import BattleScene from "#app/battle-scene"; export class BattleEndPhase extends BattlePhase { /** If true, will increment battles won */ @@ -41,7 +41,7 @@ export class BattleEndPhase extends BattlePhase { } } - for (const pokemon of this.scene.getParty().filter(p => p.isAllowedInBattle())) { + for (const pokemon of this.scene.getPokemonAllowedInBattle()) { applyPostBattleAbAttrs(PostBattleAbAttr, pokemon); } diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index 5e459d0e6b5..b87dff32f60 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -37,7 +37,7 @@ export class CheckSwitchPhase extends BattlePhase { return; } - if (!this.scene.getParty().slice(1).filter(p => p.isActive()).length) { + if (!this.scene.getPlayerParty().slice(1).filter(p => p.isActive()).length) { super.end(); return; } diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index b7071c4cc6f..c4d919c0325 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -1,40 +1,40 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex, BattleType } from "#app/battle"; +import BattleScene from "#app/battle-scene"; +import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { applyAbAttrs, SyncEncounterNatureAbAttr } from "#app/data/ability"; +import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims"; import { getCharVariantFromDialogue } from "#app/data/dialogue"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { doTrainerExclamation } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { TrainerSlot } from "#app/data/trainer-config"; import { getRandomWeatherType } from "#app/data/weather"; -import { BattleSpec } from "#app/enums/battle-spec"; -import { PlayerGender } from "#app/enums/player-gender"; -import { Species } from "#app/enums/species"; import { EncounterPhaseEvent } from "#app/events/battle-scene"; import Pokemon, { FieldPosition } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { ModifierPoolType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { BoostBugSpawnModifier, IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier"; +import { ModifierPoolType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import Overrides from "#app/overrides"; +import { BattlePhase } from "#app/phases/battle-phase"; +import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { PostSummonPhase } from "#app/phases/post-summon-phase"; +import { ReturnPhase } from "#app/phases/return-phase"; +import { ScanIvsPhase } from "#app/phases/scan-ivs-phase"; +import { ShinySparklePhase } from "#app/phases/shiny-sparkle-phase"; +import { SummonPhase } from "#app/phases/summon-phase"; +import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase"; import { achvs } from "#app/system/achv"; import { handleTutorial, Tutorial } from "#app/tutorial"; import { Mode } from "#app/ui/ui"; -import i18next from "i18next"; -import { BattlePhase } from "./battle-phase"; -import * as Utils from "#app/utils"; -import { randSeedInt } from "#app/utils"; -import { CheckSwitchPhase } from "./check-switch-phase"; -import { GameOverPhase } from "./game-over-phase"; -import { PostSummonPhase } from "./post-summon-phase"; -import { ReturnPhase } from "./return-phase"; -import { ScanIvsPhase } from "./scan-ivs-phase"; -import { ShinySparklePhase } from "./shiny-sparkle-phase"; -import { SummonPhase } from "./summon-phase"; -import { ToggleDoublePositionPhase } from "./toggle-double-position-phase"; -import Overrides from "#app/overrides"; -import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims"; -import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; -import { doTrainerExclamation } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; -import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { randSeedInt, randSeedItem } from "#app/utils"; +import { BattleSpec } from "#enums/battle-spec"; import { Biome } from "#enums/biome"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { PlayerGender } from "#enums/player-gender"; +import { Species } from "#enums/species"; +import i18next from "i18next"; import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; export class EncounterPhase extends BattlePhase { @@ -116,7 +116,7 @@ export class EncounterPhase extends BattlePhase { if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { battle.enemyParty[e].ivs = new Array(6).fill(31); } - this.scene.getParty().slice(0, !battle.double ? 1 : 2).reverse().forEach(playerPokemon => { + this.scene.getPlayerParty().slice(0, !battle.double ? 1 : 2).reverse().forEach(playerPokemon => { applyAbAttrs(SyncEncounterNatureAbAttr, playerPokemon, null, false, battle.enemyParty[e]); }); } @@ -156,7 +156,7 @@ export class EncounterPhase extends BattlePhase { return true; }); - if (this.scene.getParty().filter(p => p.isShiny()).length === 6) { + if (this.scene.getPlayerParty().filter(p => p.isShiny()).length === PLAYER_PARTY_MAX_SIZE) { this.scene.validateAchv(achvs.SHINY_PARTY); } @@ -248,7 +248,7 @@ export class EncounterPhase extends BattlePhase { /*if (startingWave > 10) { for (let m = 0; m < Math.min(Math.floor(startingWave / 10), 99); m++) - this.scene.addModifier(getPlayerModifierTypeOptionsForWave((m + 1) * 10, 1, this.scene.getParty())[0].type.newModifier(), true); + this.scene.addModifier(getPlayerModifierTypeOptionsForWave((m + 1) * 10, 1, this.scene.getPlayerParty())[0].type.newModifier(), true); this.scene.updateModifiers(true); }*/ @@ -259,7 +259,7 @@ export class EncounterPhase extends BattlePhase { this.scene.mysteryEncounterSaveData.encounterSpawnChance += WEIGHT_INCREMENT_ON_SPAWN_MISS; } - for (const pokemon of this.scene.getParty()) { + for (const pokemon of this.scene.getPlayerParty()) { if (pokemon) { pokemon.resetBattleData(); } @@ -338,7 +338,7 @@ export class EncounterPhase extends BattlePhase { const doSummon = () => { this.scene.currentBattle.started = true; this.scene.playBgm(undefined); - this.scene.pbTray.showPbTray(this.scene.getParty()); + this.scene.pbTray.showPbTray(this.scene.getPlayerParty()); this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty()); const doTrainerSummon = () => { this.hideEnemyTrainer(); @@ -362,7 +362,7 @@ export class EncounterPhase extends BattlePhase { doSummon(); } else { let message: string; - this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.waveIndex); + this.scene.executeWithSeedOffset(() => message = randSeedItem(encounterMessages), this.scene.currentBattle.waveIndex); message = message!; // tell TS compiler it's defined now const showDialogueAndSummon = () => { this.scene.ui.showDialogue(message, trainer?.getName(TrainerSlot.NONE, true), null, () => { @@ -447,13 +447,13 @@ export class EncounterPhase extends BattlePhase { if (![ BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER ].includes(this.scene.currentBattle.battleType)) { enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => { // if there is not a player party, we can't continue - if (!this.scene.getParty()?.length) { + if (!this.scene.getPlayerParty().length) { return false; } // how many player pokemon are on the field ? - const pokemonsOnFieldCount = this.scene.getParty().filter(p => p.isOnField()).length; + const pokemonsOnFieldCount = this.scene.getPlayerParty().filter(p => p.isOnField()).length; // if it's a 2vs1, there will never be a 2nd pokemon on our field even - const requiredPokemonsOnField = Math.min(this.scene.getParty().filter((p) => !p.isFainted()).length, 2); + const requiredPokemonsOnField = Math.min(this.scene.getPlayerParty().filter((p) => !p.isFainted()).length, 2); // if it's a double, there should be 2, otherwise 1 if (this.scene.currentBattle.double) { return pokemonsOnFieldCount === requiredPokemonsOnField; @@ -467,7 +467,7 @@ export class EncounterPhase extends BattlePhase { } if (!this.loaded) { - const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()); + const availablePartyMembers = this.scene.getPokemonAllowedInBattle(); if (!availablePartyMembers[0].isOnField()) { this.scene.pushPhase(new SummonPhase(this.scene, 0)); diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index 59b73fe9e11..7b079c3fbf9 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -223,7 +223,7 @@ export class EvolutionPhase extends Phase { this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => { const levelMoves = this.pokemon.getLevelMoves(this.lastLevel + 1, true); for (const lm of levelMoves) { - this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getParty().indexOf(this.pokemon), lm[1])); + this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getPlayerParty().indexOf(this.pokemon), lm[1])); } this.scene.unshiftPhase(new EndEvolutionPhase(this.scene)); diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 8eb84beded5..3e90233a38c 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -1,24 +1,24 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex, BattleType } from "#app/battle"; -import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability"; +import BattleScene from "#app/battle-scene"; +import { applyPostFaintAbAttrs, applyPostKnockOutAbAttrs, applyPostVictoryAbAttrs, PostFaintAbAttr, PostKnockOutAbAttr, PostVictoryAbAttr } from "#app/data/ability"; import { BattlerTagLapseType, DestinyBondTag } from "#app/data/battler-tags"; import { battleSpecDialogue } from "#app/data/dialogue"; import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move"; +import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; import { BattleSpec } from "#app/enums/battle-spec"; import { StatusEffect } from "#app/enums/status-effect"; -import Pokemon, { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon"; +import Pokemon, { EnemyPokemon, HitResult, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonInstantReviveModifier } from "#app/modifier/modifier"; +import { SwitchType } from "#enums/switch-type"; import i18next from "i18next"; import { DamagePhase } from "./damage-phase"; +import { GameOverPhase } from "./game-over-phase"; import { PokemonPhase } from "./pokemon-phase"; +import { SwitchPhase } from "./switch-phase"; import { SwitchSummonPhase } from "./switch-summon-phase"; import { ToggleDoublePositionPhase } from "./toggle-double-position-phase"; -import { GameOverPhase } from "./game-over-phase"; -import { SwitchPhase } from "./switch-phase"; import { VictoryPhase } from "./victory-phase"; -import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; -import { SwitchType } from "#enums/switch-type"; import { isNullOrUndefined } from "#app/utils"; import { FRIENDSHIP_LOSS_FROM_FAINT } from "#app/data/balance/starters"; @@ -120,7 +120,7 @@ export class FaintPhase extends PokemonPhase { if (this.player) { /** The total number of Pokemon in the player's party that can legally fight */ - const legalPlayerPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); + const legalPlayerPokemon = this.scene.getPokemonAllowedInBattle(); /** The total number of legal player Pokemon that aren't currently on the field */ const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true)); if (!legalPlayerPokemon.length) { diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 9c444fc40f0..1302d8fc652 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -1,8 +1,8 @@ import { clientSessionId } from "#app/account"; import { BattleType } from "#app/battle"; import BattleScene from "#app/battle-scene"; -import { getCharVariantFromDialogue } from "#app/data/dialogue"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; +import { getCharVariantFromDialogue } from "#app/data/dialogue"; import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species"; import { trainerConfigs } from "#app/data/trainer-config"; import Pokemon from "#app/field/pokemon"; @@ -65,7 +65,7 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.loadSession(this.scene, this.scene.sessionSlotId).then(() => { this.scene.pushPhase(new EncounterPhase(this.scene, true)); - const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length; + const availablePartyMembers = this.scene.getPokemonAllowedInBattle().length; this.scene.pushPhase(new SummonPhase(this.scene, 0)); if (this.scene.currentBattle.double && availablePartyMembers > 1) { @@ -97,7 +97,7 @@ export class GameOverPhase extends BattlePhase { firstClear = this.scene.validateAchv(achvs.CLASSIC_VICTORY); this.scene.validateAchv(achvs.UNEVOLVED_CLASSIC_VICTORY); this.scene.gameData.gameStats.sessionsWon++; - for (const pokemon of this.scene.getParty()) { + for (const pokemon of this.scene.getPlayerParty()) { this.awardRibbon(pokemon); if (pokemon.species.getRootSpeciesId() !== pokemon.species.getRootSpeciesId(true)) { @@ -195,13 +195,13 @@ export class GameOverPhase extends BattlePhase { if (!this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.ENDLESS_MODE)); } - if (this.scene.getParty().filter(p => p.fusionSpecies).length && !this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) { + if (this.scene.getPlayerParty().filter(p => p.fusionSpecies).length && !this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) { this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.SPLICED_ENDLESS_MODE)); } if (!this.scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE]) { this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.MINI_BLACK_HOLE)); } - if (!this.scene.gameData.unlocks[Unlockables.EVIOLITE] && this.scene.getParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)) { + if (!this.scene.gameData.unlocks[Unlockables.EVIOLITE] && this.scene.getPlayerParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)) { this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.EVIOLITE)); } } diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 49e78fa5369..e7d1f7e9074 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -1,31 +1,31 @@ +import { BattlerTagLapseType } from "#app/data/battler-tags"; +import MysteryEncounterOption, { OptionPhaseCallback } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { NewBattlePhase } from "#app/phases/new-battle-phase"; +import { PostTurnStatusEffectPhase } from "#app/phases/post-turn-status-effect-phase"; +import { ReturnPhase } from "#app/phases/return-phase"; +import { ScanIvsPhase } from "#app/phases/scan-ivs-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { SummonPhase } from "#app/phases/summon-phase"; +import { SwitchPhase } from "#app/phases/switch-phase"; +import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase"; +import { BattleSpec } from "#enums/battle-spec"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { SwitchType } from "#enums/switch-type"; import i18next from "i18next"; import BattleScene from "../battle-scene"; +import { getCharVariantFromDialogue } from "../data/dialogue"; +import { OptionSelectSettings, transitionMysteryEncounterIntroVisuals } from "../data/mystery-encounters/utils/encounter-phase-utils"; +import { TrainerSlot } from "../data/trainer-config"; +import { IvScannerModifier } from "../modifier/modifier"; import { Phase } from "../phase"; import { Mode } from "../ui/ui"; -import { transitionMysteryEncounterIntroVisuals, OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils"; -import MysteryEncounterOption, { OptionPhaseCallback } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { getCharVariantFromDialogue } from "../data/dialogue"; -import { TrainerSlot } from "../data/trainer-config"; -import { BattleSpec } from "#enums/battle-spec"; -import { IvScannerModifier } from "../modifier/modifier"; import * as Utils from "../utils"; import { isNullOrUndefined } from "../utils"; -import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { BattlerTagLapseType } from "#app/data/battler-tags"; -import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; -import { PostTurnStatusEffectPhase } from "#app/phases/post-turn-status-effect-phase"; -import { SummonPhase } from "#app/phases/summon-phase"; -import { ScanIvsPhase } from "#app/phases/scan-ivs-phase"; -import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase"; -import { ReturnPhase } from "#app/phases/return-phase"; -import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; -import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; -import { NewBattlePhase } from "#app/phases/new-battle-phase"; -import { GameOverPhase } from "#app/phases/game-over-phase"; -import { SwitchPhase } from "#app/phases/switch-phase"; -import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; -import { SwitchType } from "#enums/switch-type"; -import { BattlerTagType } from "#enums/battler-tag-type"; /** * Will handle (in order): @@ -238,7 +238,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { } // The total number of Pokemon in the player's party that can legally fight - const legalPlayerPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); + const legalPlayerPokemon = this.scene.getPokemonAllowedInBattle(); // The total number of legal player Pokemon that aren't currently on the field const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true)); if (!legalPlayerPokemon.length) { @@ -343,7 +343,7 @@ export class MysteryEncounterBattlePhase extends Phase { const doSummon = () => { scene.currentBattle.started = true; scene.playBgm(undefined); - scene.pbTray.showPbTray(scene.getParty()); + scene.pbTray.showPbTray(scene.getPlayerParty()); scene.pbTrayEnemy.showPbTray(scene.getEnemyParty()); const doTrainerSummon = () => { this.hideEnemyTrainer(); @@ -402,7 +402,7 @@ export class MysteryEncounterBattlePhase extends Phase { } } - const availablePartyMembers = scene.getParty().filter(p => p.isAllowedInBattle()); + const availablePartyMembers = scene.getPlayerParty().filter(p => p.isAllowedInBattle()); if (!availablePartyMembers[0].isOnField()) { scene.pushPhase(new SummonPhase(scene, 0)); diff --git a/src/phases/new-biome-encounter-phase.ts b/src/phases/new-biome-encounter-phase.ts index eea591c3936..910306b76ad 100644 --- a/src/phases/new-biome-encounter-phase.ts +++ b/src/phases/new-biome-encounter-phase.ts @@ -11,13 +11,13 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { doEncounter(): void { this.scene.playBgm(undefined, true); - for (const pokemon of this.scene.getParty()) { + for (const pokemon of this.scene.getPlayerParty()) { if (pokemon) { pokemon.resetBattleData(); } } - for (const pokemon of this.scene.getParty().filter(p => p.isOnField())) { + for (const pokemon of this.scene.getPlayerParty().filter(p => p.isOnField())) { applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null); } diff --git a/src/phases/next-encounter-phase.ts b/src/phases/next-encounter-phase.ts index 407d7c26b5d..e086ed4fe3e 100644 --- a/src/phases/next-encounter-phase.ts +++ b/src/phases/next-encounter-phase.ts @@ -13,7 +13,7 @@ export class NextEncounterPhase extends EncounterPhase { doEncounter(): void { this.scene.playBgm(undefined, true); - for (const pokemon of this.scene.getParty()) { + for (const pokemon of this.scene.getPlayerParty()) { if (pokemon) { pokemon.resetBattleData(); } diff --git a/src/phases/party-heal-phase.ts b/src/phases/party-heal-phase.ts index e6ee11202df..4841bf9a5b4 100644 --- a/src/phases/party-heal-phase.ts +++ b/src/phases/party-heal-phase.ts @@ -19,7 +19,7 @@ export class PartyHealPhase extends BattlePhase { this.scene.fadeOutBgm(1000, false); } this.scene.ui.fadeOut(1000).then(() => { - for (const pokemon of this.scene.getParty()) { + for (const pokemon of this.scene.getPlayerParty()) { pokemon.hp = pokemon.getMaxHp(); pokemon.resetStatus(); for (const move of pokemon.moveset) { diff --git a/src/phases/party-member-pokemon-phase.ts b/src/phases/party-member-pokemon-phase.ts index 2b6ca01261d..f2e2b23bfb2 100644 --- a/src/phases/party-member-pokemon-phase.ts +++ b/src/phases/party-member-pokemon-phase.ts @@ -18,7 +18,7 @@ export abstract class PartyMemberPokemonPhase extends FieldPhase { } getParty(): Pokemon[] { - return this.player ? this.scene.getParty() : this.scene.getEnemyParty(); + return this.player ? this.scene.getPlayerParty() : this.scene.getEnemyParty(); } getPokemon(): Pokemon { diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index e5a60692bb4..98975e30720 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -38,7 +38,7 @@ export class SelectModifierPhase extends BattlePhase { this.scene.reroll = false; } - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); if (!this.isCopy) { regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount); } @@ -289,7 +289,7 @@ export class SelectModifierPhase extends BattlePhase { } getModifierTypeOptions(modifierCount: integer): ModifierTypeOption[] { - return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings); + return getPlayerModifierTypeOptions(modifierCount, this.scene.getPlayerParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings); } copy(): SelectModifierPhase { diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index 1692b5f2234..2273ab1cd3c 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -3,16 +3,15 @@ import { applyChallenges, ChallengeType } from "#app/data/challenge"; import { Gender } from "#app/data/gender"; import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms"; import { getPokemonSpecies } from "#app/data/pokemon-species"; -import { Species } from "#app/enums/species"; -import { PlayerPokemon } from "#app/field/pokemon"; -import { overrideModifiers, overrideHeldItems } from "#app/modifier/modifier"; +import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier"; +import Overrides from "#app/overrides"; import { Phase } from "#app/phase"; +import { TitlePhase } from "#app/phases/title-phase"; import { SaveSlotUiMode } from "#app/ui/save-slot-select-ui-handler"; import { Starter } from "#app/ui/starter-select-ui-handler"; import { Mode } from "#app/ui/ui"; +import { Species } from "#enums/species"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -import { TitlePhase } from "./title-phase"; -import Overrides from "#app/overrides"; export class SelectStarterPhase extends Phase { @@ -44,7 +43,7 @@ export class SelectStarterPhase extends Phase { * @param starters {@linkcode Pokemon} with which to start the first battle */ initBattle(starters: Starter[]) { - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); const loadPokemonAssets: Promise[] = []; starters.forEach((starter: Starter, i: integer) => { if (!i && Overrides.STARTER_SPECIES_OVERRIDE) { @@ -103,7 +102,7 @@ export class SelectStarterPhase extends Phase { this.scene.sessionPlayTime = 0; this.scene.lastSavePlayTime = 0; // Ensures Keldeo (or any future Pokemon that have this type of form change) starts in the correct form - this.scene.getParty().forEach((p: PlayerPokemon) => { + this.scene.getPlayerParty().forEach((p) => { this.scene.triggerPokemonFormChange(p, SpeciesFormChangeMoveLearnedTrigger); }); this.end(); diff --git a/src/phases/switch-phase.ts b/src/phases/switch-phase.ts index f5ce2179715..2abb109a529 100644 --- a/src/phases/switch-phase.ts +++ b/src/phases/switch-phase.ts @@ -1,7 +1,7 @@ import BattleScene from "#app/battle-scene"; -import { SwitchType } from "#enums/switch-type"; -import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; +import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import { Mode } from "#app/ui/ui"; +import { SwitchType } from "#enums/switch-type"; import { BattlePhase } from "./battle-phase"; import { SwitchSummonPhase } from "./switch-summon-phase"; @@ -38,7 +38,7 @@ export class SwitchPhase extends BattlePhase { super.start(); // Skip modal switch if impossible (no remaining party members that aren't in battle) - if (this.isModal && !this.scene.getParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) { + if (this.isModal && !this.scene.getPlayerParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) { return super.end(); } @@ -49,7 +49,7 @@ export class SwitchPhase extends BattlePhase { * if the mon should have already been returned but is still alive and well * on the field. see also; battle.test.ts */ - if (this.isModal && !this.doReturn && !this.scene.getParty()[this.fieldIndex].isFainted()) { + if (this.isModal && !this.doReturn && !this.scene.getPlayerParty()[this.fieldIndex].isFainted()) { return super.end(); } @@ -59,7 +59,7 @@ export class SwitchPhase extends BattlePhase { } // Override field index to 0 in case of double battle where 2/3 remaining legal party members fainted at once - const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? this.fieldIndex : 0; + const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getPokemonAllowedInBattle().length > 1 ? this.fieldIndex : 0; this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => { if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) { diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index c7e7bbe011e..36db8b7a7e7 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -54,7 +54,7 @@ export class SwitchSummonPhase extends SummonPhase { } } - if (!this.doReturn || (this.slotIndex !== -1 && !(this.player ? this.scene.getParty() : this.scene.getEnemyParty())[this.slotIndex])) { + if (!this.doReturn || (this.slotIndex !== -1 && !(this.player ? this.scene.getPlayerParty() : this.scene.getEnemyParty())[this.slotIndex])) { if (this.player) { return this.switchAndSummon(); } else { diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 58683cf8ec8..8338d39b81f 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -1,21 +1,21 @@ import { loggedInUser } from "#app/account"; -import BattleScene from "#app/battle-scene"; import { BattleType } from "#app/battle"; -import { getDailyRunStarters, fetchDailyRunSeed } from "#app/data/daily-run"; +import BattleScene from "#app/battle-scene"; +import { fetchDailyRunSeed, getDailyRunStarters } from "#app/data/daily-run"; import { Gender } from "#app/data/gender"; import { getBiomeKey } from "#app/field/arena"; -import { GameModes, GameMode, getGameMode } from "#app/game-mode"; -import { regenerateModifierPoolThresholds, ModifierPoolType, modifierTypes, getDailyRunStarterModifiers } from "#app/modifier/modifier-type"; +import { GameMode, GameModes, getGameMode } from "#app/game-mode"; +import { Modifier } from "#app/modifier/modifier"; +import { getDailyRunStarterModifiers, ModifierPoolType, modifierTypes, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { Phase } from "#app/phase"; import { SessionSaveData } from "#app/system/game-data"; import { Unlockables } from "#app/system/unlockables"; import { vouchers } from "#app/system/voucher"; -import { OptionSelectItem, OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handler"; +import { OptionSelectConfig, OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { SaveSlotUiMode } from "#app/ui/save-slot-select-ui-handler"; import { Mode } from "#app/ui/ui"; -import i18next from "i18next"; import * as Utils from "#app/utils"; -import { Modifier } from "#app/modifier/modifier"; +import i18next from "i18next"; import { CheckSwitchPhase } from "./check-switch-phase"; import { EncounterPhase } from "./encounter-phase"; import { SelectChallengePhase } from "./select-challenge-phase"; @@ -203,7 +203,7 @@ export class TitlePhase extends Phase { const starters = getDailyRunStarters(this.scene, seed); const startingLevel = this.scene.gameMode.getStartingLevel(); - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); const loadPokemonAssets: Promise[] = []; for (const starter of starters) { const starterProps = this.scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); @@ -276,7 +276,7 @@ export class TitlePhase extends Phase { this.scene.pushPhase(new EncounterPhase(this.scene, this.loaded)); if (this.loaded) { - const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length; + const availablePartyMembers = this.scene.getPokemonAllowedInBattle().length; this.scene.pushPhase(new SummonPhase(this.scene, 0, true, true)); if (this.scene.currentBattle.double && availablePartyMembers > 1) { diff --git a/src/phases/toggle-double-position-phase.ts b/src/phases/toggle-double-position-phase.ts index 563af8575d7..eff92bc6acd 100644 --- a/src/phases/toggle-double-position-phase.ts +++ b/src/phases/toggle-double-position-phase.ts @@ -16,9 +16,9 @@ export class ToggleDoublePositionPhase extends BattlePhase { const playerPokemon = this.scene.getPlayerField().find(p => p.isActive(true)); if (playerPokemon) { - playerPokemon.setFieldPosition(this.double && this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => { + playerPokemon.setFieldPosition(this.double && this.scene.getPokemonAllowedInBattle().length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => { if (playerPokemon.getFieldIndex() === 1) { - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); party[1] = party[0]; party[0] = playerPokemon; } diff --git a/src/phases/turn-init-phase.ts b/src/phases/turn-init-phase.ts index 2f1b539cdcf..baff6c7d73f 100644 --- a/src/phases/turn-init-phase.ts +++ b/src/phases/turn-init-phase.ts @@ -1,15 +1,15 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; +import BattleScene from "#app/battle-scene"; +import { handleMysteryEncounterBattleStartEffects, handleMysteryEncounterTurnStartEffects } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { TurnInitEvent } from "#app/events/battle-scene"; import { PlayerPokemon } from "#app/field/pokemon"; import i18next from "i18next"; -import { FieldPhase } from "./field-phase"; -import { ToggleDoublePositionPhase } from "./toggle-double-position-phase"; import { CommandPhase } from "./command-phase"; import { EnemyCommandPhase } from "./enemy-command-phase"; +import { FieldPhase } from "./field-phase"; import { GameOverPhase } from "./game-over-phase"; +import { ToggleDoublePositionPhase } from "./toggle-double-position-phase"; import { TurnStartPhase } from "./turn-start-phase"; -import { handleMysteryEncounterBattleStartEffects, handleMysteryEncounterTurnStartEffects } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; export class TurnInitPhase extends FieldPhase { constructor(scene: BattleScene) { @@ -24,7 +24,7 @@ export class TurnInitPhase extends FieldPhase { if (p.isOnField() && !p.isAllowedInBattle()) { this.scene.queueMessage(i18next.t("challenges:illegalEvolution", { "pokemon": p.name }), null, true); - const allowedPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); + const allowedPokemon = this.scene.getPokemonAllowedInBattle(); if (!allowedPokemon.length) { // If there are no longer any legal pokemon in the party, game over. diff --git a/src/system/achv.ts b/src/system/achv.ts index 366813328e2..d94fcba48f2 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -328,7 +328,7 @@ export const achvs = { HIDDEN_ABILITY: new Achv("HIDDEN_ABILITY", "", "HIDDEN_ABILITY.description", "ability_charm", 75), PERFECT_IVS: new Achv("PERFECT_IVS", "", "PERFECT_IVS.description", "blunder_policy", 100), CLASSIC_VICTORY: new Achv("CLASSIC_VICTORY", "", "CLASSIC_VICTORY.description", "relic_crown", 150, c => c.gameData.gameStats.sessionsWon === 0), - UNEVOLVED_CLASSIC_VICTORY: new Achv("UNEVOLVED_CLASSIC_VICTORY", "", "UNEVOLVED_CLASSIC_VICTORY.description", "eviolite", 175, c => c.getParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)), + UNEVOLVED_CLASSIC_VICTORY: new Achv("UNEVOLVED_CLASSIC_VICTORY", "", "UNEVOLVED_CLASSIC_VICTORY.description", "eviolite", 175, c => c.getPlayerParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)), MONO_GEN_ONE_VICTORY: new ChallengeAchv("MONO_GEN_ONE", "", "MONO_GEN_ONE.description", "ribbon_gen1", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 1 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_TWO_VICTORY: new ChallengeAchv("MONO_GEN_TWO", "", "MONO_GEN_TWO.description", "ribbon_gen2", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 2 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_THREE_VICTORY: new ChallengeAchv("MONO_GEN_THREE", "", "MONO_GEN_THREE.description", "ribbon_gen3", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 3 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), diff --git a/src/system/game-data.ts b/src/system/game-data.ts index c00159a7fd7..49a677cd019 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -949,7 +949,7 @@ export class GameData { seed: scene.seed, playTime: scene.sessionPlayTime, gameMode: scene.gameMode.modeId, - party: scene.getParty().map(p => new PokemonData(p)), + party: scene.getPlayerParty().map(p => new PokemonData(p)), enemyParty: scene.getEnemyParty().map(p => new PokemonData(p)), modifiers: scene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), enemyModifiers: scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), @@ -1028,7 +1028,7 @@ export class GameData { const loadPokemonAssets: Promise[] = []; - const party = scene.getParty(); + const party = scene.getPlayerParty(); party.splice(0, party.length); for (const p of sessionData.party) { diff --git a/src/system/modifier-data.ts b/src/system/modifier-data.ts index 1514f7e3fb3..c68f9ccb47d 100644 --- a/src/system/modifier-data.ts +++ b/src/system/modifier-data.ts @@ -38,7 +38,7 @@ export default class ModifierData { type.id = this.typeId; if (type instanceof ModifierTypeGenerator) { - type = (type as ModifierTypeGenerator).generateType(this.player ? scene.getParty() : scene.getEnemyField(), this.typePregenArgs); + type = (type as ModifierTypeGenerator).generateType(this.player ? scene.getPlayerParty() : scene.getEnemyField(), this.typePregenArgs); } const ret = Reflect.construct(constructor, ([ type ] as any[]).concat(this.args).concat(this.stackCount)) as PersistentModifier; diff --git a/src/test/abilities/aroma_veil.test.ts b/src/test/abilities/aroma_veil.test.ts index 4284eb43a75..e74d0ff5a39 100644 --- a/src/test/abilities/aroma_veil.test.ts +++ b/src/test/abilities/aroma_veil.test.ts @@ -36,7 +36,7 @@ describe("Moves - Aroma Veil", () => { it("Aroma Veil protects the Pokemon's side against most Move Restriction Battler Tags", async () => { await game.classicMode.startBattle([ Species.REGIELEKI, Species.BULBASAUR ]); - const party = game.scene.getParty()! as PlayerPokemon[]; + const party = game.scene.getPlayerParty()! as PlayerPokemon[]; game.move.select(Moves.GROWL); game.move.select(Moves.GROWL); @@ -50,7 +50,7 @@ describe("Moves - Aroma Veil", () => { it("Aroma Veil does not protect against Imprison", async () => { await game.classicMode.startBattle([ Species.REGIELEKI, Species.BULBASAUR ]); - const party = game.scene.getParty()! as PlayerPokemon[]; + const party = game.scene.getPlayerParty()! as PlayerPokemon[]; game.move.select(Moves.GROWL); game.move.select(Moves.GROWL, 1); diff --git a/src/test/abilities/battle_bond.test.ts b/src/test/abilities/battle_bond.test.ts index 283fb0d0f14..e9970e1c049 100644 --- a/src/test/abilities/battle_bond.test.ts +++ b/src/test/abilities/battle_bond.test.ts @@ -40,7 +40,7 @@ describe("Abilities - BATTLE BOND", () => { it("check if fainted pokemon switches to base form on arena reset", async () => { await game.classicMode.startBattle([ Species.MAGIKARP, Species.GRENINJA ]); - const greninja = game.scene.getParty()[1]; + const greninja = game.scene.getPlayerParty()[1]; expect(greninja.formIndex).toBe(ashForm); greninja.hp = 0; diff --git a/src/test/abilities/disguise.test.ts b/src/test/abilities/disguise.test.ts index 0241aa4b9ea..f44782207cb 100644 --- a/src/test/abilities/disguise.test.ts +++ b/src/test/abilities/disguise.test.ts @@ -138,7 +138,7 @@ describe("Abilities - Disguise", () => { }); await game.classicMode.startBattle([ Species.FURRET, Species.MIMIKYU ]); - const mimikyu = game.scene.getParty()[1]!; + const mimikyu = game.scene.getPlayerParty()[1]!; expect(mimikyu.formIndex).toBe(bustedForm); game.move.select(Moves.SPLASH); diff --git a/src/test/abilities/forecast.test.ts b/src/test/abilities/forecast.test.ts index 6da31307789..18d43a67a9d 100644 --- a/src/test/abilities/forecast.test.ts +++ b/src/test/abilities/forecast.test.ts @@ -81,7 +81,7 @@ describe("Abilities - Forecast", () => { }); await game.startBattle([ Species.CASTFORM, Species.FEEBAS, Species.KYOGRE, Species.GROUDON, Species.RAYQUAZA, Species.ALTARIA ]); - vi.spyOn(game.scene.getParty()[5], "getAbility").mockReturnValue(allAbilities[Abilities.CLOUD_NINE]); + vi.spyOn(game.scene.getPlayerParty()[5], "getAbility").mockReturnValue(allAbilities[Abilities.CLOUD_NINE]); const castform = game.scene.getPlayerField()[0]; expect(castform.formIndex).toBe(NORMAL_FORM); diff --git a/src/test/abilities/ice_face.test.ts b/src/test/abilities/ice_face.test.ts index 1c7f7bd6093..e31bee1c721 100644 --- a/src/test/abilities/ice_face.test.ts +++ b/src/test/abilities/ice_face.test.ts @@ -192,7 +192,7 @@ describe("Abilities - Ice Face", () => { game.doSwitchPokemon(1); await game.phaseInterceptor.to(TurnEndPhase); - eiscue = game.scene.getParty()[1]; + eiscue = game.scene.getPlayerParty()[1]; expect(eiscue.formIndex).toBe(noiceForm); expect(eiscue.getTag(BattlerTagType.ICE_FACE)).toBeUndefined(); diff --git a/src/test/abilities/mimicry.test.ts b/src/test/abilities/mimicry.test.ts index 4e62ddc0b76..8f22de33061 100644 --- a/src/test/abilities/mimicry.test.ts +++ b/src/test/abilities/mimicry.test.ts @@ -35,7 +35,7 @@ describe("Abilities - Mimicry", () => { game.override.enemyAbility(Abilities.MISTY_SURGE); await game.classicMode.startBattle([ Species.FEEBAS, Species.ABRA ]); - const [ playerPokemon1, playerPokemon2 ] = game.scene.getParty(); + const [ playerPokemon1, playerPokemon2 ] = game.scene.getPlayerParty(); game.move.select(Moves.SPLASH); await game.toNextTurn(); expect(playerPokemon1.getTypes().includes(Type.FAIRY)).toBe(true); diff --git a/src/test/abilities/pastel_veil.test.ts b/src/test/abilities/pastel_veil.test.ts index 660ff2616e9..6f09fd8ff06 100644 --- a/src/test/abilities/pastel_veil.test.ts +++ b/src/test/abilities/pastel_veil.test.ts @@ -51,7 +51,7 @@ describe("Abilities - Pastel Veil", () => { it("it heals the poisoned status condition of allies if user is sent out into battle", async () => { await game.startBattle([ Species.MAGIKARP, Species.FEEBAS, Species.GALAR_PONYTA ]); - const ponyta = game.scene.getParty()[2]; + const ponyta = game.scene.getPlayerParty()[2]; const magikarp = game.scene.getPlayerField()[0]; ponyta.abilityIndex = 1; diff --git a/src/test/abilities/power_construct.test.ts b/src/test/abilities/power_construct.test.ts index 1a9e7d4818a..bb80e9f3ac8 100644 --- a/src/test/abilities/power_construct.test.ts +++ b/src/test/abilities/power_construct.test.ts @@ -43,7 +43,7 @@ describe("Abilities - POWER CONSTRUCT", () => { await game.classicMode.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]); - const zygarde = game.scene.getParty().find((p) => p.species.speciesId === Species.ZYGARDE); + const zygarde = game.scene.getPlayerParty().find((p) => p.species.speciesId === Species.ZYGARDE); expect(zygarde).not.toBe(undefined); expect(zygarde!.formIndex).toBe(completeForm); @@ -73,7 +73,7 @@ describe("Abilities - POWER CONSTRUCT", () => { await game.classicMode.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]); - const zygarde = game.scene.getParty().find((p) => p.species.speciesId === Species.ZYGARDE); + const zygarde = game.scene.getPlayerParty().find((p) => p.species.speciesId === Species.ZYGARDE); expect(zygarde).not.toBe(undefined); expect(zygarde!.formIndex).toBe(completeForm); diff --git a/src/test/abilities/schooling.test.ts b/src/test/abilities/schooling.test.ts index aacc7bbd4c2..5f953dbf9ab 100644 --- a/src/test/abilities/schooling.test.ts +++ b/src/test/abilities/schooling.test.ts @@ -43,7 +43,7 @@ describe("Abilities - SCHOOLING", () => { await game.startBattle([ Species.MAGIKARP, Species.WISHIWASHI ]); - const wishiwashi = game.scene.getParty().find((p) => p.species.speciesId === Species.WISHIWASHI)!; + const wishiwashi = game.scene.getPlayerParty().find((p) => p.species.speciesId === Species.WISHIWASHI)!; expect(wishiwashi).not.toBe(undefined); expect(wishiwashi.formIndex).toBe(schoolForm); diff --git a/src/test/abilities/serene_grace.test.ts b/src/test/abilities/serene_grace.test.ts index ddca87496e9..3318c7fc27a 100644 --- a/src/test/abilities/serene_grace.test.ts +++ b/src/test/abilities/serene_grace.test.ts @@ -43,7 +43,7 @@ describe("Abilities - Serene Grace", () => { game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + expect(game.scene.getPlayerParty()[0].formIndex).toBe(0); game.move.select(moveToUse); @@ -70,7 +70,7 @@ describe("Abilities - Serene Grace", () => { ]); game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + expect(game.scene.getPlayerParty()[0].formIndex).toBe(0); game.move.select(moveToUse); diff --git a/src/test/abilities/sheer_force.test.ts b/src/test/abilities/sheer_force.test.ts index 63c81e9aafc..826694752b7 100644 --- a/src/test/abilities/sheer_force.test.ts +++ b/src/test/abilities/sheer_force.test.ts @@ -1,17 +1,16 @@ import { BattlerIndex } from "#app/battle"; import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability"; -import { Stat } from "#enums/stat"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import * as Utils from "#app/utils"; +import { NumberHolder } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { allMoves } from "#app/data/move"; - describe("Abilities - Sheer Force", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -39,13 +38,10 @@ describe("Abilities - Sheer Force", () => { it("Sheer Force", async () => { const moveToUse = Moves.AIR_SLASH; game.override.ability(Abilities.SHEER_FORCE); - await game.startBattle([ - Species.PIDGEOT - ]); + await game.classicMode.startBattle([ Species.PIDGEOT ]); - - game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + game.scene.getEnemyPokemon()!.stats[Stat.SPDEF] = 10000; + expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); game.move.select(moveToUse); @@ -57,8 +53,8 @@ describe("Abilities - Sheer Force", () => { expect(move.id).toBe(Moves.AIR_SLASH); //Verify the move is boosted and has no chance of secondary effects - const power = new Utils.IntegerHolder(move.power); - const chance = new Utils.IntegerHolder(move.chance); + const power = new NumberHolder(move.power); + const chance = new NumberHolder(move.chance); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); @@ -72,13 +68,11 @@ describe("Abilities - Sheer Force", () => { it("Sheer Force with exceptions including binding moves", async () => { const moveToUse = Moves.BIND; game.override.ability(Abilities.SHEER_FORCE); - await game.startBattle([ - Species.PIDGEOT - ]); + await game.classicMode.startBattle([ Species.PIDGEOT ]); - game.scene.getEnemyParty()[0].stats[Stat.DEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; + expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); game.move.select(moveToUse); @@ -90,8 +84,8 @@ describe("Abilities - Sheer Force", () => { expect(move.id).toBe(Moves.BIND); //Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1 - const power = new Utils.IntegerHolder(move.power); - const chance = new Utils.IntegerHolder(move.chance); + const power = new NumberHolder(move.power); + const chance = new NumberHolder(move.chance); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); @@ -105,13 +99,11 @@ describe("Abilities - Sheer Force", () => { it("Sheer Force with moves with no secondary effect", async () => { const moveToUse = Moves.TACKLE; game.override.ability(Abilities.SHEER_FORCE); - await game.startBattle([ - Species.PIDGEOT - ]); + await game.classicMode.startBattle([ Species.PIDGEOT ]); - game.scene.getEnemyParty()[0].stats[Stat.DEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; + expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); game.move.select(moveToUse); @@ -123,8 +115,8 @@ describe("Abilities - Sheer Force", () => { expect(move.id).toBe(Moves.TACKLE); //Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1 - const power = new Utils.IntegerHolder(move.power); - const chance = new Utils.IntegerHolder(move.chance); + const power = new NumberHolder(move.power); + const chance = new NumberHolder(move.chance); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); @@ -140,13 +132,11 @@ describe("Abilities - Sheer Force", () => { game.override.enemyAbility(Abilities.COLOR_CHANGE); game.override.startingHeldItems([{ name: "KINGS_ROCK", count: 1 }]); game.override.ability(Abilities.SHEER_FORCE); - await game.startBattle([ - Species.PIDGEOT - ]); + await game.startBattle([ Species.PIDGEOT ]); - game.scene.getEnemyParty()[0].stats[Stat.DEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; + expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); game.move.select(moveToUse); @@ -158,8 +148,8 @@ describe("Abilities - Sheer Force", () => { expect(move.id).toBe(Moves.CRUSH_CLAW); //Disable color change due to being hit by Sheer Force - const power = new Utils.IntegerHolder(move.power); - const chance = new Utils.IntegerHolder(move.chance); + const power = new NumberHolder(move.power); + const chance = new NumberHolder(move.chance); const user = phase.getUserPokemon()!; const target = phase.getFirstTarget()!; const opponentType = target.getTypes()[0]; @@ -186,7 +176,7 @@ describe("Abilities - Sheer Force", () => { Species.PIDGEOT ]); - const pidgeot = game.scene.getParty()[0]; + const pidgeot = game.scene.getPlayerParty()[0]; const onix = game.scene.getEnemyParty()[0]; pidgeot.stats[Stat.DEF] = 10000; diff --git a/src/test/abilities/shield_dust.test.ts b/src/test/abilities/shield_dust.test.ts index ccedf6873d7..9f1e6aeb11d 100644 --- a/src/test/abilities/shield_dust.test.ts +++ b/src/test/abilities/shield_dust.test.ts @@ -1,11 +1,11 @@ import { BattlerIndex } from "#app/battle"; import { applyAbAttrs, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability"; -import { Stat } from "#enums/stat"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import * as Utils from "#app/utils"; +import { NumberHolder } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -27,26 +27,22 @@ describe("Abilities - Shield Dust", () => { beforeEach(() => { game = new GameManager(phaserGame); - const movesToUse = [ Moves.AIR_SLASH ]; game.override.battleType("single"); game.override.enemySpecies(Species.ONIX); game.override.enemyAbility(Abilities.SHIELD_DUST); game.override.startingLevel(100); - game.override.moveset(movesToUse); - game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]); + game.override.moveset(Moves.AIR_SLASH); + game.override.enemyMoveset(Moves.TACKLE); }); it("Shield Dust", async () => { - const moveToUse = Moves.AIR_SLASH; - await game.startBattle([ - Species.PIDGEOT - ]); + await game.classicMode.startBattle([ Species.PIDGEOT ]); - game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + game.scene.getEnemyPokemon()!.stats[Stat.SPDEF] = 10000; + expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); - game.move.select(moveToUse); + game.move.select(Moves.AIR_SLASH); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.phaseInterceptor.to(MoveEffectPhase, false); @@ -56,7 +52,7 @@ describe("Abilities - Shield Dust", () => { const move = phase.move.getMove(); expect(move.id).toBe(Moves.AIR_SLASH); - const chance = new Utils.IntegerHolder(move.chance); + 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); expect(chance.value).toBe(0); diff --git a/src/test/abilities/shields_down.test.ts b/src/test/abilities/shields_down.test.ts index d3d72ac80a5..fbb2e96e463 100644 --- a/src/test/abilities/shields_down.test.ts +++ b/src/test/abilities/shields_down.test.ts @@ -43,7 +43,7 @@ describe("Abilities - SHIELDS DOWN", () => { await game.startBattle([ Species.MAGIKARP, Species.MINIOR ]); - const minior = game.scene.getParty().find((p) => p.species.speciesId === Species.MINIOR)!; + const minior = game.scene.getPlayerParty().find((p) => p.species.speciesId === Species.MINIOR)!; expect(minior).not.toBe(undefined); expect(minior.formIndex).toBe(coreForm); diff --git a/src/test/abilities/synchronize.test.ts b/src/test/abilities/synchronize.test.ts index cdd2834f588..d34b5631271 100644 --- a/src/test/abilities/synchronize.test.ts +++ b/src/test/abilities/synchronize.test.ts @@ -1,8 +1,8 @@ import { StatusEffect } from "#app/data/status-effect"; -import GameManager from "#app/test/utils/gameManager"; 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"; @@ -30,7 +30,7 @@ describe("Abilities - Synchronize", () => { .enemyAbility(Abilities.SYNCHRONIZE) .moveset([ Moves.SPLASH, Moves.THUNDER_WAVE, Moves.SPORE, Moves.PSYCHO_SHIFT ]) .ability(Abilities.NO_GUARD); - }, 20000); + }); it("does not trigger when no status is applied by opponent Pokemon", async () => { await game.classicMode.startBattle([ Species.FEEBAS ]); @@ -38,9 +38,9 @@ describe("Abilities - Synchronize", () => { game.move.select(Moves.SPLASH); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getParty()[0].status).toBeUndefined(); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); - }, 20000); + }); it("sets the status of the source pokemon to Paralysis when paralyzed by it", async () => { await game.classicMode.startBattle([ Species.FEEBAS ]); @@ -48,10 +48,10 @@ describe("Abilities - Synchronize", () => { game.move.select(Moves.THUNDER_WAVE); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); - expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); - }, 20000); + }); it("does not trigger on Sleep", async () => { await game.classicMode.startBattle(); @@ -60,10 +60,10 @@ describe("Abilities - Synchronize", () => { await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getParty()[0].status?.effect).toBeUndefined(); - expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.SLEEP); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBeUndefined(); + expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.SLEEP); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); - }, 20000); + }); it("does not trigger when Pokemon is statused by Toxic Spikes", async () => { game.override @@ -79,10 +79,10 @@ describe("Abilities - Synchronize", () => { game.doSwitchPokemon(1); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.POISON); - expect(game.scene.getEnemyParty()[0].status?.effect).toBeUndefined(); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.POISON); + expect(game.scene.getEnemyPokemon()!.status?.effect).toBeUndefined(); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); - }, 20000); + }); it("shows ability even if it fails to set the status of the opponent Pokemon", async () => { await game.classicMode.startBattle([ Species.PIKACHU ]); @@ -90,10 +90,10 @@ describe("Abilities - Synchronize", () => { game.move.select(Moves.THUNDER_WAVE); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getParty()[0].status?.effect).toBeUndefined(); - expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBeUndefined(); + expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); - }, 20000); + }); it("should activate with Psycho Shift after the move clears the status", async () => { game.override.statusEffect(StatusEffect.PARALYSIS); @@ -102,8 +102,8 @@ describe("Abilities - Synchronize", () => { game.move.select(Moves.PSYCHO_SHIFT); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise - expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise + expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); - }, 20000); + }); }); diff --git a/src/test/abilities/wimp_out.test.ts b/src/test/abilities/wimp_out.test.ts index ee943befc16..6f56a2f4e7e 100644 --- a/src/test/abilities/wimp_out.test.ts +++ b/src/test/abilities/wimp_out.test.ts @@ -42,7 +42,7 @@ describe("Abilities - Wimp Out", () => { }); function confirmSwitch(): void { - const [ pokemon1, pokemon2 ] = game.scene.getParty(); + const [ pokemon1, pokemon2 ] = game.scene.getPlayerParty(); expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); @@ -54,7 +54,7 @@ describe("Abilities - Wimp Out", () => { } function confirmNoSwitch(): void { - const [ pokemon1, pokemon2 ] = game.scene.getParty(); + const [ pokemon1, pokemon2 ] = game.scene.getPlayerParty(); expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); @@ -135,7 +135,7 @@ describe("Abilities - Wimp Out", () => { expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); expect(game.scene.getPlayerPokemon()!.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); - expect(game.scene.getParty()[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined(); + expect(game.scene.getPlayerParty()[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined(); confirmSwitch(); }); @@ -263,7 +263,7 @@ describe("Abilities - Wimp Out", () => { game.doSelectPartyPokemon(1); await game.phaseInterceptor.to("TurnEndPhase"); - expect(game.scene.getParty()[1].getHpRatio()).toBeGreaterThan(0.5); + expect(game.scene.getPlayerParty()[1].getHpRatio()).toBeGreaterThan(0.5); expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT); }); @@ -424,7 +424,7 @@ describe("Abilities - Wimp Out", () => { game.move.select(Moves.SPLASH); await game.phaseInterceptor.to("TurnEndPhase"); - expect(game.scene.getParty()[0].getHpRatio()).toEqual(0.51); + expect(game.scene.getPlayerParty()[0].getHpRatio()).toEqual(0.51); expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD); }); @@ -522,7 +522,7 @@ describe("Abilities - Wimp Out", () => { game.doSelectPartyPokemon(1); await game.phaseInterceptor.to("TurnEndPhase"); - expect(game.scene.getParty()[1].status?.effect).toEqual(StatusEffect.POISON); + expect(game.scene.getPlayerParty()[1].status?.effect).toEqual(StatusEffect.POISON); confirmSwitch(); }); diff --git a/src/test/abilities/zen_mode.test.ts b/src/test/abilities/zen_mode.test.ts index 2601954a9a4..74575415788 100644 --- a/src/test/abilities/zen_mode.test.ts +++ b/src/test/abilities/zen_mode.test.ts @@ -1,29 +1,23 @@ -import { Stat } from "#enums/stat"; import { BattlerIndex } from "#app/battle"; +import { Status, StatusEffect } from "#app/data/status-effect"; import { DamagePhase } from "#app/phases/damage-phase"; -import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; -import { MessagePhase } from "#app/phases/message-phase"; -import { PostSummonPhase } from "#app/phases/post-summon-phase"; -import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; -import { SwitchPhase } from "#app/phases/switch-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; -import { TurnStartPhase } from "#app/phases/turn-start-phase"; import { Mode } from "#app/ui/ui"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import { SwitchType } from "#enums/switch-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -import { Status, StatusEffect } from "#app/data/status-effect"; -import { SwitchType } from "#enums/switch-type"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Abilities - ZEN MODE", () => { let phaserGame: Phaser.Game; let game: GameManager; + const baseForm = 0; + const zenForm = 1; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -37,121 +31,104 @@ describe("Abilities - ZEN MODE", () => { beforeEach(() => { game = new GameManager(phaserGame); - const moveToUse = Moves.SPLASH; - game.override.battleType("single"); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(Abilities.HYDRATION); - game.override.ability(Abilities.ZEN_MODE); - game.override.startingLevel(100); - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]); + game.override + .battleType("single") + .enemySpecies(Species.RATTATA) + .enemyAbility(Abilities.HYDRATION) + .ability(Abilities.ZEN_MODE) + .startingLevel(100) + .moveset(Moves.SPLASH) + .enemyMoveset(Moves.TACKLE); }); - test( - "not enough damage to change form", - async () => { - const moveToUse = Moves.SPLASH; - await game.startBattle([ Species.DARMANITAN ]); - game.scene.getParty()[0].stats[Stat.HP] = 100; - game.scene.getParty()[0].hp = 100; - expect(game.scene.getParty()[0].formIndex).toBe(0); + it("shouldn't change form when taking damage if not dropping below 50% HP", async () => { + await game.classicMode.startBattle([ Species.DARMANITAN ]); + const player = game.scene.getPlayerPokemon()!; + player.stats[Stat.HP] = 100; + player.hp = 100; + expect(player.formIndex).toBe(baseForm); - game.move.select(moveToUse); + game.move.select(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to(DamagePhase, false); - // await game.phaseInterceptor.runFrom(DamagePhase).to(DamagePhase, false); - const damagePhase = game.scene.getCurrentPhase() as DamagePhase; - damagePhase.updateAmount(40); - await game.phaseInterceptor.runFrom(DamagePhase).to(TurnEndPhase, false); - expect(game.scene.getParty()[0].hp).toBeLessThan(100); - expect(game.scene.getParty()[0].formIndex).toBe(0); - }, - ); + expect(player.hp).toBeLessThan(100); + expect(player.formIndex).toBe(baseForm); + }); - test( - "enough damage to change form", - async () => { - const moveToUse = Moves.SPLASH; - await game.startBattle([ Species.DARMANITAN ]); - game.scene.getParty()[0].stats[Stat.HP] = 1000; - game.scene.getParty()[0].hp = 100; - expect(game.scene.getParty()[0].formIndex).toBe(0); + it("should change form when falling below 50% HP", async () => { + await game.classicMode.startBattle([ Species.DARMANITAN ]); - game.move.select(moveToUse); + const player = game.scene.getPlayerPokemon()!; + player.stats[Stat.HP] = 1000; + player.hp = 100; + expect(player.formIndex).toBe(baseForm); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to(QuietFormChangePhase); - await game.phaseInterceptor.to(TurnInitPhase, false); - expect(game.scene.getParty()[0].hp).not.toBe(100); - expect(game.scene.getParty()[0].formIndex).not.toBe(0); - }, - ); + game.move.select(Moves.SPLASH); - test( - "kill pokemon while on zen mode", - async () => { - const moveToUse = Moves.SPLASH; - await game.startBattle([ Species.DARMANITAN, Species.CHARIZARD ]); - game.scene.getParty()[0].stats[Stat.HP] = 1000; - game.scene.getParty()[0].hp = 100; - expect(game.scene.getParty()[0].formIndex).toBe(0); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("QuietFormChangePhase"); + await game.phaseInterceptor.to("TurnInitPhase", false); - game.move.select(moveToUse); + expect(player.hp).not.toBe(100); + expect(player.formIndex).toBe(zenForm); + }); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to(DamagePhase, false); - // await game.phaseInterceptor.runFrom(DamagePhase).to(DamagePhase, false); - const damagePhase = game.scene.getCurrentPhase() as DamagePhase; - damagePhase.updateAmount(80); - await game.phaseInterceptor.runFrom(DamagePhase).to(QuietFormChangePhase); - expect(game.scene.getParty()[0].hp).not.toBe(100); - expect(game.scene.getParty()[0].formIndex).not.toBe(0); - await game.killPokemon(game.scene.getParty()[0]); - expect(game.scene.getParty()[0].isFainted()).toBe(true); - await game.phaseInterceptor.run(MessagePhase); - await game.phaseInterceptor.run(EnemyCommandPhase); - await game.phaseInterceptor.run(TurnStartPhase); - game.onNextPrompt("SwitchPhase", Mode.PARTY, () => { - game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, SwitchType.SWITCH, 0, 1, false)); - game.scene.ui.setMode(Mode.MESSAGE); - }); - game.onNextPrompt("SwitchPhase", Mode.MESSAGE, () => { - game.endPhase(); - }); - await game.phaseInterceptor.run(SwitchPhase); - await game.phaseInterceptor.to(PostSummonPhase); - expect(game.scene.getParty()[1].formIndex).toBe(1); - }, - ); + it("should stay zen mode when fainted", async () => { + await game.classicMode.startBattle([ Species.DARMANITAN, Species.CHARIZARD ]); + const player = game.scene.getPlayerPokemon()!; + player.stats[Stat.HP] = 1000; + player.hp = 100; + expect(player.formIndex).toBe(baseForm); - test( - "check if fainted pokemon switches to base form on arena reset", - async () => { - const baseForm = 0, - zenForm = 1; - game.override.startingWave(4); - game.override.starterForms({ - [Species.DARMANITAN]: zenForm, - }); + game.move.select(Moves.SPLASH); - await game.startBattle([ Species.MAGIKARP, Species.DARMANITAN ]); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to(DamagePhase, false); + const damagePhase = game.scene.getCurrentPhase() as DamagePhase; + damagePhase.updateAmount(80); + await game.phaseInterceptor.to("QuietFormChangePhase"); - const darmanitan = game.scene.getParty().find((p) => p.species.speciesId === Species.DARMANITAN)!; - expect(darmanitan).not.toBe(undefined); - expect(darmanitan.formIndex).toBe(zenForm); + expect(player.hp).not.toBe(100); + expect(player.formIndex).toBe(zenForm); - darmanitan.hp = 0; - darmanitan.status = new Status(StatusEffect.FAINT); - expect(darmanitan.isFainted()).toBe(true); + await game.killPokemon(player); + expect(player.isFainted()).toBe(true); - game.move.select(Moves.SPLASH); - await game.doKillOpponents(); - await game.phaseInterceptor.to(TurnEndPhase); - game.doSelectModifier(); - await game.phaseInterceptor.to(QuietFormChangePhase); + await game.phaseInterceptor.to("TurnStartPhase"); + game.onNextPrompt("SwitchPhase", Mode.PARTY, () => { + game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, SwitchType.SWITCH, 0, 1, false)); + game.scene.ui.setMode(Mode.MESSAGE); + }); + game.onNextPrompt("SwitchPhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + await game.phaseInterceptor.to("PostSummonPhase"); - expect(darmanitan.formIndex).toBe(baseForm); - }, - ); + expect(game.scene.getPlayerParty()[1].formIndex).toBe(zenForm); + }); + + it("should switch to base form on arena reset", async () => { + game.override.startingWave(4); + game.override.starterForms({ + [Species.DARMANITAN]: zenForm, + }); + + await game.classicMode.startBattle([ Species.MAGIKARP, Species.DARMANITAN ]); + + const darmanitan = game.scene.getPlayerParty().find((p) => p.species.speciesId === Species.DARMANITAN)!; + expect(darmanitan.formIndex).toBe(zenForm); + + darmanitan.hp = 0; + darmanitan.status = new Status(StatusEffect.FAINT); + expect(darmanitan.isFainted()).toBe(true); + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.phaseInterceptor.to("TurnEndPhase"); + game.doSelectModifier(); + await game.phaseInterceptor.to("QuietFormChangePhase"); + + expect(darmanitan.formIndex).toBe(baseForm); + }); }); diff --git a/src/test/abilities/zero_to_hero.test.ts b/src/test/abilities/zero_to_hero.test.ts index 48a451e99a2..160b43abf1e 100644 --- a/src/test/abilities/zero_to_hero.test.ts +++ b/src/test/abilities/zero_to_hero.test.ts @@ -41,8 +41,8 @@ describe("Abilities - ZERO TO HERO", () => { await game.startBattle([ Species.FEEBAS, Species.PALAFIN, Species.PALAFIN ]); - const palafin1 = game.scene.getParty()[1]; - const palafin2 = game.scene.getParty()[2]; + const palafin1 = game.scene.getPlayerParty()[1]; + const palafin2 = game.scene.getPlayerParty()[2]; expect(palafin1.formIndex).toBe(heroForm); expect(palafin2.formIndex).toBe(heroForm); palafin2.hp = 0; diff --git a/src/test/battle/battle.test.ts b/src/test/battle/battle.test.ts index eed76397f85..656cc62ac59 100644 --- a/src/test/battle/battle.test.ts +++ b/src/test/battle/battle.test.ts @@ -136,9 +136,9 @@ describe("Test Battle Phase", () => { Species.CHANSEY, Species.MEW ]); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.CHARIZARD); - expect(game.scene.getParty()[1].species.speciesId).toBe(Species.CHANSEY); - expect(game.scene.getParty()[2].species.speciesId).toBe(Species.MEW); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.CHARIZARD); + expect(game.scene.getPlayerParty()[1].species.speciesId).toBe(Species.CHANSEY); + expect(game.scene.getPlayerParty()[2].species.speciesId).toBe(Species.MEW); }, 20000); it("test remove random battle seed int", async () => { diff --git a/src/test/battle/double_battle.test.ts b/src/test/battle/double_battle.test.ts index b7a5616d642..a7585d55bab 100644 --- a/src/test/battle/double_battle.test.ts +++ b/src/test/battle/double_battle.test.ts @@ -49,7 +49,7 @@ describe("Double Battles", () => { await game.phaseInterceptor.to(BattleEndPhase); game.doSelectModifier(); - const charizard = game.scene.getParty().findIndex(p => p.species.speciesId === Species.CHARIZARD); + const charizard = game.scene.getPlayerParty().findIndex(p => p.species.speciesId === Species.CHARIZARD); game.doRevivePokemon(charizard); await game.phaseInterceptor.to(TurnInitPhase); diff --git a/src/test/daily_mode.test.ts b/src/test/daily_mode.test.ts index 100cf07f9c0..2a88ce10ae7 100644 --- a/src/test/daily_mode.test.ts +++ b/src/test/daily_mode.test.ts @@ -30,7 +30,7 @@ describe("Daily Mode", () => { it("should initialize properly", async () => { await game.dailyMode.runToSummon(); - const party = game.scene.getParty(); + const party = game.scene.getPlayerParty(); expect(party).toHaveLength(3); party.forEach(pkm => { expect(pkm.level).toBe(20); diff --git a/src/test/evolution.test.ts b/src/test/evolution.test.ts index b94d45d6537..3046d103cbc 100644 --- a/src/test/evolution.test.ts +++ b/src/test/evolution.test.ts @@ -35,8 +35,8 @@ describe("Evolution", () => { it("should keep hidden ability after evolving", async () => { await game.classicMode.runToSummon([ Species.EEVEE, Species.TRAPINCH ]); - const eevee = game.scene.getParty()[0]; - const trapinch = game.scene.getParty()[1]; + const eevee = game.scene.getPlayerParty()[0]; + const trapinch = game.scene.getPlayerParty()[1]; eevee.abilityIndex = 2; trapinch.abilityIndex = 2; @@ -50,8 +50,8 @@ describe("Evolution", () => { it("should keep same ability slot after evolving", async () => { await game.classicMode.runToSummon([ Species.BULBASAUR, Species.CHARMANDER ]); - const bulbasaur = game.scene.getParty()[0]; - const charmander = game.scene.getParty()[1]; + const bulbasaur = game.scene.getPlayerParty()[0]; + const charmander = game.scene.getPlayerParty()[1]; bulbasaur.abilityIndex = 0; charmander.abilityIndex = 1; @@ -80,8 +80,8 @@ describe("Evolution", () => { nincada.metBiome = -1; nincada.evolve(pokemonEvolutions[Species.NINCADA][0], nincada.getSpeciesForm()); - const ninjask = game.scene.getParty()[0]; - const shedinja = game.scene.getParty()[1]; + const ninjask = game.scene.getPlayerParty()[0]; + const shedinja = game.scene.getPlayerParty()[1]; expect(ninjask.abilityIndex).toBe(2); expect(shedinja.abilityIndex).toBe(1); // Regression test for https://github.com/pagefaultgames/pokerogue/issues/3842 diff --git a/src/test/field/pokemon.test.ts b/src/test/field/pokemon.test.ts index aeaecab5874..ce9862e22e4 100644 --- a/src/test/field/pokemon.test.ts +++ b/src/test/field/pokemon.test.ts @@ -45,7 +45,7 @@ describe("Spec - Pokemon", () => { const zubat = scene.getEnemyPokemon()!; zubat.addToParty(PokeballType.LUXURY_BALL); - const party = scene.getParty(); + const party = scene.getPlayerParty(); expect(party).toHaveLength(6); party.forEach((pkm, index) =>{ expect(pkm.species.speciesId).toBe(index === 5 ? Species.ZUBAT : Species.ABRA); @@ -57,7 +57,7 @@ describe("Spec - Pokemon", () => { const zubat = scene.getEnemyPokemon()!; zubat.addToParty(PokeballType.LUXURY_BALL, slotIndex); - const party = scene.getParty(); + const party = scene.getPlayerParty(); expect(party).toHaveLength(6); party.forEach((pkm, index) =>{ expect(pkm.species.speciesId).toBe(index === slotIndex ? Species.ZUBAT : Species.ABRA); diff --git a/src/test/items/eviolite.test.ts b/src/test/items/eviolite.test.ts index 7b2f9a15fce..a97c287da29 100644 --- a/src/test/items/eviolite.test.ts +++ b/src/test/items/eviolite.test.ts @@ -1,10 +1,10 @@ -import { Stat } from "#enums/stat"; +import { StatBoosterModifier } from "#app/modifier/modifier"; +import { NumberHolder, randItem } from "#app/utils"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import Phase from "phaser"; -import * as Utils from "#app/utils"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { StatBoosterModifier } from "#app/modifier/modifier"; describe("Items - Eviolite", () => { let phaserGame: Phaser.Game; @@ -28,14 +28,12 @@ describe("Items - Eviolite", () => { }); it("should provide 50% boost to DEF and SPDEF for unevolved, unfused pokemon", async() => { - await game.classicMode.startBattle([ - Species.PICHU - ]); + await game.classicMode.startBattle([ Species.PICHU ]); const partyMember = game.scene.getPlayerPokemon()!; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity @@ -51,14 +49,12 @@ describe("Items - Eviolite", () => { }); it("should not provide a boost for fully evolved, unfused pokemon", async() => { - await game.classicMode.startBattle([ - Species.RAICHU, - ]); + await game.classicMode.startBattle([ Species.RAICHU ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerPokemon()!; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity @@ -75,12 +71,9 @@ describe("Items - Eviolite", () => { }); it("should provide 50% boost to DEF and SPDEF for completely unevolved, fused pokemon", async() => { - await game.classicMode.startBattle([ - Species.PICHU, - Species.CLEFFA - ]); + await game.classicMode.startBattle([ Species.PICHU, Species.CLEFFA ]); - const [ partyMember, ally ] = game.scene.getParty(); + const [ partyMember, ally ] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -92,7 +85,7 @@ describe("Items - Eviolite", () => { partyMember.fusionLuck = ally.luck; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity @@ -108,12 +101,9 @@ describe("Items - Eviolite", () => { }); it("should provide 25% boost to DEF and SPDEF for partially unevolved (base), fused pokemon", async() => { - await game.classicMode.startBattle([ - Species.PICHU, - Species.CLEFABLE - ]); + await game.classicMode.startBattle([ Species.PICHU, Species.CLEFABLE ]); - const [ partyMember, ally ] = game.scene.getParty(); + const [ partyMember, ally ] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -125,7 +115,7 @@ describe("Items - Eviolite", () => { partyMember.fusionLuck = ally.luck; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity @@ -141,12 +131,9 @@ describe("Items - Eviolite", () => { }); it("should provide 25% boost to DEF and SPDEF for partially unevolved (fusion), fused pokemon", async() => { - await game.classicMode.startBattle([ - Species.RAICHU, - Species.CLEFFA - ]); + await game.classicMode.startBattle([ Species.RAICHU, Species.CLEFFA ]); - const [ partyMember, ally ] = game.scene.getParty(); + const [ partyMember, ally ] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -158,7 +145,7 @@ describe("Items - Eviolite", () => { partyMember.fusionLuck = ally.luck; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity @@ -174,12 +161,9 @@ describe("Items - Eviolite", () => { }); it("should not provide a boost for fully evolved, fused pokemon", async() => { - await game.classicMode.startBattle([ - Species.RAICHU, - Species.CLEFABLE - ]); + await game.classicMode.startBattle([ Species.RAICHU, Species.CLEFABLE ]); - const [ partyMember, ally ] = game.scene.getParty(); + const [ partyMember, ally ] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -191,7 +175,7 @@ describe("Items - Eviolite", () => { partyMember.fusionLuck = ally.luck; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity @@ -216,14 +200,12 @@ describe("Items - Eviolite", () => { const gMaxablePokemon = [ Species.PIKACHU, Species.EEVEE, Species.DURALUDON, Species.MEOWTH ]; - await game.classicMode.startBattle([ - Utils.randItem(gMaxablePokemon) - ]); + await game.classicMode.startBattle([ randItem(gMaxablePokemon) ]); const partyMember = game.scene.getPlayerPokemon()!; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity diff --git a/src/test/items/leek.test.ts b/src/test/items/leek.test.ts index e27462a9265..901b353b3d3 100644 --- a/src/test/items/leek.test.ts +++ b/src/test/items/leek.test.ts @@ -89,7 +89,7 @@ describe("Items - Leek", () => { Species.PIKACHU, ]); - const [ partyMember, ally ] = game.scene.getParty(); + const [ partyMember, ally ] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -120,7 +120,7 @@ describe("Items - Leek", () => { species[Utils.randInt(species.length)] ]); - const [ partyMember, ally ] = game.scene.getParty(); + const [ partyMember, ally ] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; diff --git a/src/test/items/light_ball.test.ts b/src/test/items/light_ball.test.ts index 78375487f3b..fe79b6a2045 100644 --- a/src/test/items/light_ball.test.ts +++ b/src/test/items/light_ball.test.ts @@ -35,7 +35,7 @@ describe("Items - Light Ball", () => { Species.PIKACHU ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; // Checking console log to make sure Light Ball is applied when getEffectiveStat (with the appropriate stat) is called partyMember.getEffectiveStat(Stat.DEF); @@ -68,7 +68,7 @@ describe("Items - Light Ball", () => { Species.PIKACHU ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const atkStat = partyMember.getStat(Stat.ATK); const spAtkStat = partyMember.getStat(Stat.SPATK); @@ -97,8 +97,8 @@ describe("Items - Light Ball", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -136,8 +136,8 @@ describe("Items - Light Ball", () => { Species.PIKACHU ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -174,7 +174,7 @@ describe("Items - Light Ball", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const atkStat = partyMember.getStat(Stat.ATK); const spAtkStat = partyMember.getStat(Stat.SPATK); diff --git a/src/test/items/metal_powder.test.ts b/src/test/items/metal_powder.test.ts index c577182f350..86e7d329ecb 100644 --- a/src/test/items/metal_powder.test.ts +++ b/src/test/items/metal_powder.test.ts @@ -35,7 +35,7 @@ describe("Items - Metal Powder", () => { Species.DITTO ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; // Checking console log to make sure Metal Powder is applied when getEffectiveStat (with the appropriate stat) is called partyMember.getEffectiveStat(Stat.DEF); @@ -68,7 +68,7 @@ describe("Items - Metal Powder", () => { Species.DITTO ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const defStat = partyMember.getStat(Stat.DEF); @@ -91,8 +91,8 @@ describe("Items - Metal Powder", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -124,8 +124,8 @@ describe("Items - Metal Powder", () => { Species.DITTO ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -156,7 +156,7 @@ describe("Items - Metal Powder", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const defStat = partyMember.getStat(Stat.DEF); diff --git a/src/test/items/quick_powder.test.ts b/src/test/items/quick_powder.test.ts index 4eb6f6fb164..905d023ad8b 100644 --- a/src/test/items/quick_powder.test.ts +++ b/src/test/items/quick_powder.test.ts @@ -35,7 +35,7 @@ describe("Items - Quick Powder", () => { Species.DITTO ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; // Checking console log to make sure Quick Powder is applied when getEffectiveStat (with the appropriate stat) is called partyMember.getEffectiveStat(Stat.DEF); @@ -68,7 +68,7 @@ describe("Items - Quick Powder", () => { Species.DITTO ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const spdStat = partyMember.getStat(Stat.SPD); @@ -91,8 +91,8 @@ describe("Items - Quick Powder", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -124,8 +124,8 @@ describe("Items - Quick Powder", () => { Species.DITTO ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -156,7 +156,7 @@ describe("Items - Quick Powder", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const spdStat = partyMember.getStat(Stat.SPD); diff --git a/src/test/items/thick_club.test.ts b/src/test/items/thick_club.test.ts index 74158089c77..d349a1ad7b9 100644 --- a/src/test/items/thick_club.test.ts +++ b/src/test/items/thick_club.test.ts @@ -35,7 +35,7 @@ describe("Items - Thick Club", () => { Species.CUBONE ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; // Checking console log to make sure Thick Club is applied when getEffectiveStat (with the appropriate stat) is called partyMember.getEffectiveStat(Stat.DEF); @@ -68,7 +68,7 @@ describe("Items - Thick Club", () => { Species.CUBONE ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const atkStat = partyMember.getStat(Stat.ATK); @@ -90,7 +90,7 @@ describe("Items - Thick Club", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const atkStat = partyMember.getStat(Stat.ATK); @@ -112,7 +112,7 @@ describe("Items - Thick Club", () => { Species.ALOLA_MAROWAK ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const atkStat = partyMember.getStat(Stat.ATK); @@ -139,8 +139,8 @@ describe("Items - Thick Club", () => { Species.PIKACHU ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -176,8 +176,8 @@ describe("Items - Thick Club", () => { species[randSpecies] ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -208,7 +208,7 @@ describe("Items - Thick Club", () => { Species.PIKACHU ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const atkStat = partyMember.getStat(Stat.ATK); diff --git a/src/test/moves/aromatherapy.test.ts b/src/test/moves/aromatherapy.test.ts index f547ed0e54c..874dadc0a1f 100644 --- a/src/test/moves/aromatherapy.test.ts +++ b/src/test/moves/aromatherapy.test.ts @@ -33,7 +33,7 @@ describe("Moves - Aromatherapy", () => { it("should cure status effect of the user, its ally, and all party pokemon", async () => { await game.classicMode.startBattle([ Species.RATTATA, Species.RATTATA, Species.RATTATA ]); - const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getParty(); + const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getPlayerParty(); vi.spyOn(leftPlayer, "resetStatus"); vi.spyOn(rightPlayer, "resetStatus"); @@ -79,7 +79,7 @@ describe("Moves - Aromatherapy", () => { it("should not cure status effect of allies ON FIELD with Sap Sipper, should still cure allies in party", async () => { game.override.ability(Abilities.SAP_SIPPER); await game.classicMode.startBattle([ Species.RATTATA, Species.RATTATA, Species.RATTATA ]); - const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getParty(); + const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getPlayerParty(); vi.spyOn(leftPlayer, "resetStatus"); vi.spyOn(rightPlayer, "resetStatus"); diff --git a/src/test/moves/baton_pass.test.ts b/src/test/moves/baton_pass.test.ts index 9d4a9358715..52e4c3ec016 100644 --- a/src/test/moves/baton_pass.test.ts +++ b/src/test/moves/baton_pass.test.ts @@ -95,7 +95,7 @@ describe("Moves - Baton Pass", () => { game.override.enemyMoveset([ Moves.SALT_CURE ]); await game.classicMode.startBattle([ Species.PIKACHU, Species.FEEBAS ]); - const [ player1, player2 ] = game.scene.getParty(); + const [ player1, player2 ] = game.scene.getPlayerParty(); game.move.select(Moves.BATON_PASS); await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); diff --git a/src/test/moves/beat_up.test.ts b/src/test/moves/beat_up.test.ts index e51ac6ea452..a0129621f0e 100644 --- a/src/test/moves/beat_up.test.ts +++ b/src/test/moves/beat_up.test.ts @@ -65,7 +65,7 @@ describe("Moves - Beat Up", () => { const playerPokemon = game.scene.getPlayerPokemon()!; - game.scene.getParty()[1].trySetStatus(StatusEffect.BURN); + game.scene.getPlayerParty()[1].trySetStatus(StatusEffect.BURN); game.move.select(Moves.BEAT_UP); diff --git a/src/test/moves/dragon_rage.test.ts b/src/test/moves/dragon_rage.test.ts index dcbed7107e6..595f347a6b5 100644 --- a/src/test/moves/dragon_rage.test.ts +++ b/src/test/moves/dragon_rage.test.ts @@ -48,7 +48,7 @@ describe("Moves - Dragon Rage", () => { await game.startBattle(); - partyPokemon = game.scene.getParty()[0]; + partyPokemon = game.scene.getPlayerParty()[0]; enemyPokemon = game.scene.getEnemyPokemon()!; // remove berries diff --git a/src/test/moves/dragon_tail.test.ts b/src/test/moves/dragon_tail.test.ts index cf801eb42c1..6b3e669f770 100644 --- a/src/test/moves/dragon_tail.test.ts +++ b/src/test/moves/dragon_tail.test.ts @@ -73,7 +73,7 @@ describe("Moves - Dragon Tail", () => { .enemyAbility(Abilities.ROUGH_SKIN); await game.classicMode.startBattle([ Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD ]); - const leadPokemon = game.scene.getParty()[0]!; + const leadPokemon = game.scene.getPlayerParty()[0]!; const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; const enemySecPokemon = game.scene.getEnemyParty()[1]!; @@ -105,8 +105,8 @@ describe("Moves - Dragon Tail", () => { .enemyAbility(Abilities.ROUGH_SKIN); await game.classicMode.startBattle([ Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD ]); - const leadPokemon = game.scene.getParty()[0]!; - const secPokemon = game.scene.getParty()[1]!; + const leadPokemon = game.scene.getPlayerParty()[0]!; + const secPokemon = game.scene.getPlayerParty()[1]!; const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; const enemySecPokemon = game.scene.getEnemyParty()[1]!; diff --git a/src/test/moves/fissure.test.ts b/src/test/moves/fissure.test.ts index 16c3faa6827..12f075f1b55 100644 --- a/src/test/moves/fissure.test.ts +++ b/src/test/moves/fissure.test.ts @@ -43,7 +43,7 @@ describe("Moves - Fissure", () => { await game.startBattle(); - partyPokemon = game.scene.getParty()[0]; + partyPokemon = game.scene.getPlayerParty()[0]; enemyPokemon = game.scene.getEnemyPokemon()!; // remove berries diff --git a/src/test/moves/fusion_flare_bolt.test.ts b/src/test/moves/fusion_flare_bolt.test.ts index 0d9b9898276..1bcd0514357 100644 --- a/src/test/moves/fusion_flare_bolt.test.ts +++ b/src/test/moves/fusion_flare_bolt.test.ts @@ -176,7 +176,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { Species.ZEKROM ]); - const party = game.scene.getParty(); + const party = game.scene.getPlayerParty(); const enemyParty = game.scene.getEnemyParty(); // Get rid of any modifiers that may alter power @@ -235,7 +235,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { Species.ZEKROM ]); - const party = game.scene.getParty(); + const party = game.scene.getPlayerParty(); const enemyParty = game.scene.getEnemyParty(); // Get rid of any modifiers that may alter power diff --git a/src/test/moves/heal_bell.test.ts b/src/test/moves/heal_bell.test.ts index e4a019d9c0e..b180588d3a3 100644 --- a/src/test/moves/heal_bell.test.ts +++ b/src/test/moves/heal_bell.test.ts @@ -33,7 +33,7 @@ describe("Moves - Heal Bell", () => { it("should cure status effect of the user, its ally, and all party pokemon", async () => { await game.classicMode.startBattle([ Species.RATTATA, Species.RATTATA, Species.RATTATA ]); - const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getParty(); + const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getPlayerParty(); vi.spyOn(leftPlayer, "resetStatus"); vi.spyOn(rightPlayer, "resetStatus"); @@ -79,7 +79,7 @@ describe("Moves - Heal Bell", () => { it("should not cure status effect of allies ON FIELD with Soundproof, should still cure allies in party", async () => { game.override.ability(Abilities.SOUNDPROOF); await game.classicMode.startBattle([ Species.RATTATA, Species.RATTATA, Species.RATTATA ]); - const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getParty(); + const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getPlayerParty(); vi.spyOn(leftPlayer, "resetStatus"); vi.spyOn(rightPlayer, "resetStatus"); diff --git a/src/test/moves/magnet_rise.test.ts b/src/test/moves/magnet_rise.test.ts index 0afcad67ea3..b26bbf42ed0 100644 --- a/src/test/moves/magnet_rise.test.ts +++ b/src/test/moves/magnet_rise.test.ts @@ -35,10 +35,10 @@ describe("Moves - Magnet Rise", () => { it("MAGNET RISE", async () => { await game.startBattle(); - const startingHp = game.scene.getParty()[0].hp; + const startingHp = game.scene.getPlayerParty()[0].hp; game.move.select(moveToUse); await game.phaseInterceptor.to(TurnEndPhase); - const finalHp = game.scene.getParty()[0].hp; + const finalHp = game.scene.getPlayerParty()[0].hp; const hpLost = finalHp - startingHp; expect(hpLost).toBe(0); }, 20000); @@ -46,15 +46,15 @@ describe("Moves - Magnet Rise", () => { it("MAGNET RISE - Gravity", async () => { await game.startBattle(); - const startingHp = game.scene.getParty()[0].hp; + const startingHp = game.scene.getPlayerParty()[0].hp; game.move.select(moveToUse); await game.phaseInterceptor.to(CommandPhase); - let finalHp = game.scene.getParty()[0].hp; + let finalHp = game.scene.getPlayerParty()[0].hp; let hpLost = finalHp - startingHp; expect(hpLost).toBe(0); game.move.select(Moves.GRAVITY); await game.phaseInterceptor.to(TurnEndPhase); - finalHp = game.scene.getParty()[0].hp; + finalHp = game.scene.getPlayerParty()[0].hp; hpLost = finalHp - startingHp; expect(hpLost).not.toBe(0); }, 20000); diff --git a/src/test/moves/multi_target.test.ts b/src/test/moves/multi_target.test.ts index b6836b1dcb9..965876d3445 100644 --- a/src/test/moves/multi_target.test.ts +++ b/src/test/moves/multi_target.test.ts @@ -79,7 +79,7 @@ describe("Multi-target damage reduction", () => { it("should reduce earthquake when more than one pokemon other than user is not fainted", async () => { await game.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); - const player2 = game.scene.getParty()[1]; + const player2 = game.scene.getPlayerParty()[1]; const [ enemy1, enemy2 ] = game.scene.getEnemyField(); game.move.select(Moves.EARTHQUAKE); diff --git a/src/test/moves/parting_shot.test.ts b/src/test/moves/parting_shot.test.ts index e5a2fc38b94..cfdf6c15966 100644 --- a/src/test/moves/parting_shot.test.ts +++ b/src/test/moves/parting_shot.test.ts @@ -84,19 +84,19 @@ describe("Moves - Parting Shot", () => { // use Memento 3 times to debuff enemy game.move.select(Moves.MEMENTO); await game.phaseInterceptor.to(FaintPhase); - expect(game.scene.getParty()[0].isFainted()).toBe(true); + expect(game.scene.getPlayerParty()[0].isFainted()).toBe(true); game.doSelectPartyPokemon(1); await game.phaseInterceptor.to(TurnInitPhase, false); game.move.select(Moves.MEMENTO); await game.phaseInterceptor.to(FaintPhase); - expect(game.scene.getParty()[0].isFainted()).toBe(true); + expect(game.scene.getPlayerParty()[0].isFainted()).toBe(true); game.doSelectPartyPokemon(2); await game.phaseInterceptor.to(TurnInitPhase, false); game.move.select(Moves.MEMENTO); await game.phaseInterceptor.to(FaintPhase); - expect(game.scene.getParty()[0].isFainted()).toBe(true); + expect(game.scene.getPlayerParty()[0].isFainted()).toBe(true); game.doSelectPartyPokemon(3); // set up done @@ -182,8 +182,8 @@ describe("Moves - Parting Shot", () => { game.move.select(Moves.SPLASH); // intentionally kill party pokemon, switch to second slot (now 1 party mon is fainted) - await game.killPokemon(game.scene.getParty()[0]); - expect(game.scene.getParty()[0].isFainted()).toBe(true); + await game.killPokemon(game.scene.getPlayerParty()[0]); + expect(game.scene.getPlayerParty()[0].isFainted()).toBe(true); await game.phaseInterceptor.run(MessagePhase); game.doSelectPartyPokemon(1); diff --git a/src/test/moves/rollout.test.ts b/src/test/moves/rollout.test.ts index 4a14b100f6d..199f4e1dcf2 100644 --- a/src/test/moves/rollout.test.ts +++ b/src/test/moves/rollout.test.ts @@ -44,7 +44,7 @@ describe("Moves - Rollout", () => { await game.startBattle(); - const playerPkm = game.scene.getParty()[0]; + const playerPkm = game.scene.getPlayerParty()[0]; vi.spyOn(playerPkm, "stats", "get").mockReturnValue([ 500000, 1, 1, 1, 1, 1 ]); // HP, ATK, DEF, SPATK, SPDEF, SPD const enemyPkm = game.scene.getEnemyParty()[0]; diff --git a/src/test/moves/shed_tail.test.ts b/src/test/moves/shed_tail.test.ts index 4d761a8af24..33a7d81e460 100644 --- a/src/test/moves/shed_tail.test.ts +++ b/src/test/moves/shed_tail.test.ts @@ -59,7 +59,7 @@ describe("Moves - Shed Tail", () => { await game.classicMode.startBattle([ Species.MAGIKARP ]); const magikarp = game.scene.getPlayerPokemon()!; - expect(game.scene.getParty().length).toBe(1); + expect(game.scene.getPlayerParty().length).toBe(1); game.move.select(Moves.SHED_TAIL); diff --git a/src/test/moves/sparkly_swirl.test.ts b/src/test/moves/sparkly_swirl.test.ts index 8449f2785f8..a83f1c3a437 100644 --- a/src/test/moves/sparkly_swirl.test.ts +++ b/src/test/moves/sparkly_swirl.test.ts @@ -38,7 +38,7 @@ describe("Moves - Sparkly Swirl", () => { .battleType("double") .statusEffect(StatusEffect.BURN); await game.classicMode.startBattle([ Species.RATTATA, Species.RATTATA, Species.RATTATA ]); - const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getParty(); + const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getPlayerParty(); const leftOpp = game.scene.getEnemyPokemon()!; vi.spyOn(leftPlayer, "resetStatus"); diff --git a/src/test/moves/spikes.test.ts b/src/test/moves/spikes.test.ts index 1dd13f8f65e..35e89c8caf7 100644 --- a/src/test/moves/spikes.test.ts +++ b/src/test/moves/spikes.test.ts @@ -46,7 +46,7 @@ describe("Moves - Spikes", () => { game.doSwitchPokemon(1); await game.toNextTurn(); - const player = game.scene.getParty()[0]; + const player = game.scene.getPlayerParty()[0]; expect(player.hp).toBe(player.getMaxHp()); }, 20000); diff --git a/src/test/moves/tackle.test.ts b/src/test/moves/tackle.test.ts index 5d5ff1a366d..ff50f027f87 100644 --- a/src/test/moves/tackle.test.ts +++ b/src/test/moves/tackle.test.ts @@ -53,7 +53,7 @@ describe("Moves - Tackle", () => { Species.MIGHTYENA, ]); game.scene.currentBattle.enemyParty[0].stats[Stat.DEF] = 50; - game.scene.getParty()[0].stats[Stat.ATK] = 50; + game.scene.getPlayerParty()[0].stats[Stat.ATK] = 50; const hpOpponent = game.scene.currentBattle.enemyParty[0].hp; diff --git a/src/test/moves/tera_starstorm.test.ts b/src/test/moves/tera_starstorm.test.ts index f0759dd242d..c7b6b66c1ce 100644 --- a/src/test/moves/tera_starstorm.test.ts +++ b/src/test/moves/tera_starstorm.test.ts @@ -70,8 +70,8 @@ describe("Moves - Tera Starstorm", () => { it("applies the effects when Terapagos in Stellar Form is fused with another Pokemon", async () => { await game.classicMode.startBattle([ Species.TERAPAGOS, Species.CHARMANDER, Species.MAGIKARP ]); - const fusionedMon = game.scene.getParty()[0]; - const magikarp = game.scene.getParty()[2]; + const fusionedMon = game.scene.getPlayerParty()[0]; + const magikarp = game.scene.getPlayerParty()[2]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) fusionedMon.fusionSpecies = magikarp.species; diff --git a/src/test/moves/u_turn.test.ts b/src/test/moves/u_turn.test.ts index b995c20f503..c6e255e01b2 100644 --- a/src/test/moves/u_turn.test.ts +++ b/src/test/moves/u_turn.test.ts @@ -1,10 +1,8 @@ -import { Abilities } from "#app/enums/abilities"; -import { StatusEffect } from "#app/enums/status-effect"; -import { SwitchPhase } from "#app/phases/switch-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import GameManager from "#app/test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -38,19 +36,16 @@ describe("Moves - U-turn", () => { // arrange const playerHp = 1; game.override.ability(Abilities.REGENERATOR); - await game.startBattle([ - Species.RAICHU, - Species.SHUCKLE - ]); + await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]); game.scene.getPlayerPokemon()!.hp = playerHp; // act game.move.select(Moves.U_TURN); game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); // assert - expect(game.scene.getParty()[1].hp).toEqual(Math.floor(game.scene.getParty()[1].getMaxHp() * 0.33 + playerHp)); + expect(game.scene.getPlayerParty()[1].hp).toEqual(Math.floor(game.scene.getPlayerParty()[1].getMaxHp() * 0.33 + playerHp)); expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE); }, 20000); @@ -58,15 +53,12 @@ describe("Moves - U-turn", () => { it("triggers rough skin on the u-turn user before a new pokemon is switched in", async () => { // arrange game.override.enemyAbility(Abilities.ROUGH_SKIN); - await game.startBattle([ - Species.RAICHU, - Species.SHUCKLE - ]); + await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]); // act game.move.select(Moves.U_TURN); game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to(SwitchPhase, false); + await game.phaseInterceptor.to("SwitchPhase", false); // assert const playerPkm = game.scene.getPlayerPokemon()!; @@ -79,15 +71,12 @@ describe("Moves - U-turn", () => { it("triggers contact abilities on the u-turn user (eg poison point) before a new pokemon is switched in", async () => { // arrange game.override.enemyAbility(Abilities.POISON_POINT); - await game.startBattle([ - Species.RAICHU, - Species.SHUCKLE - ]); + await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]); vi.spyOn(game.scene.getEnemyPokemon()!, "randSeedInt").mockReturnValue(0); // act game.move.select(Moves.U_TURN); - await game.phaseInterceptor.to(SwitchPhase, false); + await game.phaseInterceptor.to("SwitchPhase", false); // assert const playerPkm = game.scene.getPlayerPokemon()!; @@ -108,7 +97,7 @@ describe("Moves - U-turn", () => { // KO the opponent with U-Turn game.move.select(Moves.U_TURN); game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.isFainted()).toBe(true); // Check that U-Turn forced a switch diff --git a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts index a72a9fbb5a3..61d8aaa9f5a 100644 --- a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -143,7 +143,7 @@ describe("Absolute Avarice - Mystery Encounter", () => { await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - for (const partyPokemon of scene.getParty()) { + for (const partyPokemon of scene.getPlayerParty()) { const pokemonId = partyPokemon.id; const pokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === pokemonId, true) as PokemonHeldItemModifier[]; @@ -230,13 +230,13 @@ describe("Absolute Avarice - Mystery Encounter", () => { it("should add Greedent to the party", async () => { await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); - const partyCountBefore = scene.getParty().length; + const partyCountBefore = scene.getPlayerParty().length; await runMysteryEncounterToEnd(game, 3); - const partyCountAfter = scene.getParty().length; + const partyCountAfter = scene.getPlayerParty().length; expect(partyCountBefore + 1).toBe(partyCountAfter); - const greedent = scene.getParty()[scene.getParty().length - 1]; + const greedent = scene.getPlayerParty()[scene.getPlayerParty().length - 1]; expect(greedent.species.speciesId).toBe(Species.GREEDENT); const moveset = greedent.moveset.map(m => m?.moveId); expect(moveset?.length).toBe(4); diff --git a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts index 0585b4ce72b..727a3993d9b 100644 --- a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -147,13 +147,13 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { it("Should remove the Pokemon from the party", async () => { await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); - const initialPartySize = scene.getParty().length; + const initialPartySize = scene.getPlayerParty().length; const pokemonName = scene.currentBattle.mysteryEncounter!.misc.pokemon.name; await runMysteryEncounterToEnd(game, 1); - expect(scene.getParty().length).toBe(initialPartySize - 1); - expect(scene.getParty().find(p => p.name === pokemonName)).toBeUndefined(); + expect(scene.getPlayerParty().length).toBe(initialPartySize - 1); + expect(scene.getPlayerParty().find(p => p.name === pokemonName)).toBeUndefined(); }); it("should leave encounter without battle", async () => { @@ -186,7 +186,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { it("should award EXP to a pokemon with an ability in EXTORTION_ABILITIES", async () => { await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); - const party = scene.getParty(); + const party = scene.getPlayerParty(); const gyarados = party.find((pkm) => pkm.species.speciesId === Species.GYARADOS)!; const expBefore = gyarados.exp; @@ -199,7 +199,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { it("should award EXP to a pokemon with a move in EXTORTION_MOVES", async () => { game.override.ability(Abilities.SYNCHRONIZE); // Not an extortion ability, so we can test extortion move await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, [ Species.ABRA ]); - const party = scene.getParty(); + const party = scene.getPlayerParty(); const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; abra.moveset = [ new PokemonMove(Moves.BEAT_UP) ]; const expBefore = abra.exp; diff --git a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts index bfa3d428bc0..2507b94e5ae 100644 --- a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -176,7 +176,7 @@ describe("Berries Abound - Mystery Encounter", () => { const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText"); await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); - scene.getParty().forEach(pkm => { + scene.getPlayerParty().forEach(pkm => { vi.spyOn(pkm, "getStat").mockReturnValue(1); // for ease return for every stat }); @@ -200,7 +200,7 @@ describe("Berries Abound - Mystery Encounter", () => { const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText"); await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); - scene.getParty().forEach(pkm => { + scene.getPlayerParty().forEach(pkm => { vi.spyOn(pkm, "getStat").mockReturnValue(1); // for ease return for every stat }); @@ -225,7 +225,7 @@ describe("Berries Abound - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); - scene.getParty().forEach(pkm => { + scene.getPlayerParty().forEach(pkm => { vi.spyOn(pkm, "getStat").mockReturnValue(9999); // for ease return for every stat }); diff --git a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index 82ed558e6db..7ea1f883bd1 100644 --- a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -218,7 +218,7 @@ describe("Clowning Around - Mystery Encounter", () => { // Stop next battle before it runs await game.phaseInterceptor.to(NewBattlePhase, false); - const leadPokemon = scene.getParty()[0]; + const leadPokemon = scene.getPlayerParty()[0]; expect(leadPokemon.customPokemonData?.ability).toBe(abilityToTrain); }); }); @@ -251,35 +251,35 @@ describe("Clowning Around - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); // Set some moves on party for attack type booster generation - scene.getParty()[0].moveset = [ new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.THIEF) ]; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.THIEF) ]; // 2 Sitrus Berries on lead scene.modifiers = []; let itemType = generateModifierType(scene, modifierTypes.BERRY, [ BerryType.SITRUS ]) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[0], 2, itemType); // 2 Ganlon Berries on lead itemType = generateModifierType(scene, modifierTypes.BERRY, [ BerryType.GANLON ]) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[0], 2, itemType); // 5 Golden Punch on lead (ultra) itemType = generateModifierType(scene, modifierTypes.GOLDEN_PUNCH) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); // 5 Lucky Egg on lead (ultra) itemType = generateModifierType(scene, modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); // 5 Soul Dew on lead (rogue) itemType = generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); // 2 Golden Egg on lead (rogue) itemType = generateModifierType(scene, modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[0], 2, itemType); // 5 Soul Dew on second party pokemon (these should not change) itemType = generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[1], 5, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[1], 5, itemType); await runMysteryEncounterToEnd(game, 2); - const leadItemsAfter = scene.getParty()[0].getHeldItems(); + const leadItemsAfter = scene.getPlayerParty()[0].getHeldItems(); const ultraCountAfter = leadItemsAfter .filter(m => m.type.tier === ModifierTier.ULTRA) .reduce((a, b) => a + b.stackCount, 0); @@ -289,7 +289,7 @@ describe("Clowning Around - Mystery Encounter", () => { expect(ultraCountAfter).toBe(10); expect(rogueCountAfter).toBe(7); - const secondItemsAfter = scene.getParty()[1].getHeldItems(); + const secondItemsAfter = scene.getPlayerParty()[1].getHeldItems(); expect(secondItemsAfter.length).toBe(1); expect(secondItemsAfter[0].type.id).toBe("SOUL_DEW"); expect(secondItemsAfter[0]?.stackCount).toBe(5); @@ -333,16 +333,16 @@ describe("Clowning Around - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); // Same type moves on lead - scene.getParty()[0].moveset = [ new PokemonMove(Moves.ICE_BEAM), new PokemonMove(Moves.SURF) ]; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.ICE_BEAM), new PokemonMove(Moves.SURF) ]; // Different type moves on second - scene.getParty()[1].moveset = [ new PokemonMove(Moves.GRASS_KNOT), new PokemonMove(Moves.ELECTRO_BALL) ]; + scene.getPlayerParty()[1].moveset = [ new PokemonMove(Moves.GRASS_KNOT), new PokemonMove(Moves.ELECTRO_BALL) ]; // No moves on third - scene.getParty()[2].moveset = []; + scene.getPlayerParty()[2].moveset = []; await runMysteryEncounterToEnd(game, 3); - const leadTypesAfter = scene.getParty()[0].customPokemonData?.types; - const secondaryTypesAfter = scene.getParty()[1].customPokemonData?.types; - const thirdTypesAfter = scene.getParty()[2].customPokemonData?.types; + const leadTypesAfter = scene.getPlayerParty()[0].customPokemonData?.types; + const secondaryTypesAfter = scene.getPlayerParty()[1].customPokemonData?.types; + const thirdTypesAfter = scene.getPlayerParty()[2].customPokemonData?.types; expect(leadTypesAfter.length).toBe(2); expect(leadTypesAfter[0]).toBe(Type.WATER); diff --git a/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts b/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts index 47625541160..147930f05d1 100644 --- a/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts @@ -98,7 +98,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); // Make party lead's level arbitrarily high to not get KOed by move - const partyLead = scene.getParty()[0]; + const partyLead = scene.getPlayerParty()[0]; partyLead.level = 1000; partyLead.calculateStats(); await runMysteryEncounterToEnd(game, 1, undefined, true); @@ -119,7 +119,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { it("should have a Baton in the rewards after battle", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); // Make party lead's level arbitrarily high to not get KOed by move - const partyLead = scene.getParty()[0]; + const partyLead = scene.getPlayerParty()[0]; partyLead.level = 1000; partyLead.calculateStats(); await runMysteryEncounterToEnd(game, 1, undefined, true); @@ -157,7 +157,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { const phaseSpy = vi.spyOn(scene, "unshiftPhase"); await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - scene.getParty()[0].moveset = []; + scene.getPlayerParty()[0].moveset = []; await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof LearnMovePhase).map(p => p[0]); @@ -169,7 +169,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - scene.getParty()[0].moveset = []; + scene.getPlayerParty()[0].moveset = []; await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); @@ -196,13 +196,13 @@ describe("Dancing Lessons - Mystery Encounter", () => { it("should add Oricorio to the party", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - const partyCountBefore = scene.getParty().length; - scene.getParty()[0].moveset = [ new PokemonMove(Moves.DRAGON_DANCE) ]; + const partyCountBefore = scene.getPlayerParty().length; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.DRAGON_DANCE) ]; await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); - const partyCountAfter = scene.getParty().length; + const partyCountAfter = scene.getPlayerParty().length; expect(partyCountBefore + 1).toBe(partyCountAfter); - const oricorio = scene.getParty()[scene.getParty().length - 1]; + const oricorio = scene.getPlayerParty()[scene.getPlayerParty().length - 1]; expect(oricorio.species.speciesId).toBe(Species.ORICORIO); const moveset = oricorio.moveset.map(m => m?.moveId); expect(moveset?.some(m => m === Moves.REVELATION_DANCE)).toBeTruthy(); @@ -211,8 +211,8 @@ describe("Dancing Lessons - Mystery Encounter", () => { it("should NOT be selectable if the player doesn't have a Dance type move", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - const partyCountBefore = scene.getParty().length; - scene.getParty().forEach(p => p.moveset = []); + const partyCountBefore = scene.getPlayerParty().length; + scene.getPlayerParty().forEach(p => p.moveset = []); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.getCurrentPhase(); @@ -223,7 +223,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { vi.spyOn(scene.ui, "playError"); await runSelectMysteryEncounterOption(game, 3); - const partyCountAfter = scene.getParty().length; + const partyCountAfter = scene.getPlayerParty().length; expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled @@ -236,7 +236,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - scene.getParty()[0].moveset = [ new PokemonMove(Moves.DRAGON_DANCE) ]; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.DRAGON_DANCE) ]; await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); diff --git a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index 66d628ef82f..c226d60a9b4 100644 --- a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -191,7 +191,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 2 Sitrus berries on party lead scene.modifiers = []; const sitrus = generateModifierType(scene, modifierTypes.BERRY, [ BerryType.SITRUS ])!; - const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; sitrusMod.stackCount = 2; await scene.addModifier(sitrusMod, true, false, false, true); await scene.updateModifiers(true); @@ -212,7 +212,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Reviver Seed on party lead scene.modifiers = []; const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -238,7 +238,7 @@ describe("Delibird-y - Mystery Encounter", () => { const sitrus = generateModifierType(scene, modifierTypes.BERRY, [ BerryType.SITRUS ])!; // Sitrus berries on party - const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; sitrusMod.stackCount = 2; await scene.addModifier(sitrusMod, true, false, false, true); await scene.updateModifiers(true); @@ -267,7 +267,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Reviver Seed on party lead const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -291,7 +291,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]); + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]); await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -319,7 +319,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Reviver Seed on party lead const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -353,7 +353,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 2 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 2; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -374,7 +374,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -400,7 +400,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Soul Dew on party lead const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -424,7 +424,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Reviver Seed on party lead scene.modifiers = []; const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getParty()[0]); + const modifier = revSeed.newModifier(scene.getPlayerParty()[0]); await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -453,7 +453,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); diff --git a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index 5a270f1cbec..92555e98679 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -175,7 +175,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - const leadPokemonId = scene.getParty()?.[0].id; + const leadPokemonId = scene.getPlayerParty()?.[0].id; const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier); @@ -202,7 +202,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { it("should damage all non-fire party PKM by 20%, and burn + give Heatproof to a random Pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); - const party = scene.getParty(); + const party = scene.getPlayerParty(); const lapras = party.find((pkm) => pkm.species.speciesId === Species.LAPRAS)!; lapras.status = new Status(StatusEffect.POISON); const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; @@ -254,7 +254,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - const leadPokemonItems = scene.getParty()?.[0].getHeldItems() as PokemonHeldItemModifier[]; + const leadPokemonItems = scene.getPlayerParty()?.[0].getHeldItems() as PokemonHeldItemModifier[]; const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier); expect(item).toBeDefined; }); diff --git a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts index d23e7919267..a5098f682eb 100644 --- a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -151,7 +151,7 @@ describe("Fight or Flight - Mystery Encounter", () => { it("should NOT be selectable if the player doesn't have a Stealing move", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); - scene.getParty().forEach(p => p.moveset = []); + scene.getPlayerParty().forEach(p => p.moveset = []); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.getCurrentPhase(); @@ -175,7 +175,7 @@ describe("Fight or Flight - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); // Mock moveset - scene.getParty()[0].moveset = [ new PokemonMove(Moves.KNOCK_OFF) ]; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.KNOCK_OFF) ]; const item = game.scene.currentBattle.mysteryEncounter!.misc; await runMysteryEncounterToEnd(game, 2); diff --git a/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts index b08f8008b68..e8d19ff50b9 100644 --- a/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -106,10 +106,10 @@ describe("Global Trade System - Mystery Encounter", () => { it("Should trade a Pokemon from the player's party for the first of 3 Pokemon options", async () => { await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); - const speciesBefore = scene.getParty()[0].species.speciesId; + const speciesBefore = scene.getPlayerParty()[0].species.speciesId; await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); - const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + const speciesAfter = scene.getPlayerParty().at(-1)?.species.speciesId; expect(speciesAfter).toBeDefined(); expect(speciesBefore).not.toBe(speciesAfter); @@ -119,10 +119,10 @@ describe("Global Trade System - Mystery Encounter", () => { it("Should trade a Pokemon from the player's party for the second of 3 Pokemon options", async () => { await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); - const speciesBefore = scene.getParty()[1].species.speciesId; + const speciesBefore = scene.getPlayerParty()[1].species.speciesId; await runMysteryEncounterToEnd(game, 1, { pokemonNo: 2, optionNo: 2 }); - const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + const speciesAfter = scene.getPlayerParty().at(-1)?.species.speciesId; expect(speciesAfter).toBeDefined(); expect(speciesBefore).not.toBe(speciesAfter); @@ -132,10 +132,10 @@ describe("Global Trade System - Mystery Encounter", () => { it("Should trade a Pokemon from the player's party for the third of 3 Pokemon options", async () => { await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); - const speciesBefore = scene.getParty()[2].species.speciesId; + const speciesBefore = scene.getPlayerParty()[2].species.speciesId; await runMysteryEncounterToEnd(game, 1, { pokemonNo: 3, optionNo: 3 }); - const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + const speciesAfter = scene.getPlayerParty().at(-1)?.species.speciesId; expect(speciesAfter).toBeDefined(); expect(speciesBefore).not.toBe(speciesAfter); @@ -166,10 +166,10 @@ describe("Global Trade System - Mystery Encounter", () => { it("Should trade a Pokemon from the player's party for a random wonder trade Pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); - const speciesBefore = scene.getParty()[2].species.speciesId; + const speciesBefore = scene.getPlayerParty()[2].species.speciesId; await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); - const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + const speciesAfter = scene.getPlayerParty().at(-1)?.species.speciesId; expect(speciesAfter).toBeDefined(); expect(speciesBefore).not.toBe(speciesAfter); @@ -204,7 +204,7 @@ describe("Global Trade System - Mystery Encounter", () => { // Set 2 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 2; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -229,7 +229,7 @@ describe("Global Trade System - Mystery Encounter", () => { // Set 1 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); diff --git a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index dec14d46cc8..51f759c9268 100644 --- a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -114,7 +114,7 @@ describe("Lost at Sea - Mystery Encounter", () => { const laprasSpecies = getPokemonSpecies(Species.LAPRAS); await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); - const party = game.scene.getParty(); + const party = game.scene.getPlayerParty(); const blastoise = party.find((pkm) => pkm.species.speciesId === Species.BLASTOISE); const expBefore = blastoise!.exp; @@ -179,7 +179,7 @@ describe("Lost at Sea - Mystery Encounter", () => { game.override.startingWave(wave); await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); - const party = game.scene.getParty(); + const party = game.scene.getPlayerParty(); const pidgeot = party.find((pkm) => pkm.species.speciesId === Species.PIDGEOT); const expBefore = pidgeot!.exp; @@ -241,7 +241,7 @@ describe("Lost at Sea - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); - const party = game.scene.getParty(); + const party = game.scene.getPlayerParty(); const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; vi.spyOn(abra, "isAllowedInBattle").mockReturnValue(false); diff --git a/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts index ba8ce648a3f..e063a4f3349 100644 --- a/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -101,7 +101,7 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Override party levels to 50 so stats can be fully reflective - scene.getParty().forEach(p => { + scene.getPlayerParty().forEach(p => { p.level = 50; p.calculateStats(); }); @@ -109,7 +109,7 @@ describe("Part-Timer - Mystery Encounter", () => { expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(1), true, false); // Expect PP of mon's moves to have been reduced to 2 - const moves = scene.getParty()[0].moveset; + const moves = scene.getPlayerParty()[0].moveset; for (const move of moves) { expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); } @@ -120,7 +120,7 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Override party levels to 50 so stats can be fully reflective - scene.getParty().forEach(p => { + scene.getPlayerParty().forEach(p => { p.level = 50; p.ivs = [ 20, 20, 20, 20, 20, 20 ]; p.calculateStats(); @@ -129,7 +129,7 @@ describe("Part-Timer - Mystery Encounter", () => { expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(4), true, false); // Expect PP of mon's moves to have been reduced to 2 - const moves = scene.getParty()[1].moveset; + const moves = scene.getPlayerParty()[1].moveset; for (const move of moves) { expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); } @@ -166,7 +166,7 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Override party levels to 50 so stats can be fully reflective - scene.getParty().forEach(p => { + scene.getPlayerParty().forEach(p => { p.level = 50; p.calculateStats(); }); @@ -174,7 +174,7 @@ describe("Part-Timer - Mystery Encounter", () => { expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(1), true, false); // Expect PP of mon's moves to have been reduced to 2 - const moves = scene.getParty()[2].moveset; + const moves = scene.getPlayerParty()[2].moveset; for (const move of moves) { expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); } @@ -185,7 +185,7 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Override party levels to 50 so stats can be fully reflective - scene.getParty().forEach(p => { + scene.getPlayerParty().forEach(p => { p.level = 50; p.ivs = [ 20, 20, 20, 20, 20, 20 ]; p.calculateStats(); @@ -194,7 +194,7 @@ describe("Part-Timer - Mystery Encounter", () => { expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(4), true, false); // Expect PP of mon's moves to have been reduced to 2 - const moves = scene.getParty()[3].moveset; + const moves = scene.getPlayerParty()[3].moveset; for (const move of moves) { expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); } @@ -232,7 +232,7 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Mock movesets - scene.getParty().forEach(p => p.moveset = []); + scene.getPlayerParty().forEach(p => p.moveset = []); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.getCurrentPhase(); @@ -256,12 +256,12 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Mock moveset - scene.getParty()[0].moveset = [ new PokemonMove(Moves.ATTRACT) ]; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.ATTRACT) ]; await runMysteryEncounterToEnd(game, 3); expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(2.5), true, false); // Expect PP of mon's moves to have been reduced to 2 - const moves = scene.getParty()[0].moveset; + const moves = scene.getPlayerParty()[0].moveset; for (const move of moves) { expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); } diff --git a/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts b/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts index a3a43815ec6..7fc2490fcc9 100644 --- a/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts @@ -152,7 +152,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); - expect(scene.getParty().length).toBe(1); + expect(scene.getPlayerParty().length).toBe(1); }); it("Should reward the player with friendship and eggs based on pokemon selected", async () => { @@ -231,7 +231,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); - expect(scene.getParty().length).toBe(1); + expect(scene.getPlayerParty().length).toBe(1); }); it("Should reward the player with friendship and eggs based on pokemon selected", async () => { @@ -310,7 +310,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); - expect(scene.getParty().length).toBe(1); + expect(scene.getPlayerParty().length).toBe(1); }); it("Should reward the player with friendship and eggs based on pokemon selected", async () => { diff --git a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts index 040381c4ac3..a50c0cf4c9e 100644 --- a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -140,14 +140,14 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { scene.money = 20000; await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); - const initialPartySize = scene.getParty().length; + const initialPartySize = scene.getPlayerParty().length; const pokemonName = scene.currentBattle.mysteryEncounter!.misc.pokemon.name; await runMysteryEncounterToEnd(game, 1); - expect(scene.getParty().length).toBe(initialPartySize + 1); + expect(scene.getPlayerParty().length).toBe(initialPartySize + 1); - const newlyPurchasedPokemon = scene.getParty()[scene.getParty().length - 1]; + const newlyPurchasedPokemon = scene.getPlayerParty()[scene.getPlayerParty().length - 1]; expect(newlyPurchasedPokemon.name).toBe(pokemonName); expect(newlyPurchasedPokemon!.moveset.length > 0).toBeTruthy(); }); diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index 4d95ff31d36..be0e6e68b5e 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -143,10 +143,10 @@ describe("The Strong Stuff - Mystery Encounter", () => { it("should lower stats of 2 highest BST and raise stats for rest of party", async () => { await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); - const bstsPrior = scene.getParty().map(p => p.getSpeciesForm().getBaseStatTotal()); + const bstsPrior = scene.getPlayerParty().map(p => p.getSpeciesForm().getBaseStatTotal()); await runMysteryEncounterToEnd(game, 1); - const bstsAfter = scene.getParty().map(p => { + const bstsAfter = scene.getPlayerParty().map(p => { const baseStats = p.getSpeciesForm().baseStats.slice(0); scene.applyModifiers(PokemonBaseStatTotalModifier, true, p, baseStats); return baseStats.reduce((a, b) => a + b); diff --git a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts index fa1b5ecdeb7..c811bda673d 100644 --- a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -214,11 +214,11 @@ describe("Uncommon Breed - Mystery Encounter", () => { // Berries on party lead const sitrus = generateModifierType(scene, modifierTypes.BERRY, [ BerryType.SITRUS ])!; - const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; sitrusMod.stackCount = 2; await scene.addModifier(sitrusMod, true, false, false, true); const ganlon = generateModifierType(scene, modifierTypes.BERRY, [ BerryType.GANLON ])!; - const ganlonMod = ganlon.newModifier(scene.getParty()[0]) as BerryModifier; + const ganlonMod = ganlon.newModifier(scene.getPlayerParty()[0]) as BerryModifier; ganlonMod.stackCount = 3; await scene.addModifier(ganlonMod, true, false, false, true); await scene.updateModifiers(true); @@ -248,7 +248,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { it("should NOT be selectable if the player doesn't have an Attracting move", async () => { await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); - scene.getParty().forEach(p => p.moveset = []); + scene.getPlayerParty().forEach(p => p.moveset = []); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.getCurrentPhase(); @@ -270,7 +270,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); // Mock moveset - scene.getParty()[0].moveset = [ new PokemonMove(Moves.CHARM) ]; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.CHARM) ]; await runMysteryEncounterToEnd(game, 3); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); diff --git a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts index c1fa6d83a18..ec9cf1509b1 100644 --- a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -110,14 +110,14 @@ describe("Weird Dream - Mystery Encounter", () => { it("should transform the new party into new species, 2 at +90/+110, the rest at +40/50 BST", async () => { await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); - const pokemonPrior = scene.getParty().map(pokemon => pokemon); + const pokemonPrior = scene.getPlayerParty().map(pokemon => pokemon); const bstsPrior = pokemonPrior.map(species => species.getSpeciesForm().getBaseStatTotal()); await runMysteryEncounterToEnd(game, 1); await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - const pokemonAfter = scene.getParty(); + const pokemonAfter = scene.getPlayerParty(); const bstsAfter = pokemonAfter.map(pokemon => pokemon.getSpeciesForm().getBaseStatTotal()); const bstDiff = bstsAfter.map((bst, index) => bst - bstsPrior[index]); @@ -184,7 +184,7 @@ describe("Weird Dream - Mystery Encounter", () => { const enemyField = scene.getEnemyField(); expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(enemyField.length).toBe(1); - expect(scene.getEnemyParty().length).toBe(scene.getParty().length); + expect(scene.getEnemyParty().length).toBe(scene.getPlayerParty().length); }); it("should have 2 Rogue/2 Ultra/2 Great items in rewards", async () => { @@ -227,14 +227,14 @@ describe("Weird Dream - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); - const levelsPrior = scene.getParty().map(p => p.level); + const levelsPrior = scene.getPlayerParty().map(p => p.level); await runMysteryEncounterToEnd(game, 3); - const levelsAfter = scene.getParty().map(p => p.level); + const levelsAfter = scene.getPlayerParty().map(p => p.level); for (let i = 0; i < levelsPrior.length; i++) { expect(Math.max(Math.ceil(0.9 * levelsPrior[i]), 1)).toBe(levelsAfter[i]); - expect(scene.getParty()[i].levelExp).toBe(0); + expect(scene.getPlayerParty()[i].levelExp).toBe(0); } expect(leaveEncounterWithoutBattleSpy).toBeCalled(); diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index 134966a188d..b57a88d5cad 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -50,7 +50,7 @@ describe("Mystery Encounter Utils", () => { it("gets a fainted pokemon from player party if isAllowedInBattle is false", () => { // Both pokemon fainted - scene.getParty().forEach(p => { + scene.getPlayerParty().forEach(p => { p.hp = 0; p.trySetStatus(StatusEffect.FAINT); p.updateInfo(); @@ -70,7 +70,7 @@ describe("Mystery Encounter Utils", () => { it("gets an unfainted legal pokemon from player party if isAllowed is true and isFainted is false", () => { // Only faint 1st pokemon - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); party[0].updateInfo(); @@ -89,7 +89,7 @@ describe("Mystery Encounter Utils", () => { it("returns last unfainted pokemon if doNotReturnLastAbleMon is false", () => { // Only faint 1st pokemon - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); party[0].updateInfo(); @@ -108,7 +108,7 @@ describe("Mystery Encounter Utils", () => { it("never returns last unfainted pokemon if doNotReturnLastAbleMon is true", () => { // Only faint 1st pokemon - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); party[0].updateInfo(); @@ -128,7 +128,7 @@ describe("Mystery Encounter Utils", () => { describe("getHighestLevelPlayerPokemon", () => { it("gets highest level pokemon", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].level = 100; const result = getHighestLevelPlayerPokemon(scene); @@ -136,7 +136,7 @@ describe("Mystery Encounter Utils", () => { }); it("gets highest level pokemon at different index", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[1].level = 100; const result = getHighestLevelPlayerPokemon(scene); @@ -144,7 +144,7 @@ describe("Mystery Encounter Utils", () => { }); it("breaks ties by getting returning lower index", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].level = 100; party[1].level = 100; @@ -153,7 +153,7 @@ describe("Mystery Encounter Utils", () => { }); it("returns highest level unfainted if unfainted is true", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].level = 100; party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); @@ -167,7 +167,7 @@ describe("Mystery Encounter Utils", () => { describe("getLowestLevelPokemon", () => { it("gets lowest level pokemon", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].level = 100; const result = getLowestLevelPlayerPokemon(scene); @@ -175,7 +175,7 @@ describe("Mystery Encounter Utils", () => { }); it("gets lowest level pokemon at different index", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[1].level = 100; const result = getLowestLevelPlayerPokemon(scene); @@ -183,7 +183,7 @@ describe("Mystery Encounter Utils", () => { }); it("breaks ties by getting returning lower index", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].level = 100; party[1].level = 100; @@ -192,7 +192,7 @@ describe("Mystery Encounter Utils", () => { }); it("returns lowest level unfainted if unfainted is true", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].level = 10; party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); @@ -239,7 +239,7 @@ describe("Mystery Encounter Utils", () => { describe("koPlayerPokemon", () => { it("KOs a pokemon", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); const arceus = party[0]; arceus.hp = 100; expect(arceus.isAllowedInBattle()).toBe(true); diff --git a/src/test/phases/select-modifier-phase.test.ts b/src/test/phases/select-modifier-phase.test.ts index a945aff055b..60f81f3ad54 100644 --- a/src/test/phases/select-modifier-phase.test.ts +++ b/src/test/phases/select-modifier-phase.test.ts @@ -150,10 +150,10 @@ describe("SelectModifierPhase", () => { const pokemon = new PlayerPokemon(scene, getPokemonSpecies(Species.BULBASAUR), 10, undefined, 0, undefined, true, 2, undefined, undefined, undefined); // Fill party with max shinies - while (scene.getParty().length > 0) { - scene.getParty().pop(); + while (scene.getPlayerParty().length > 0) { + scene.getPlayerParty().pop(); } - scene.getParty().push(pokemon, pokemon, pokemon, pokemon, pokemon, pokemon); + scene.getPlayerParty().push(pokemon, pokemon, pokemon, pokemon, pokemon, pokemon); const selectModifierPhase = new SelectModifierPhase(scene, 0, undefined, customModifiers); scene.pushPhase(selectModifierPhase); diff --git a/src/test/ui/starter-select.test.ts b/src/test/ui/starter-select.test.ts index 94370ca1b74..8f86d62c546 100644 --- a/src/test/ui/starter-select.test.ts +++ b/src/test/ui/starter-select.test.ts @@ -91,10 +91,10 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(2); - expect(game.scene.getParty()[0].gender).toBe(Gender.MALE); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(true); + expect(game.scene.getPlayerParty()[0].variant).toBe(2); + expect(game.scene.getPlayerParty()[0].gender).toBe(Gender.MALE); }, 20000); it("Bulbasaur - shiny - variant 2 female hardy overgrow", async() => { @@ -152,11 +152,11 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(2); - expect(game.scene.getParty()[0].nature).toBe(Nature.HARDY); - expect(game.scene.getParty()[0].getAbility().id).toBe(Abilities.OVERGROW); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(true); + expect(game.scene.getPlayerParty()[0].variant).toBe(2); + expect(game.scene.getPlayerParty()[0].nature).toBe(Nature.HARDY); + expect(game.scene.getPlayerParty()[0].getAbility().id).toBe(Abilities.OVERGROW); }, 20000); it("Bulbasaur - shiny - variant 2 female lonely chlorophyl", async() => { @@ -216,12 +216,12 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(2); - expect(game.scene.getParty()[0].gender).toBe(Gender.FEMALE); - expect(game.scene.getParty()[0].nature).toBe(Nature.LONELY); - expect(game.scene.getParty()[0].getAbility().id).toBe(Abilities.CHLOROPHYLL); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(true); + expect(game.scene.getPlayerParty()[0].variant).toBe(2); + expect(game.scene.getPlayerParty()[0].gender).toBe(Gender.FEMALE); + expect(game.scene.getPlayerParty()[0].nature).toBe(Nature.LONELY); + expect(game.scene.getPlayerParty()[0].getAbility().id).toBe(Abilities.CHLOROPHYLL); }, 20000); it("Bulbasaur - shiny - variant 2 female", async() => { @@ -279,10 +279,10 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(2); - expect(game.scene.getParty()[0].gender).toBe(Gender.FEMALE); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(true); + expect(game.scene.getPlayerParty()[0].variant).toBe(2); + expect(game.scene.getPlayerParty()[0].gender).toBe(Gender.FEMALE); }, 20000); it("Bulbasaur - not shiny", async() => { @@ -340,9 +340,9 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(false); - expect(game.scene.getParty()[0].variant).toBe(0); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(false); + expect(game.scene.getPlayerParty()[0].variant).toBe(0); }, 20000); it("Bulbasaur - shiny - variant 1", async() => { @@ -401,9 +401,9 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(1); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(true); + expect(game.scene.getPlayerParty()[0].variant).toBe(1); }, 20000); it("Bulbasaur - shiny - variant 0", async() => { @@ -461,9 +461,9 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(0); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(true); + expect(game.scene.getPlayerParty()[0].variant).toBe(0); }, 20000); it("Check if first pokemon in party is caterpie from gen 1 and 1rd row, 3rd column", async() => { @@ -527,7 +527,7 @@ describe("UI - Starter select", () => { saveSlotSelectUiHandler.processInput(Button.ACTION); }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.CATERPIE); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.CATERPIE); }, 20000); it("Check if first pokemon in party is nidoran_m from gen 1 and 2nd row, 4th column (cursor (9+4)-1)", async() => { @@ -593,6 +593,6 @@ describe("UI - Starter select", () => { saveSlotSelectUiHandler.processInput(Button.ACTION); }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.NIDORAN_M); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.NIDORAN_M); }, 20000); }); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index 86c51972c8b..a7d67911228 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -1,20 +1,20 @@ import { updateUserInfo } from "#app/account"; import { BattlerIndex } from "#app/battle"; import BattleScene from "#app/battle-scene"; -import { BattleStyle } from "#app/enums/battle-style"; -import { Moves } from "#app/enums/moves"; import { getMoveTargets } from "#app/data/move"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import Trainer from "#app/field/trainer"; import { GameModes, getGameMode } from "#app/game-mode"; import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; import overrides from "#app/overrides"; +import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; import { CommandPhase } from "#app/phases/command-phase"; import { EncounterPhase } from "#app/phases/encounter-phase"; import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; import { FaintPhase } from "#app/phases/faint-phase"; import { LoginPhase } from "#app/phases/login-phase"; import { MovePhase } from "#app/phases/move-phase"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { NewBattlePhase } from "#app/phases/new-battle-phase"; import { SelectStarterPhase } from "#app/phases/select-starter-phase"; import { SelectTargetPhase } from "#app/phases/select-target-phase"; @@ -24,37 +24,36 @@ import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { TurnStartPhase } from "#app/phases/turn-start-phase"; import ErrorInterceptor from "#app/test/utils/errorInterceptor"; import InputsHandler from "#app/test/utils/inputsHandler"; +import BattleMessageUiHandler from "#app/ui/battle-message-ui-handler"; import CommandUiHandler from "#app/ui/command-ui-handler"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import PartyUiHandler from "#app/ui/party-ui-handler"; import TargetSelectUiHandler from "#app/ui/target-select-ui-handler"; import { Mode } from "#app/ui/ui"; +import { isNullOrUndefined } from "#app/utils"; +import { BattleStyle } from "#enums/battle-style"; import { Button } from "#enums/buttons"; +import { ExpGainsSpeed } from "#enums/exp-gains-speed"; import { ExpNotification } from "#enums/exp-notification"; +import { Moves } from "#enums/moves"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { generateStarter, waitUntil } from "#test/utils/gameManagerUtils"; import GameWrapper from "#test/utils/gameWrapper"; +import { ChallengeModeHelper } from "#test/utils/helpers/challengeModeHelper"; +import { ClassicModeHelper } from "#test/utils/helpers/classicModeHelper"; +import { DailyModeHelper } from "#test/utils/helpers/dailyModeHelper"; +import { ModifierHelper } from "#test/utils/helpers/modifiersHelper"; +import { MoveHelper } from "#test/utils/helpers/moveHelper"; +import { OverridesHelper } from "#test/utils/helpers/overridesHelper"; +import { ReloadHelper } from "#test/utils/helpers/reloadHelper"; +import { SettingsHelper } from "#test/utils/helpers/settingsHelper"; import PhaseInterceptor from "#test/utils/phaseInterceptor"; import TextInterceptor from "#test/utils/TextInterceptor"; import { AES, enc } from "crypto-js"; import fs from "fs"; -import { vi } from "vitest"; -import { ClassicModeHelper } from "./helpers/classicModeHelper"; -import { DailyModeHelper } from "./helpers/dailyModeHelper"; -import { ChallengeModeHelper } from "./helpers/challengeModeHelper"; -import { MoveHelper } from "./helpers/moveHelper"; -import { OverridesHelper } from "./helpers/overridesHelper"; -import { SettingsHelper } from "./helpers/settingsHelper"; -import { ReloadHelper } from "./helpers/reloadHelper"; -import { ModifierHelper } from "./helpers/modifiersHelper"; -import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; -import BattleMessageUiHandler from "#app/ui/battle-message-ui-handler"; -import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; -import { expect } from "vitest"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { isNullOrUndefined } from "#app/utils"; -import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; +import { expect, vi } from "vitest"; /** * Class to manage the game state and transitions between phases. @@ -427,7 +426,7 @@ export default class GameManager { * @param pokemonIndex the index of the pokemon in your party to revive */ doRevivePokemon(pokemonIndex: number) { - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); const candidate = new ModifierTypeOption(modifierTypes.MAX_REVIVE(), 0); const modifier = candidate.type!.newModifier(party[pokemonIndex]); this.scene.addModifier(modifier, false); diff --git a/src/test/utils/gameManagerUtils.ts b/src/test/utils/gameManagerUtils.ts index 543ee9627fe..0c70bcf7f18 100644 --- a/src/test/utils/gameManagerUtils.ts +++ b/src/test/utils/gameManagerUtils.ts @@ -105,7 +105,7 @@ export function initSceneWithoutEncounterPhase(scene: BattleScene, species?: Spe const starterIvs = scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); const starterPokemon = scene.addPlayerPokemon(starter.species, scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, starterIvs, starter.nature); starter.moveset && starterPokemon.tryPopulateMoveset(starter.moveset); - scene.getParty().push(starterPokemon); + scene.getPlayerParty().push(starterPokemon); }); scene.currentBattle = new Battle(getGameMode(GameModes.CLASSIC), 5, BattleType.WILD, undefined, false); diff --git a/src/ui/battle-message-ui-handler.ts b/src/ui/battle-message-ui-handler.ts index 832d665b290..180fc66ed9b 100644 --- a/src/ui/battle-message-ui-handler.ts +++ b/src/ui/battle-message-ui-handler.ts @@ -170,7 +170,7 @@ export default class BattleMessageUiHandler extends MessageUiHandler { if (!this.scene.showLevelUpStats) { return resolve(); } - const newStats = (this.scene as BattleScene).getParty()[partyMemberIndex].stats; + const newStats = (this.scene as BattleScene).getPlayerParty()[partyMemberIndex].stats; let levelUpStatsValuesText = ""; for (const s of PERMANENT_STATS) { levelUpStatsValuesText += `${showTotals ? newStats[s] : newStats[s] - prevStats[s]}\n`; diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index e96fde8d54f..a26aa572ef3 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -346,13 +346,13 @@ export default class PartyUiHandler extends MessageUiHandler { if (this.optionsMode) { const option = this.options[this.optionsCursor]; if (button === Button.ACTION) { - const pokemon = this.scene.getParty()[this.cursor]; + const pokemon = this.scene.getPlayerParty()[this.cursor]; if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode && option !== PartyOption.CANCEL) { this.startTransfer(); let ableToTransfer: string; - for (let p = 0; p < this.scene.getParty().length; p++) { // this fore look goes through each of the party pokemon - const newPokemon = this.scene.getParty()[p]; + for (let p = 0; p < this.scene.getPlayerParty().length; p++) { // this fore look goes through each of the party pokemon + const newPokemon = this.scene.getPlayerParty()[p]; // this next line gets all of the transferable items from pokemon [p]; it does this by getting all the held modifiers that are transferable and checking to see if they belong to pokemon [p] const getTransferrableItemsFromPokemon = (newPokemon: PlayerPokemon) => this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).isTransferable && (m as PokemonHeldItemModifier).pokemonId === newPokemon.id) as PokemonHeldItemModifier[]; @@ -409,7 +409,7 @@ export default class PartyUiHandler extends MessageUiHandler { filterResult = this.moveSelectFilter(pokemon.moveset[this.optionsCursor]!); // TODO: is this bang correct? } } else { - filterResult = (this.selectFilter as PokemonModifierTransferSelectFilter)(pokemon, getTransferrableItemsFromPokemon(this.scene.getParty()[this.transferCursor])[this.transferOptionCursor]); + filterResult = (this.selectFilter as PokemonModifierTransferSelectFilter)(pokemon, getTransferrableItemsFromPokemon(this.scene.getPlayerParty()[this.transferCursor])[this.transferOptionCursor]); } if (filterResult === null) { if (this.partyUiMode !== PartyUiMode.SPLICE) { @@ -419,7 +419,7 @@ export default class PartyUiHandler extends MessageUiHandler { if (option === PartyOption.TRANSFER) { if (this.transferCursor !== this.cursor) { if (this.transferAll) { - getTransferrableItemsFromPokemon(this.scene.getParty()[this.transferCursor]).forEach((_, i) => (this.selectCallback as PartyModifierTransferSelectCallback)(this.transferCursor, i, this.transferQuantitiesMax[i], this.cursor)); + getTransferrableItemsFromPokemon(this.scene.getPlayerParty()[this.transferCursor]).forEach((_, i) => (this.selectCallback as PartyModifierTransferSelectCallback)(this.transferCursor, i, this.transferQuantitiesMax[i], this.cursor)); } else { (this.selectCallback as PartyModifierTransferSelectCallback)(this.transferCursor, this.transferOptionCursor, this.transferQuantities[this.transferOptionCursor], this.cursor); } @@ -580,7 +580,7 @@ export default class PartyUiHandler extends MessageUiHandler { // show move description if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) { const option = this.options[this.optionsCursor]; - const pokemon = this.scene.getParty()[this.cursor]; + const pokemon = this.scene.getPlayerParty()[this.cursor]; const move = allMoves[pokemon.getLearnableLevelMoves()[option]]; if (move) { this.moveInfoOverlay.show(move); @@ -596,7 +596,7 @@ export default class PartyUiHandler extends MessageUiHandler { if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode) { /** Initialize item quantities for the selected Pokemon */ const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.isTransferable && m.pokemonId === this.scene.getParty()[this.cursor].id) as PokemonHeldItemModifier[]; + && m.isTransferable && m.pokemonId === this.scene.getPlayerParty()[this.cursor].id) as PokemonHeldItemModifier[]; this.transferQuantities = itemModifiers.map(item => item.getStackCount()); this.transferQuantitiesMax = itemModifiers.map(item => item.getStackCount()); } @@ -664,7 +664,7 @@ export default class PartyUiHandler extends MessageUiHandler { } populatePartySlots() { - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); if (this.cursor < 6 && this.cursor >= party.length) { this.cursor = party.length - 1; @@ -803,7 +803,7 @@ export default class PartyUiHandler extends MessageUiHandler { } updateOptions(): void { - const pokemon = this.scene.getParty()[this.cursor]; + const pokemon = this.scene.getPlayerParty()[this.cursor]; const learnableLevelMoves = this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER ? pokemon.getLearnableLevelMoves() @@ -1059,13 +1059,13 @@ export default class PartyUiHandler extends MessageUiHandler { } doRelease(slotIndex: integer): void { - this.showText(this.getReleaseMessage(getPokemonNameWithAffix(this.scene.getParty()[slotIndex])), null, () => { + this.showText(this.getReleaseMessage(getPokemonNameWithAffix(this.scene.getPlayerParty()[slotIndex])), null, () => { this.clearPartySlots(); this.scene.removePartyMemberModifiers(slotIndex); - const releasedPokemon = this.scene.getParty().splice(slotIndex, 1)[0]; + const releasedPokemon = this.scene.getPlayerParty().splice(slotIndex, 1)[0]; releasedPokemon.destroy(); this.populatePartySlots(); - if (this.cursor >= this.scene.getParty().length) { + if (this.cursor >= this.scene.getPlayerParty().length) { this.setCursor(this.cursor - 1); } if (this.partyUiMode === PartyUiMode.RELEASE) { diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index b35c0ca3303..9a61dd0f688 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -554,7 +554,7 @@ export default class SummaryUiHandler extends UiHandler { break; } const isDown = button === Button.DOWN; - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); const partyMemberIndex = this.pokemon ? party.indexOf(this.pokemon) : -1; if ((isDown && partyMemberIndex < party.length - 1) || (!isDown && partyMemberIndex)) { const page = this.cursor; From 836b37f383a7048dba5f963bcb2ee92ddf080f74 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 3 Nov 2024 18:55:12 -0800 Subject: [PATCH 14/98] [Refactor] Clean up some code related to starter select UI (#4752) * Clean up some code related to SSUI Replace a few nested ternaries with if blocks Remove some `undefined`s with destructuring * Add comment, use `v === 0` instead of `!v` Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --------- Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/system/game-data.ts | 46 +++++++-- src/ui/save-slot-select-ui-handler.ts | 2 +- src/ui/starter-select-ui-handler.ts | 129 +++++++++++++++++--------- 3 files changed, 123 insertions(+), 54 deletions(-) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 49a677cd019..e252e03afaf 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1829,17 +1829,40 @@ export class GameData { return starterCount; } - getSpeciesDefaultDexAttr(species: PokemonSpecies, forSeen: boolean = false, optimistic: boolean = false): bigint { + getSpeciesDefaultDexAttr(species: PokemonSpecies, _forSeen: boolean = false, optimistic: boolean = false): bigint { let ret = 0n; const dexEntry = this.dexData[species.speciesId]; const attr = dexEntry.caughtAttr; - ret |= optimistic - ? attr & DexAttr.SHINY ? DexAttr.SHINY : DexAttr.NON_SHINY - : attr & DexAttr.NON_SHINY || !(attr & DexAttr.SHINY) ? DexAttr.NON_SHINY : DexAttr.SHINY; + if (optimistic) { + if (attr & DexAttr.SHINY) { + ret |= DexAttr.SHINY; + + if (attr & DexAttr.VARIANT_3) { + ret |= DexAttr.VARIANT_3; + } else if (attr & DexAttr.VARIANT_2) { + ret |= DexAttr.VARIANT_2; + } else { + ret |= DexAttr.DEFAULT_VARIANT; + } + } else { + ret |= DexAttr.NON_SHINY; + ret |= DexAttr.DEFAULT_VARIANT; + } + } else { + // Default to non shiny. Fallback to shiny if it's the only thing that's unlocked + ret |= (attr & DexAttr.NON_SHINY || !(attr & DexAttr.SHINY)) ? DexAttr.NON_SHINY : DexAttr.SHINY; + + if (attr & DexAttr.DEFAULT_VARIANT) { + ret |= DexAttr.DEFAULT_VARIANT; + } else if (attr & DexAttr.VARIANT_2) { + ret |= DexAttr.VARIANT_2; + } else if (attr & DexAttr.VARIANT_3) { + ret |= DexAttr.VARIANT_3; + } else { + ret |= DexAttr.DEFAULT_VARIANT; + } + } ret |= attr & DexAttr.MALE || !(attr & DexAttr.FEMALE) ? DexAttr.MALE : DexAttr.FEMALE; - ret |= optimistic - ? attr & DexAttr.SHINY ? attr & DexAttr.VARIANT_3 ? DexAttr.VARIANT_3 : attr & DexAttr.VARIANT_2 ? DexAttr.VARIANT_2 : DexAttr.DEFAULT_VARIANT : DexAttr.DEFAULT_VARIANT - : attr & DexAttr.DEFAULT_VARIANT ? DexAttr.DEFAULT_VARIANT : attr & DexAttr.VARIANT_2 ? DexAttr.VARIANT_2 : attr & DexAttr.VARIANT_3 ? DexAttr.VARIANT_3 : DexAttr.DEFAULT_VARIANT; ret |= this.getFormAttr(this.getFormIndex(attr)); return ret; } @@ -1847,7 +1870,14 @@ export class GameData { getSpeciesDexAttrProps(species: PokemonSpecies, dexAttr: bigint): DexAttrProps { const shiny = !(dexAttr & DexAttr.NON_SHINY); const female = !(dexAttr & DexAttr.MALE); - const variant = dexAttr & DexAttr.DEFAULT_VARIANT ? 0 : dexAttr & DexAttr.VARIANT_2 ? 1 : dexAttr & DexAttr.VARIANT_3 ? 2 : 0; + let variant: Variant = 0; + if (dexAttr & DexAttr.DEFAULT_VARIANT) { + variant = 0; + } else if (dexAttr & DexAttr.VARIANT_2) { + variant = 1; + } else if (dexAttr & DexAttr.VARIANT_3) { + variant = 2; + } const formIndex = this.getFormIndex(dexAttr); return { diff --git a/src/ui/save-slot-select-ui-handler.ts b/src/ui/save-slot-select-ui-handler.ts index 7bfecb1f99b..b36c0af6ec8 100644 --- a/src/ui/save-slot-select-ui-handler.ts +++ b/src/ui/save-slot-select-ui-handler.ts @@ -132,7 +132,7 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler { }, () => { ui.revertMode(); ui.showText("", 0); - }, false, 0, 19, 2000); + }, false, 0, 19, import.meta.env.DEV ? 300 : 2000); }); } else if (this.sessionSlots[cursor].hasData === false) { saveAndCallback(); diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 22408ef829f..aa0139ff7e1 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -197,6 +197,15 @@ function findClosestStarterRow(index: number, numberOfRows: number) { return closestRowIndex; } +interface SpeciesDetails { + shiny?: boolean, + formIndex?: integer + female?: boolean, + variant?: Variant, + abilityIndex?: integer, + natureIndex?: integer, + forSeen?: boolean, // default = false +} export default class StarterSelectUiHandler extends MessageUiHandler { private starterSelectContainer: Phaser.GameObjects.Container; @@ -1593,11 +1602,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { if (!starterAttributes) { starterAttributes = this.starterPreferences[this.lastSpecies.speciesId] = {}; } - starterAttributes.nature = n as unknown as integer; + starterAttributes.nature = n; this.clearText(); ui.setMode(Mode.STARTER_SELECT); // set nature for starter - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, n, undefined); + this.setSpeciesDetails(this.lastSpecies, { natureIndex: n }); this.blockInput = false; return true; } @@ -1635,7 +1644,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { handler: () => { starterData.passiveAttr |= PassiveAttr.ENABLED; ui.setMode(Mode.STARTER_SELECT); - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies); return true; } }); @@ -1645,7 +1654,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { handler: () => { starterData.passiveAttr ^= PassiveAttr.ENABLED; ui.setMode(Mode.STARTER_SELECT); - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies); return true; } }); @@ -1864,7 +1873,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { // Change to shiny, we need to get the proper default variant const newProps = this.scene.gameData.getSpeciesDexAttrProps(this.lastSpecies, this.getCurrentDexProps(this.lastSpecies.speciesId)); const newVariant = starterAttributes.variant ? starterAttributes.variant as Variant : newProps.variant; - this.setSpeciesDetails(this.lastSpecies, true, undefined, undefined, newVariant, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies, { shiny: true, variant: newVariant }); this.scene.playSound("se/sparkle"); // Set the variant label to the shiny tint @@ -1873,12 +1882,40 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonShinyIcon.setTint(tint); this.pokemonShinyIcon.setVisible(true); } else { - this.setSpeciesDetails(this.lastSpecies, false, undefined, undefined, 0, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies, { shiny: false, variant: 0 }); this.pokemonShinyIcon.setVisible(false); success = true; } } break; + case Button.V: + if (this.canCycleVariant) { + let newVariant = props.variant; + do { + newVariant = (newVariant + 1) % 3; + if (newVariant === 0) { + if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.DEFAULT_VARIANT) { // TODO: is this bang correct? + break; + } + } else if (newVariant === 1) { + if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_2) { // TODO: is this bang correct? + break; + } + } else { + if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_3) { // TODO: is this bang correct? + break; + } + } + } while (newVariant !== props.variant); + starterAttributes.variant = newVariant; // store the selected variant + this.setSpeciesDetails(this.lastSpecies, { variant: newVariant as Variant }); + // Cycle tint based on current sprite tint + const tint = getVariantTint(newVariant as Variant); + this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant as Variant)); + this.pokemonShinyIcon.setTint(tint); + success = true; + } + break; case Button.CYCLE_FORM: if (this.canCycleForm) { const formCount = this.lastSpecies.forms.length; @@ -1890,14 +1927,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } } while (newFormIndex !== props.formIndex); starterAttributes.form = newFormIndex; // store the selected form - this.setSpeciesDetails(this.lastSpecies, undefined, newFormIndex, undefined, undefined, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies, { formIndex: newFormIndex }); success = true; } break; case Button.CYCLE_GENDER: if (this.canCycleGender) { starterAttributes.female = !props.female; - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, !props.female, undefined, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies, { female: !props.female }); success = true; } break; @@ -1934,7 +1971,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.scene.ui.editTooltip(`${newAbility.name}`, `${newAbility.description}`); } - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, newAbilityIndex, undefined); + this.setSpeciesDetails(this.lastSpecies, { abilityIndex: newAbilityIndex }); success = true; } break; @@ -1945,35 +1982,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const newNature = natures[natureIndex < natures.length - 1 ? natureIndex + 1 : 0]; // store cycled nature as default starterAttributes.nature = newNature as unknown as integer; - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, newNature, undefined); - success = true; - } - break; - case Button.V: - if (this.canCycleVariant) { - let newVariant = props.variant; - do { - newVariant = (newVariant + 1) % 3; - if (!newVariant) { - if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.DEFAULT_VARIANT) { // TODO: is this bang correct? - break; - } - } else if (newVariant === 1) { - if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_2) { // TODO: is this bang correct? - break; - } - } else { - if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_3) { // TODO: is this bang correct? - break; - } - } - } while (newVariant !== props.variant); - starterAttributes.variant = newVariant; // store the selected variant - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, newVariant as Variant, undefined, undefined); - // Cycle tint based on current sprite tint - const tint = getVariantTint(newVariant as Variant); - this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant as Variant)); - this.pokemonShinyIcon.setTint(tint); + this.setSpeciesDetails(this.lastSpecies, { natureIndex: newNature }); success = true; } break; @@ -2190,7 +2199,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } else { this.scene.gameData.starterData[speciesId].moveset = this.starterMoveset?.slice(0) as StarterMoveset; } - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, undefined, false); + this.setSpeciesDetails(this.lastSpecies, { forSeen: false }); // switch moves of starter if exists if (this.starterMovesets.length) { @@ -2875,7 +2884,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler { if (starterIndex > -1) { props = this.scene.gameData.getSpeciesDexAttrProps(species, this.starterAttr[starterIndex]); - this.setSpeciesDetails(species, props.shiny, props.formIndex, props.female, props.variant, this.starterAbilityIndexes[starterIndex], this.starterNatures[starterIndex]); + this.setSpeciesDetails(species, { + shiny: props.shiny, + formIndex: props.formIndex, + female: props.female, + variant: props.variant, + abilityIndex: this.starterAbilityIndexes[starterIndex], + natureIndex: this.starterNatures[starterIndex] + }); } else { const defaultDexAttr = this.getCurrentDexProps(species.speciesId); const defaultAbilityIndex = starterAttributes?.ability ?? this.scene.gameData.getStarterSpeciesDefaultAbilityIndex(species); @@ -2890,7 +2906,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler { props.formIndex = starterAttributes?.form ?? props.formIndex; props.female = starterAttributes?.female ?? props.female; - this.setSpeciesDetails(species, props.shiny, props.formIndex, props.female, props.variant, defaultAbilityIndex, defaultNature); + this.setSpeciesDetails(species, { + shiny: props.shiny, + formIndex: props.formIndex, + female: props.female, + variant: props.variant, + abilityIndex: defaultAbilityIndex, + natureIndex: defaultNature + }); } const speciesForm = getPokemonSpeciesForm(species.speciesId, props.formIndex); @@ -2924,7 +2947,15 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const defaultNature = this.scene.gameData.getSpeciesDefaultNature(species); const props = this.scene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); - this.setSpeciesDetails(species, props.shiny, props.formIndex, props.female, props.variant, defaultAbilityIndex, defaultNature, true); + this.setSpeciesDetails(species, { + shiny: props.shiny, + formIndex: props.formIndex, + female: props.female, + variant: props.variant, + abilityIndex: defaultAbilityIndex, + natureIndex: defaultNature, + forSeen: true + }); this.pokemonSprite.setTint(0x808080); } } else { @@ -2948,13 +2979,21 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonCandyCountText.setVisible(false); this.pokemonFormText.setVisible(false); - this.setSpeciesDetails(species!, false, 0, false, 0, 0, 0); // TODO: is this bang correct? + this.setSpeciesDetails(species!, { // TODO: is this bang correct? + shiny: false, + formIndex: 0, + female: false, + variant: 0, + abilityIndex: 0, + natureIndex: 0 + }); this.pokemonSprite.clearTint(); } } - - setSpeciesDetails(species: PokemonSpecies, shiny?: boolean, formIndex?: integer, female?: boolean, variant?: Variant, abilityIndex?: integer, natureIndex?: integer, forSeen: boolean = false): void { + setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void { + let { shiny, formIndex, female, variant, abilityIndex, natureIndex } = options; + const forSeen: boolean = options.forSeen ?? false; const oldProps = species ? this.scene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null; const oldAbilityIndex = this.abilityCursor > -1 ? this.abilityCursor : this.scene.gameData.getStarterSpeciesDefaultAbilityIndex(species); const oldNatureIndex = this.natureCursor > -1 ? this.natureCursor : this.scene.gameData.getSpeciesDefaultNature(species); From 1619d512cf81a38a07b6f69579d8bd62727c3da4 Mon Sep 17 00:00:00 2001 From: muscode Date: Sun, 3 Nov 2024 20:56:54 -0600 Subject: [PATCH 15/98] [Move] Implement Fairy Lock (#4736) * inital wip * wip set isTrapped to true and removed timedtrap * implement fairy lock * whitespace fix * added documentation for tag * added more suggested tests * give tests breathing room Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * give tests breathing room Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * give tests breathing room Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * updated nested ternary, changed test name --------- Co-authored-by: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/data/arena-tag.ts | 20 ++++ src/data/move.ts | 3 +- src/enums/arena-tag-type.ts | 1 + src/field/pokemon.ts | 3 +- src/phases/command-phase.ts | 43 ++++++--- src/test/moves/fairy_lock.test.ts | 152 ++++++++++++++++++++++++++++++ 6 files changed, 205 insertions(+), 17 deletions(-) create mode 100644 src/test/moves/fairy_lock.test.ts diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 43de6e02dcb..fce46f0c98d 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1203,6 +1203,24 @@ class GrassWaterPledgeTag extends ArenaTag { } } +/** + * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Fairy_Lock_(move) Fairy Lock}. + * Fairy Lock prevents all Pokémon (except Ghost types) on the field from switching out or + * fleeing during their next turn. + * If a Pokémon that's on the field when Fairy Lock is used goes on to faint later in the same turn, + * the Pokémon that replaces it will still be unable to switch out in the following turn. + */ +export class FairyLockTag extends ArenaTag { + constructor(turnCount: number, sourceId: number) { + super(ArenaTagType.FAIRY_LOCK, turnCount, Moves.FAIRY_LOCK, sourceId); + } + + onAdd(arena: Arena): void { + arena.scene.queueMessage(i18next.t("arenaTag:fairyLockOnAdd")); + } + +} + // TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove: Moves | undefined, sourceId: number, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { switch (tagType) { @@ -1261,6 +1279,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove return new WaterFirePledgeTag(sourceId, side); case ArenaTagType.GRASS_WATER_PLEDGE: return new GrassWaterPledgeTag(sourceId, side); + case ArenaTagType.FAIRY_LOCK: + return new FairyLockTag(turnCount, sourceId); default: return null; } diff --git a/src/data/move.ts b/src/data/move.ts index 2fc34d13df5..43a57db1b6d 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -9258,8 +9258,9 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS), new StatusMove(Moves.FAIRY_LOCK, Type.FAIRY, -1, 10, -1, 0, 6) .ignoresSubstitute() + .ignoresProtect() .target(MoveTarget.BOTH_SIDES) - .unimplemented(), + .attr(AddArenaTagAttr, ArenaTagType.FAIRY_LOCK, 2, true), new SelfStatusMove(Moves.KINGS_SHIELD, Type.STEEL, -1, 10, -1, 4, 6) .attr(ProtectAttr, BattlerTagType.KINGS_SHIELD) .condition(failIfLastCondition), diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index c73f4ec2ae5..1c62ccb14a6 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -28,4 +28,5 @@ export enum ArenaTagType { FIRE_GRASS_PLEDGE = "FIRE_GRASS_PLEDGE", WATER_FIRE_PLEDGE = "WATER_FIRE_PLEDGE", GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE", + FAIRY_LOCK = "FAIRY_LOCK", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 2dcef4b3965..2214dfecac5 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1565,7 +1565,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyCheckTrappedAbAttrs(CheckTrappedAbAttr, opponent, trappedByAbility, this, trappedAbMessages, simulated) ); - return (trappedByAbility.value || !!this.getTag(TrappedTag)); + const side = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + return (trappedByAbility.value || !!this.getTag(TrappedTag) || !!this.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, side)); } /** diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 6d4d46c51c9..c1d38815c2f 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -17,6 +17,8 @@ import { FieldPhase } from "./field-phase"; import { SelectTargetPhase } from "./select-target-phase"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { isNullOrUndefined } from "#app/utils"; +import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagType } from "#app/enums/arena-tag-type"; export class CommandPhase extends FieldPhase { protected fieldIndex: integer; @@ -228,32 +230,43 @@ export class CommandPhase extends FieldPhase { }, null, true); } else { const trapTag = playerPokemon.getTag(TrappedTag); + const fairyLockTag = playerPokemon.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER); // trapTag should be defined at this point, but just in case... - if (!trapTag) { + if (!trapTag && !fairyLockTag) { currentBattle.turnCommands[this.fieldIndex] = isSwitch ? { command: Command.POKEMON, cursor: cursor, args: args } : { command: Command.RUN }; break; } - if (!isSwitch) { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); this.scene.ui.setMode(Mode.MESSAGE); } - this.scene.ui.showText( - i18next.t("battle:noEscapePokemon", { - pokemonName: trapTag.sourceId && this.scene.getPokemonById(trapTag.sourceId) ? getPokemonNameWithAffix(this.scene.getPokemonById(trapTag.sourceId)!) : "", - moveName: trapTag.getMoveName(), - escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee") - }), - null, - () => { - this.scene.ui.showText("", 0); - if (!isSwitch) { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - } - }, null, true); + const showNoEscapeText = (tag: any) => { + this.scene.ui.showText( + i18next.t("battle:noEscapePokemon", { + pokemonName: tag.sourceId && this.scene.getPokemonById(tag.sourceId) ? getPokemonNameWithAffix(this.scene.getPokemonById(tag.sourceId)!) : "", + moveName: tag.getMoveName(), + escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee") + }), + null, + () => { + this.scene.ui.showText("", 0); + if (!isSwitch) { + this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + } + }, + null, + true + ); + }; + + if (trapTag) { + showNoEscapeText(trapTag); + } else if (fairyLockTag) { + showNoEscapeText(fairyLockTag); + } } } break; diff --git a/src/test/moves/fairy_lock.test.ts b/src/test/moves/fairy_lock.test.ts new file mode 100644 index 00000000000..ceb298ed0fe --- /dev/null +++ b/src/test/moves/fairy_lock.test.ts @@ -0,0 +1,152 @@ +import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagType } from "#app/enums/arena-tag-type"; +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"; + +describe("Moves - Fairy Lock", () => { + 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.FAIRY_LOCK, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("double") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([ Moves.SPLASH, Moves.U_TURN ]); + }); + + it("Applies Fairy Lock tag for two turns", async () => { + await game.classicMode.startBattle([ Species.KLEFKI, Species.TYRUNT ]); + const playerPokemon = game.scene.getPlayerField(); + const enemyField = game.scene.getEnemyField(); + + game.move.select(Moves.FAIRY_LOCK); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER)).toBeDefined(); + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.ENEMY)).toBeDefined(); + + await game.toNextTurn(); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + expect(playerPokemon[0].isTrapped()).toEqual(true); + expect(playerPokemon[1].isTrapped()).toEqual(true); + expect(enemyField[0].isTrapped()).toEqual(true); + expect(enemyField[1].isTrapped()).toEqual(true); + + await game.toNextTurn(); + expect(playerPokemon[0].isTrapped()).toEqual(false); + expect(playerPokemon[1].isTrapped()).toEqual(false); + expect(enemyField[0].isTrapped()).toEqual(false); + expect(enemyField[1].isTrapped()).toEqual(false); + }); + + it("Ghost types can escape Fairy Lock", async () => { + await game.classicMode.startBattle([ Species.DUSKNOIR, Species.GENGAR, Species.TYRUNT ]); + + game.move.select(Moves.FAIRY_LOCK); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER)).toBeDefined(); + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.ENEMY)).toBeDefined(); + + await game.toNextTurn(); + + expect(game.scene.getPlayerField()[0].isTrapped()).toEqual(false); + expect(game.scene.getPlayerField()[1].isTrapped()).toEqual(false); + + game.move.select(Moves.SPLASH); + game.doSwitchPokemon(2); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + await game.toNextTurn(); + + expect(game.scene.getPlayerField()[1].species.speciesId).not.toBe(Species.GENGAR); + }); + + it("Phasing moves will still switch out", async () => { + game.override.enemyMoveset([ Moves.SPLASH, Moves.WHIRLWIND ]); + await game.classicMode.startBattle([ Species.KLEFKI, Species.TYRUNT, Species.ZYGARDE ]); + + game.move.select(Moves.FAIRY_LOCK); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER)).toBeDefined(); + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.ENEMY)).toBeDefined(); + + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.WHIRLWIND, 0); + game.doSelectPartyPokemon(2); + await game.forceEnemyMove(Moves.WHIRLWIND, 1); + game.doSelectPartyPokemon(2); + await game.phaseInterceptor.to("BerryPhase"); + await game.toNextTurn(); + + expect(game.scene.getPlayerField()[0].species.speciesId).not.toBe(Species.KLEFKI); + expect(game.scene.getPlayerField()[1].species.speciesId).not.toBe(Species.TYRUNT); + }); + + it("If a Pokemon faints and is replaced the replacement is also trapped", async () => { + game.override.moveset([ Moves.FAIRY_LOCK, Moves.SPLASH, Moves.MEMENTO ]); + await game.classicMode.startBattle([ Species.KLEFKI, Species.GUZZLORD, Species.TYRUNT, Species.ZYGARDE ]); + + game.move.select(Moves.FAIRY_LOCK); + game.move.select(Moves.MEMENTO, 1); + game.doSelectPartyPokemon(2); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER)).toBeDefined(); + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.ENEMY)).toBeDefined(); + + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerField()[0].isTrapped()).toEqual(true); + expect(game.scene.getPlayerField()[1].isTrapped()).toEqual(true); + expect(game.scene.getEnemyField()[0].isTrapped()).toEqual(true); + expect(game.scene.getEnemyField()[1].isTrapped()).toEqual(true); + + await game.toNextTurn(); + expect(game.scene.getPlayerField()[0].isTrapped()).toEqual(false); + expect(game.scene.getPlayerField()[1].isTrapped()).toEqual(false); + expect(game.scene.getEnemyField()[0].isTrapped()).toEqual(false); + expect(game.scene.getEnemyField()[1].isTrapped()).toEqual(false); + }); +}); From 3f97c9e39fc65b594e7f0c8cd6ebf2c5b01f6e5f Mon Sep 17 00:00:00 2001 From: muscode Date: Sun, 3 Nov 2024 20:58:12 -0600 Subject: [PATCH 16/98] [Ability] Implement Unburden (#4534) * unburden implemented * Used tag instead of stat changes for Unburden * added documentation and neutralizing gas test * accounted for unburden in getEffectiveStat * fixed doubling speed in two places * merge conflict resolve * changed documentation wording, added test for stuff cheeks * refactor unburden * merge * remove console logs * Update src/data/ability.ts Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * add neut gas check Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Added test for NeutGas user entering while unburden activated * add spaces to tests, removed passive and [] from applyPostItemLostAbAttrs * added line breaks after test cases --------- Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/battle-scene.ts | 16 +- src/data/ability.ts | 42 ++++- src/data/battler-tags.ts | 18 ++ src/data/berry.ts | 8 +- src/data/move.ts | 6 +- src/enums/battler-tag-type.ts | 1 + src/field/pokemon.ts | 3 + src/test/abilities/unburden.test.ts | 245 ++++++++++++++++++++++++++++ 8 files changed, 333 insertions(+), 6 deletions(-) create mode 100644 src/test/abilities/unburden.test.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c731677775d..415a8fe9aaf 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -15,7 +15,7 @@ import { addTextObject, getTextColor, TextStyle } from "#app/ui/text"; import { allMoves } from "#app/data/move"; import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import AbilityBar from "#app/ui/ability-bar"; -import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr } from "#app/data/ability"; +import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, applyPostItemLostAbAttrs, BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr, PostItemLostAbAttr } from "#app/data/ability"; import Battle, { BattleType, FixedBattleConfig } from "#app/battle"; import { GameMode, GameModes, getGameMode } from "#app/game-mode"; import FieldSpritePipeline from "#app/pipelines/field-sprite"; @@ -2601,9 +2601,19 @@ export default class BattleScene extends SceneBase { const addModifier = () => { if (!matchingModifier || this.removeModifier(matchingModifier, !target.isPlayer())) { if (target.isPlayer()) { - this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant).then(() => resolve(true)); + this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant).then(() => { + if (source) { + applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false); + } + resolve(true); + }); } else { - this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => resolve(true)); + this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => { + if (source) { + applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false); + } + resolve(true); + }); } } else { resolve(false); diff --git a/src/data/ability.ts b/src/data/ability.ts index 8ec0f296068..8eeb5af52b8 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3857,6 +3857,41 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { } } +/** + * Triggers after the Pokemon loses or consumes an item + * @extends AbAttr + */ +export class PostItemLostAbAttr extends AbAttr { + applyPostItemLost(pokemon: Pokemon, simulated: boolean, args: any[]): boolean | Promise { + return false; + } +} + +/** + * Applies a Battler Tag to the Pokemon after it loses or consumes item + * @extends PostItemLostAbAttr + */ +export class PostItemLostApplyBattlerTagAbAttr extends PostItemLostAbAttr { + private tagType: BattlerTagType; + constructor(tagType: BattlerTagType) { + super(true); + this.tagType = tagType; + } + /** + * Adds the last used Pokeball back into the player's inventory + * @param pokemon {@linkcode Pokemon} with this ability + * @param args N/A + * @returns true if BattlerTag was applied + */ + applyPostItemLost(pokemon: Pokemon, simulated: boolean, args: any[]): boolean | Promise { + if (!pokemon.getTag(this.tagType) && !simulated) { + pokemon.addTag(this.tagType); + return true; + } + return false; + } +} + export class StatStageChangeMultiplierAbAttr extends AbAttr { private multiplier: integer; @@ -5228,6 +5263,11 @@ export function applyPostFaintAbAttrs(attrType: Constructor, return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostFaint(pokemon, passive, simulated, attacker, move, hitResult, args), args, false, simulated); } +export function applyPostItemLostAbAttrs(attrType: Constructor, + pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostItemLost(pokemon, simulated, args), args); +} + function queueShowAbility(pokemon: Pokemon, passive: boolean): void { pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.id, passive)); pokemon.scene.clearPhaseQueueSplice(); @@ -5521,7 +5561,7 @@ export function initAbilities() { new Ability(Abilities.ANGER_POINT, 4) .attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6), new Ability(Abilities.UNBURDEN, 4) - .unimplemented(), + .attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN), new Ability(Abilities.HEATPROOF, 4) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) .attr(ReduceBurnDamageAbAttr, 0.5) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index d671c56ab26..3051c2061a8 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1573,6 +1573,22 @@ export class AbilityBattlerTag extends BattlerTag { } } +/** + * Tag used by Unburden to double speed + * @extends AbilityBattlerTag + */ +export class UnburdenTag extends AbilityBattlerTag { + constructor() { + super(BattlerTagType.UNBURDEN, Abilities.UNBURDEN, BattlerTagLapseType.CUSTOM, 1); + } + onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + } + onRemove(pokemon: Pokemon): void { + super.onRemove(pokemon); + } +} + export class TruantTag extends AbilityBattlerTag { constructor() { super(BattlerTagType.TRUANT, Abilities.TRUANT, BattlerTagLapseType.MOVE, 1); @@ -2934,6 +2950,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new ThroatChoppedTag(); case BattlerTagType.GORILLA_TACTICS: return new GorillaTacticsTag(); + case BattlerTagType.UNBURDEN: + return new UnburdenTag(); case BattlerTagType.SUBSTITUTE: return new SubstituteTag(sourceMove, sourceId); case BattlerTagType.AUTOTOMIZED: diff --git a/src/data/berry.ts b/src/data/berry.ts index 7243c4c1b2e..d2bbd0fdd1c 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -2,7 +2,7 @@ import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { HitResult } from "../field/pokemon"; import { getStatusEffectHealText } from "./status-effect"; import * as Utils from "../utils"; -import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs } from "./ability"; +import { DoubleBerryEffectAbAttr, PostItemLostAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs, applyPostItemLostAbAttrs } from "./ability"; import i18next from "i18next"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -75,6 +75,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed); pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), hpHealed.value, i18next.t("battle:hpHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: getBerryName(berryType) }), true)); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.LUM: return (pokemon: Pokemon) => { @@ -86,6 +87,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { } pokemon.resetStatus(true, true); pokemon.updateInfo(); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.LIECHI: case BerryType.GANLON: @@ -101,6 +103,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { const statStages = new Utils.NumberHolder(1); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value)); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.LANSAT: return (pokemon: Pokemon) => { @@ -108,6 +111,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { pokemon.battleData.berriesEaten.push(berryType); } pokemon.addTag(BattlerTagType.CRIT_BOOST); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.STARF: return (pokemon: Pokemon) => { @@ -118,6 +122,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { const stages = new Utils.NumberHolder(2); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], stages.value)); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.LEPPA: return (pokemon: Pokemon) => { @@ -128,6 +133,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { if (ppRestoreMove !== undefined) { ppRestoreMove!.ppUsed = Math.max(ppRestoreMove!.ppUsed - 10, 0); pokemon.scene.queueMessage(i18next.t("battle:ppHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: ppRestoreMove!.getName(), berryName: getBerryName(berryType) })); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); } }; } diff --git a/src/data/move.ts b/src/data/move.ts index 43a57db1b6d..9edcbceccf3 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; +import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPostItemLostAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, PostItemLostAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; import { TerrainType } from "./terrain"; @@ -2402,6 +2402,8 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { // Decrease item amount and update icon !--removedItem.stackCount; target.scene.updateModifiers(target.isPlayer()); + applyPostItemLostAbAttrs(PostItemLostAbAttr, target, false); + if (this.berriesOnly) { user.scene.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); @@ -2481,6 +2483,7 @@ export class EatBerryAttr extends MoveEffectAttr { eatBerry(consumer: Pokemon) { getBerryEffectFunc(this.chosenBerry!.berryType)(consumer); // consumer eats the berry applyAbAttrs(HealFromBerryUseAbAttr, consumer, new Utils.BooleanHolder(false)); + applyPostItemLostAbAttrs(PostItemLostAbAttr, consumer, false); } } @@ -2516,6 +2519,7 @@ export class StealEatBerryAttr extends EatBerryAttr { } // if the target has berries, pick a random berry and steal it this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)]; + applyPostItemLostAbAttrs(PostItemLostAbAttr, target, false); const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); user.scene.queueMessage(message); this.reduceBerryModifier(target); diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 680dedb93cc..0eace9a1607 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -74,6 +74,7 @@ export enum BattlerTagType { DRAGON_CHEER = "DRAGON_CHEER", NO_RETREAT = "NO_RETREAT", GORILLA_TACTICS = "GORILLA_TACTICS", + UNBURDEN = "UNBURDEN", THROAT_CHOPPED = "THROAT_CHOPPED", TAR_SHOT = "TAR_SHOT", BURNED_UP = "BURNED_UP", diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 2214dfecac5..c4d2424f677 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -982,6 +982,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.status && this.status.effect === StatusEffect.PARALYSIS) { ret >>= 1; } + if (this.getTag(BattlerTagType.UNBURDEN) && !this.scene.getField(true).some(pokemon => pokemon !== this && pokemon.hasAbilityWithAttr(SuppressFieldAbilitiesAbAttr))) { + ret *= 2; + } break; } diff --git a/src/test/abilities/unburden.test.ts b/src/test/abilities/unburden.test.ts new file mode 100644 index 00000000000..33604efc534 --- /dev/null +++ b/src/test/abilities/unburden.test.ts @@ -0,0 +1,245 @@ +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, vi } from "vitest"; +import { Stat } from "#enums/stat"; +import { BerryType } from "#app/enums/berry-type"; +import { allMoves, StealHeldItemChanceAttr } from "#app/data/move"; + + +describe("Abilities - Unburden", () => { + 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") + .starterSpecies(Species.TREECKO) + .startingLevel(1) + .moveset([ Moves.POPULATION_BOMB, Moves.KNOCK_OFF, Moves.PLUCK, Moves.THIEF ]) + .startingHeldItems([ + { name: "BERRY", count: 1, type: BerryType.SITRUS }, + { name: "BERRY", count: 2, type: BerryType.APICOT }, + { name: "BERRY", count: 2, type: BerryType.LUM }, + ]) + .enemySpecies(Species.NINJASK) + .enemyLevel(100) + .enemyMoveset([ Moves.FALSE_SWIPE ]) + .enemyAbility(Abilities.UNBURDEN) + .enemyPassiveAbility(Abilities.NO_GUARD) + .enemyHeldItems([ + { name: "BERRY", type: BerryType.SITRUS, count: 1 }, + { name: "BERRY", type: BerryType.LUM, count: 1 }, + ]); + }); + + it("should activate when a berry is eaten", async () => { + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.abilityIndex = 2; + const playerHeldItems = playerPokemon.getHeldItems().length; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); + + expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + }); + + it("should activate when a berry is stolen", async () => { + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.PLUCK); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should activate when an item is knocked off", async () => { + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.KNOCK_OFF); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should activate when an item is stolen via attacking ability", async () => { + game.override + .ability(Abilities.MAGICIAN) + .startingHeldItems([ + { name: "MULTI_LENS", count: 3 }, + ]); + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should activate when an item is stolen via defending ability", async () => { + game.override + .startingLevel(45) + .enemyAbility(Abilities.PICKPOCKET) + .startingHeldItems([ + { name: "MULTI_LENS", count: 3 }, + { name: "SOUL_DEW", count: 1 }, + { name: "LUCKY_EGG", count: 1 }, + ]); + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.abilityIndex = 2; + const playerHeldItems = playerPokemon.getHeldItems().length; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + + expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + }); + + it("should activate when an item is stolen via move", async () => { + vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([ new StealHeldItemChanceAttr(1.0) ]); // give Thief 100% steal rate + game.override.startingHeldItems([ + { name: "MULTI_LENS", count: 3 }, + ]); + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.THIEF); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should activate when an item is stolen via grip claw", async () => { + game.override + .startingLevel(5) + .startingHeldItems([ + { name: "GRIP_CLAW", count: 5 }, + { name: "MULTI_LENS", count: 3 }, + ]) + .enemyHeldItems([ + { name: "SOUL_DEW", count: 1 }, + { name: "LUCKY_EGG", count: 1 }, + { name: "LEFTOVERS", count: 1 }, + { name: "GRIP_CLAW", count: 1 }, + { name: "MULTI_LENS", count: 1 }, + { name: "BERRY", type: BerryType.SITRUS, count: 1 }, + { name: "BERRY", type: BerryType.LUM, count: 1 }, + ]); + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + while (enemyPokemon.getHeldItems().length === enemyHeldItemCt) { + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + } + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should not activate when a neutralizing ability is present", async () => { + game.override.enemyAbility(Abilities.NEUTRALIZING_GAS); + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = playerPokemon.getHeldItems().length; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); + + expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed); + }); + + it("should activate when a move that consumes a berry is used", async () => { + game.override.enemyMoveset([ Moves.STUFF_CHEEKS ]); + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.STUFF_CHEEKS); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should deactivate when a neutralizing gas user enters the field", async () => { + game.override + .battleType("double") + .moveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.TREECKO, Species.MEOWTH, Species.WEEZING ]); + + const playerPokemon = game.scene.getParty(); + const treecko = playerPokemon[0]; + const weezing = playerPokemon[2]; + treecko.abilityIndex = 2; + weezing.abilityIndex = 1; + const playerHeldItems = treecko.getHeldItems().length; + const initialPlayerSpeed = treecko.getStat(Stat.SPD); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.FALSE_SWIPE, 0); + await game.forceEnemyMove(Moves.FALSE_SWIPE, 0); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(treecko.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + game.doSwitchPokemon(2); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(treecko.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed); + }); + +}); From c3d832aaca14c4daba5a81c782d66ec41679f210 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sun, 3 Nov 2024 18:59:55 -0800 Subject: [PATCH 17/98] [Move] Fully Implement Round (#4783) --- src/data/move.ts | 59 ++++++++++++++++++++++++++++++-- src/field/pokemon.ts | 1 + src/test/moves/round.test.ts | 65 ++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/test/moves/round.test.ts diff --git a/src/data/move.ts b/src/data/move.ts index 9edcbceccf3..6e350315e65 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4161,6 +4161,60 @@ export class CombinedPledgeStabBoostAttr extends MoveAttr { } } +/** + * Variable Power attribute for {@link https://bulbapedia.bulbagarden.net/wiki/Round_(move) | Round}. + * Doubles power if another Pokemon has previously selected Round this turn. + * @extends VariablePowerAttr + */ +export class RoundPowerAttr extends VariablePowerAttr { + override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + const power = args[0]; + if (!(power instanceof Utils.NumberHolder)) { + return false; + } + + if (user.turnData?.joinedRound) { + power.value *= 2; + return true; + } + return false; + } +} + +/** + * Attribute for the "combo" effect of {@link https://bulbapedia.bulbagarden.net/wiki/Round_(move) | Round}. + * Preempts the next move in the turn order with the first instance of any Pokemon + * using Round. Also marks the Pokemon using the cued Round to double the move's power. + * @extends MoveEffectAttr + * @see {@linkcode RoundPowerAttr} + */ +export class CueNextRoundAttr extends MoveEffectAttr { + constructor() { + super(true, { lastHitOnly: true }); + } + + override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { + const nextRoundPhase = user.scene.findPhase(phase => + phase instanceof MovePhase && phase.move.moveId === Moves.ROUND + ); + + if (!nextRoundPhase) { + return false; + } + + // Update the phase queue so that the next Pokemon using Round moves next + const nextRoundIndex = user.scene.phaseQueue.indexOf(nextRoundPhase); + const nextMoveIndex = user.scene.phaseQueue.findIndex(phase => phase instanceof MovePhase); + if (nextRoundIndex !== nextMoveIndex) { + user.scene.prependToPhase(user.scene.phaseQueue.splice(nextRoundIndex, 1)[0], MovePhase); + } + + // Mark the corresponding Pokemon as having "joined the Round" (for doubling power later) + nextRoundPhase.pokemon.turnData.joinedRound = true; + return true; + } +} + export class VariableAtkAttr extends MoveAttr { constructor() { super(); @@ -8960,8 +9014,9 @@ export function initMoves() { .condition((user, target, move) => !target.turnData.acted) .attr(AfterYouAttr), new AttackMove(Moves.ROUND, Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5) - .soundBased() - .partial(), // No effect implemented + .attr(CueNextRoundAttr) + .attr(RoundPowerAttr) + .soundBased(), new AttackMove(Moves.ECHOED_VOICE, Type.NORMAL, MoveCategory.SPECIAL, 40, 100, 15, -1, 0, 5) .attr(ConsecutiveUseMultiBasePowerAttr, 5, false) .soundBased(), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index c4d2424f677..5506c35e18a 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5194,6 +5194,7 @@ export class PokemonTurnData { public combiningPledge?: Moves; public switchedInThisTurn: boolean = false; public failedRunAway: boolean = false; + public joinedRound: boolean = false; } export enum AiType { diff --git a/src/test/moves/round.test.ts b/src/test/moves/round.test.ts new file mode 100644 index 00000000000..fd318d30c1e --- /dev/null +++ b/src/test/moves/round.test.ts @@ -0,0 +1,65 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +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, vi } from "vitest"; + +describe("Moves - Round", () => { + 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.ROUND ]) + .ability(Abilities.BALL_FETCH) + .battleType("double") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([ Moves.SPLASH, Moves.ROUND ]) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should cue other instances of Round together in Speed order", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + + const round = allMoves[Moves.ROUND]; + const spy = vi.spyOn(round, "calculateBattlePower"); + + game.move.select(Moves.ROUND, 0, BattlerIndex.ENEMY); + game.move.select(Moves.ROUND, 1, BattlerIndex.ENEMY_2); + + await game.forceEnemyMove(Moves.ROUND, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY ]); + + const actualTurnOrder: BattlerIndex[] = []; + + for (let i = 0; i < 4; i++) { + await game.phaseInterceptor.to("MoveEffectPhase", false); + actualTurnOrder.push((game.scene.getCurrentPhase() as MoveEffectPhase).getUserPokemon()!.getBattlerIndex()); + await game.phaseInterceptor.to("MoveEndPhase"); + } + + expect(actualTurnOrder).toEqual([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); + const powerResults = spy.mock.results.map(result => result.value); + expect(powerResults).toEqual( [ 60, 120, 120 ]); + }); +}); From 62f9f6dd322c05abf94e169ecf3a2c291d981f83 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Mon, 4 Nov 2024 04:04:53 +0100 Subject: [PATCH 18/98] [UI] Starter Select UI no longer reloads sprite when changing nature, ability, etc (#4782) Co-authored-by: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> --- src/ui/starter-select-ui-handler.ts | 97 ++++++++++++++--------------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index aa0139ff7e1..3a29f9431e7 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -20,7 +20,6 @@ import { Type } from "#app/data/type"; import { GameModes } from "#app/game-mode"; import { AbilityAttr, DexAttr, DexAttrProps, DexEntry, StarterMoveset, StarterAttributes, StarterPreferences, StarterPrefs } from "#app/system/game-data"; import { Tutorial, handleTutorial } from "#app/tutorial"; -import * as Utils from "#app/utils"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import MessageUiHandler from "#app/ui/message-ui-handler"; import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler"; @@ -50,6 +49,7 @@ import { EncounterPhase } from "#app/phases/encounter-phase"; import { TitlePhase } from "#app/phases/title-phase"; import { Abilities } from "#enums/abilities"; import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters"; +import { BooleanHolder, capitalizeString, fixedInt, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, randIntRange, rgbHexToRgba, toReadableString } from "#app/utils"; export type StarterSelectCallback = (starters: Starter[]) => void; @@ -307,10 +307,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { private canCycleAbility: boolean; private canCycleNature: boolean; private canCycleVariant: boolean; - private value: integer = 0; - private canAddParty: boolean; - private assetLoadCancelled: Utils.BooleanHolder | null; + private assetLoadCancelled: BooleanHolder | null; public cursorObj: Phaser.GameObjects.Image; private starterCursorObjs: Phaser.GameObjects.Image[]; private pokerusCursorObjs: Phaser.GameObjects.Image[]; @@ -396,7 +394,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { if (index === 0 || index === 19) { return; } - const typeSprite = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("types")); + const typeSprite = this.scene.add.sprite(0, 0, getLocalizedSpriteKey("types")); typeSprite.setScale(0.5); typeSprite.setFrame(type.toLowerCase()); typeOptions.push(new DropDownOption(this.scene, index, new DropDownLabel("", typeSprite))); @@ -670,12 +668,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonSprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true }); this.starterSelectContainer.add(this.pokemonSprite); - this.type1Icon = this.scene.add.sprite(8, 98, Utils.getLocalizedSpriteKey("types")); + this.type1Icon = this.scene.add.sprite(8, 98, getLocalizedSpriteKey("types")); this.type1Icon.setScale(0.5); this.type1Icon.setOrigin(0, 0); this.starterSelectContainer.add(this.type1Icon); - this.type2Icon = this.scene.add.sprite(26, 98, Utils.getLocalizedSpriteKey("types")); + this.type2Icon = this.scene.add.sprite(26, 98, getLocalizedSpriteKey("types")); this.type2Icon.setScale(0.5); this.type2Icon.setOrigin(0, 0); this.starterSelectContainer.add(this.type2Icon); @@ -1142,20 +1140,20 @@ export default class StarterSelectUiHandler extends MessageUiHandler { targets: icon, loop: -1, // Make the initial bounce a little randomly delayed - delay: Utils.randIntRange(0, 50) * 5, + delay: randIntRange(0, 50) * 5, loopDelay: 1000, tweens: [ { targets: icon, y: 2 - 5, - duration: Utils.fixedInt(125), + duration: fixedInt(125), ease: "Cubic.easeOut", yoyo: true }, { targets: icon, y: 2 - 3, - duration: Utils.fixedInt(150), + duration: fixedInt(150), ease: "Cubic.easeOut", yoyo: true } @@ -1451,7 +1449,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const [ isDupe, removeIndex ]: [boolean, number] = this.isInParty(this.lastSpecies); // checks to see if the pokemon is a duplicate; if it is, returns the index that will be removed const isPartyValid = this.isPartyValid(); - const isValidForChallenge = new Utils.BooleanHolder(true); + const isValidForChallenge = new BooleanHolder(true); Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, this.lastSpecies, isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(this.lastSpecies, this.getCurrentDexProps(this.lastSpecies.speciesId)), isPartyValid); @@ -2329,8 +2327,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } - getValueLimit(): integer { - const valueLimit = new Utils.IntegerHolder(0); + getValueLimit(): number { + const valueLimit = new NumberHolder(0); switch (this.scene.gameMode.modeId) { case GameModes.ENDLESS: case GameModes.SPLICED_ENDLESS: @@ -2366,12 +2364,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler { * Since some pokemon rely on forms to be valid (i.e. blaze tauros for fire challenges), we make a fake form and dex props to use in the challenge */ const tempFormProps = BigInt(Math.pow(2, i)) * DexAttr.DEFAULT_FORM; - const isValidForChallenge = new Utils.BooleanHolder(true); + const isValidForChallenge = new BooleanHolder(true); Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, container.species, isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(species, tempFormProps), true); allFormsValid = allFormsValid || isValidForChallenge.value; } } else { - const isValidForChallenge = new Utils.BooleanHolder(true); + const isValidForChallenge = new BooleanHolder(true); Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, container.species, isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(species, this.scene.gameData.getSpeciesDefaultDexAttr(container.species, false, true)), true); allFormsValid = isValidForChallenge.value; } @@ -2633,8 +2631,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } // Set the candy colors - container.candyUpgradeIcon.setTint(argbFromRgba(Utils.rgbHexToRgba(starterColors[speciesId][0]))); - container.candyUpgradeOverlayIcon.setTint(argbFromRgba(Utils.rgbHexToRgba(starterColors[speciesId][1]))); + container.candyUpgradeIcon.setTint(argbFromRgba(rgbHexToRgba(starterColors[speciesId][0]))); + container.candyUpgradeOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(starterColors[speciesId][1]))); this.setUpgradeIcon(container); } else if (this.scene.candyUpgradeDisplay === 1) { @@ -2769,7 +2767,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.lastSpecies = species!; // TODO: is this bang correct? if (species && (this.speciesStarterDexEntry?.seenAttr || this.speciesStarterDexEntry?.caughtAttr)) { - this.pokemonNumberText.setText(Utils.padInt(species.speciesId, 4)); + this.pokemonNumberText.setText(padInt(species.speciesId, 4)); if (starterAttributes?.nickname) { const name = decodeURIComponent(escape(atob(starterAttributes.nickname))); this.pokemonNameText.setText(name); @@ -2787,7 +2785,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible); //Growth translate - let growthReadable = Utils.toReadableString(GrowthRate[species.growthRate]); + let growthReadable = toReadableString(GrowthRate[species.growthRate]); const growthAux = growthReadable.replace(" ", "_"); if (i18next.exists("growth:" + growthAux)) { growthReadable = i18next.t("growth:" + growthAux as any); @@ -2835,9 +2833,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } else { this.pokemonCaughtHatchedContainer.setY(25); this.pokemonShinyIcon.setY(117); - this.pokemonCandyIcon.setTint(argbFromRgba(Utils.rgbHexToRgba(colorScheme[0]))); + this.pokemonCandyIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[0]))); this.pokemonCandyIcon.setVisible(true); - this.pokemonCandyOverlayIcon.setTint(argbFromRgba(Utils.rgbHexToRgba(colorScheme[1]))); + this.pokemonCandyOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[1]))); this.pokemonCandyOverlayIcon.setVisible(true); this.pokemonCandyDarknessOverlay.setVisible(true); this.pokemonCandyCountText.setText(`x${this.scene.gameData.starterData[species.speciesId].candyCount}`); @@ -2959,7 +2957,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonSprite.setTint(0x808080); } } else { - this.pokemonNumberText.setText(Utils.padInt(0, 4)); + this.pokemonNumberText.setText(padInt(0, 4)); this.pokemonNameText.setText(species ? "???" : ""); this.pokemonGrowthRateText.setText(""); this.pokemonGrowthRateLabelText.setVisible(false); @@ -3001,6 +2999,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.abilityCursor = -1; this.natureCursor = -1; + // We will only update the sprite if there is a change to form, shiny/variant + // or gender for species with gender sprite differences + const shouldUpdateSprite = (species?.genderDiffs && !isNullOrUndefined(female)) + || !isNullOrUndefined(formIndex) || !isNullOrUndefined(shiny) || !isNullOrUndefined(variant); + if (this.activeTooltip === "CANDY") { if (this.lastSpecies) { const { currentFriendship, friendshipCap } = this.getFriendship(this.lastSpecies.speciesId); @@ -3089,24 +3092,27 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.starterNatures[starterIndex] = this.natureCursor; } - const assetLoadCancelled = new Utils.BooleanHolder(false); + const assetLoadCancelled = new BooleanHolder(false); this.assetLoadCancelled = assetLoadCancelled; - species.loadAssets(this.scene, female!, formIndex, shiny, variant, true).then(() => { // TODO: is this bang correct? - if (assetLoadCancelled.value) { - return; - } - this.assetLoadCancelled = null; - this.speciesLoaded.set(species.speciesId, true); - this.pokemonSprite.play(species.getSpriteKey(female!, formIndex, shiny, variant)); // TODO: is this bang correct? - this.pokemonSprite.setPipelineData("shiny", shiny); - this.pokemonSprite.setPipelineData("variant", variant); - this.pokemonSprite.setPipelineData("spriteKey", species.getSpriteKey(female!, formIndex, shiny, variant)); // TODO: is this bang correct? + if (shouldUpdateSprite) { + species.loadAssets(this.scene, female!, formIndex, shiny, variant, true).then(() => { // TODO: is this bang correct? + if (assetLoadCancelled.value) { + return; + } + this.assetLoadCancelled = null; + this.speciesLoaded.set(species.speciesId, true); + this.pokemonSprite.play(species.getSpriteKey(female!, formIndex, shiny, variant)); // TODO: is this bang correct? + this.pokemonSprite.setPipelineData("shiny", shiny); + this.pokemonSprite.setPipelineData("variant", variant); + this.pokemonSprite.setPipelineData("spriteKey", species.getSpriteKey(female!, formIndex, shiny, variant)); // TODO: is this bang correct? + this.pokemonSprite.setVisible(!this.statsMode); + }); + } else { this.pokemonSprite.setVisible(!this.statsMode); - }); + } - - const isValidForChallenge = new Utils.BooleanHolder(true); + const isValidForChallenge = new BooleanHolder(true); Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, species, isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor), !!this.starterSpecies.length); const currentFilteredContainer = this.filteredStarterContainers.find(p => p.species.speciesId === species.speciesId); if (currentFilteredContainer) { @@ -3263,9 +3269,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler { }) as StarterMoveset; const speciesForm = getPokemonSpeciesForm(species.speciesId, formIndex!); // TODO: is the bang correct? - const formText = Utils.capitalizeString(species?.forms[formIndex!]?.formKey, "-", false, false); // TODO: is the bang correct? + const formText = capitalizeString(species?.forms[formIndex!]?.formKey, "-", false, false); // TODO: is the bang correct? - const speciesName = Utils.capitalizeString(Species[species.speciesId], "_", true, false); + const speciesName = capitalizeString(Species[species.speciesId], "_", true, false); if (species.speciesId === Species.ARCEUS) { this.pokemonFormText.setText(i18next.t(`pokemonInfo:Type.${formText?.toUpperCase()}`)); @@ -3431,12 +3437,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.valueLimitLabel.setColor(this.getTextColor(!overLimit ? TextStyle.TOOLTIP_CONTENT : TextStyle.SUMMARY_PINK)); this.valueLimitLabel.setShadowColor(this.getTextColor(!overLimit ? TextStyle.TOOLTIP_CONTENT : TextStyle.SUMMARY_PINK, true)); if (overLimit) { - this.scene.time.delayedCall(Utils.fixedInt(500), () => this.tryUpdateValue()); + this.scene.time.delayedCall(fixedInt(500), () => this.tryUpdateValue()); return false; } let isPartyValid: boolean = this.isPartyValid(); // this checks to see if the party is valid if (addingToParty) { // this does a check to see if the pokemon being added is valid; if so, it will update the isPartyValid boolean - const isNewPokemonValid = new Utils.BooleanHolder(true); + const isNewPokemonValid = new BooleanHolder(true); const species = this.filteredStarterContainers[this.cursor].species; Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, species, isNewPokemonValid, this.scene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), false); isPartyValid = isPartyValid || isNewPokemonValid.value; @@ -3445,13 +3451,10 @@ export default class StarterSelectUiHandler extends MessageUiHandler { /** * this loop is used to set the Sprite's alpha value and check if the user can select other pokemon more. */ - this.canAddParty = false; const remainValue = valueLimit - newValue; for (let s = 0; s < this.allSpecies.length; s++) { /** Cost of pokemon species */ const speciesStarterValue = this.scene.gameData.getSpeciesStarterValue(this.allSpecies[s].speciesId); - /** Used to detect if this pokemon is registered in starter */ - const speciesStarterDexEntry = this.scene.gameData.dexData[this.allSpecies[s].speciesId]; /** {@linkcode Phaser.GameObjects.Sprite} object of Pokémon for setting the alpha value */ const speciesSprite = this.starterContainers[s].icon; @@ -3466,7 +3469,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { * If speciesStarterDexEntry?.caughtAttr is true, this species registered in stater. * we change to can AddParty value to true since the user has enough cost to choose this pokemon and this pokemon registered too. */ - const isValidForChallenge = new Utils.BooleanHolder(true); + const isValidForChallenge = new BooleanHolder(true); Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, this.allSpecies[s], isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(this.allSpecies[s], this.getCurrentDexProps(this.allSpecies[s].speciesId)), isPartyValid); const canBeChosen = remainValue >= speciesStarterValue && isValidForChallenge.value; @@ -3482,9 +3485,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler { */ if (canBeChosen || (isPokemonInParty && remainValue >= speciesStarterValue)) { speciesSprite.setAlpha(1); - if (speciesStarterDexEntry?.caughtAttr) { - this.canAddParty = true; - } } else { /** * If it can't be chosen, the user can't select. @@ -3494,7 +3494,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } } - this.value = newValue; return true; } @@ -3582,7 +3581,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { isPartyValid(): boolean { let canStart = false; for (let s = 0; s < this.starterSpecies.length; s++) { - const isValidForChallenge = new Utils.BooleanHolder(true); + const isValidForChallenge = new BooleanHolder(true); const species = this.starterSpecies[s]; Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, species, isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), false); canStart = canStart || isValidForChallenge.value; From 770f388c457a924b56df4572fb64949a647ac7ab Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 3 Nov 2024 19:23:51 -0800 Subject: [PATCH 19/98] [Refactor] Miscellaneous code cleanup (#4596) * Miscellaneous code cleanup: - Update `overrides.ts` imports - Add missing comment and `;` in `eslint.config.js` - `field/pokemon.ts`: Rename `generateVariant` to `generateShinyVariant` Mark some functions as `public`/etc Update various tsdocs/comments, add tsdocs to `isOfType` Turn an unreadable 450+ character line into a readable block of code - `utils.ts`: Remove unused function `randSeedEasedWeightedItem` Mark `IntegerHolder` and `FixedInt` as deprecated Update some tsdocs/comments * Convert to inline anonymous function * Fix indentation in `move-effect-phase.ts` Rename `getTarget` to `getFirstTarget` for clarity Convert namespace import to named imports Add `public`/etc to methods * Rename `verifyLang` to `hasAllLocalizedSprite` and update tsdoc * update locale submodule * Put missing `null` guards around some uses of `getUserPokemon()` * Fix locales * Fix Unburden test --------- Co-authored-by: Moka Co-authored-by: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> --- src/data/arena-tag.ts | 10 +- src/data/battler-tags.ts | 5 +- src/field/pokemon.ts | 197 ++++++++++++++++------------ src/loading-scene.ts | 2 +- src/overrides.ts | 14 +- src/phases/move-effect-phase.ts | 15 ++- src/test/abilities/unburden.test.ts | 2 +- src/utils.ts | 41 ++---- 8 files changed, 152 insertions(+), 134 deletions(-) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index fce46f0c98d..3dd526676a0 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -313,8 +313,8 @@ export class ConditionalProtectTag extends ArenaTag { * protection effect. * @param arena {@linkcode Arena} The arena containing the protection effect * @param moveId {@linkcode Moves} The move to check against this condition - * @returns `true` if the incoming move's priority is greater than 0. This includes - * moves with modified priorities from abilities (e.g. Prankster) + * @returns `true` if the incoming move's priority is greater than 0. + * This includes moves with modified priorities from abilities (e.g. Prankster) */ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { const move = allMoves[moveId]; @@ -322,9 +322,11 @@ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { const effectPhase = arena.scene.getCurrentPhase(); if (effectPhase instanceof MoveEffectPhase) { - const attacker = effectPhase.getUserPokemon()!; + const attacker = effectPhase.getUserPokemon(); applyMoveAttrs(IncrementMovePriorityAttr, attacker, null, move, priority); - applyAbAttrs(ChangeMovePriorityAbAttr, attacker, null, false, move, priority); + if (attacker) { + applyAbAttrs(ChangeMovePriorityAbAttr, attacker, null, false, move, priority); + } } return priority.value > 0; }; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 3051c2061a8..15bc745ec9d 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2496,7 +2496,10 @@ export class SubstituteTag extends BattlerTag { onHit(pokemon: Pokemon): void { const moveEffectPhase = pokemon.scene.getCurrentPhase(); if (moveEffectPhase instanceof MoveEffectPhase) { - const attacker = moveEffectPhase.getUserPokemon()!; + const attacker = moveEffectPhase.getUserPokemon(); + if (!attacker) { + return; + } const move = moveEffectPhase.move.getMove(); const firstHit = (attacker.turnData.hitCount === attacker.turnData.hitsLeft); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5506c35e18a..d0cb0aa1ff6 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -230,7 +230,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } if (this.variant === undefined) { - this.variant = this.shiny ? this.generateVariant() : 0; + this.variant = this.shiny ? this.generateShinyVariant() : 0; } this.customPokemonData = new CustomPokemonData(); @@ -1199,7 +1199,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @returns an array of {@linkcode Moves}, the length of which is determined * by how many learnable moves there are for the {@linkcode Pokemon}. */ - getLearnableLevelMoves(): Moves[] { + public getLearnableLevelMoves(): Moves[] { let levelMoves = this.getLevelMoves(1, true, false, true).map(lm => lm[1]); if (this.metBiome === -1 && !this.scene.gameMode.isFreshStartChallenge() && !this.scene.gameMode.isDaily) { levelMoves = this.getUnlockedEggMoves().concat(levelMoves); @@ -1213,13 +1213,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Gets the types of a pokemon - * @param includeTeraType boolean to include tera-formed type, default false - * @param forDefend boolean if the pokemon is defending from an attack - * @param ignoreOverride boolean if true, ignore ability changing effects + * @param includeTeraType - `true` to include tera-formed type; Default: `false` + * @param forDefend - `true` if the pokemon is defending from an attack; Default: `false` + * @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false` * @returns array of {@linkcode Type} */ - getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride?: boolean): Type[] { - const types : Type[] = []; + public getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride: boolean = false): Type[] { + const types: Type[] = []; if (includeTeraType) { const teraType = this.getTeraType(); @@ -1284,14 +1284,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - // this.scene potentially can be undefined for a fainted pokemon in doubles - // use optional chaining to avoid runtime errors - - if (!types.length) { // become UNKNOWN if no types are present + // become UNKNOWN if no types are present + if (!types.length) { types.push(Type.UNKNOWN); } - if (types.length > 1 && types.includes(Type.UNKNOWN)) { // remove UNKNOWN if other types are present + // remove UNKNOWN if other types are present + if (types.length > 1 && types.includes(Type.UNKNOWN)) { const index = types.indexOf(Type.UNKNOWN); if (index !== -1) { types.splice(index, 1); @@ -1311,19 +1310,27 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return types; } - isOfType(type: Type, includeTeraType: boolean = true, forDefend: boolean = false, ignoreOverride?: boolean): boolean { - return !!this.getTypes(includeTeraType, forDefend, ignoreOverride).some(t => t === type); + /** + * Checks if the pokemon's typing includes the specified type + * @param type - {@linkcode Type} to check + * @param includeTeraType - `true` to include tera-formed type; Default: `true` + * @param forDefend - `true` if the pokemon is defending from an attack; Default: `false` + * @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false` + * @returns `true` if the Pokemon's type matches + */ + public isOfType(type: Type, includeTeraType: boolean = true, forDefend: boolean = false, ignoreOverride: boolean = false): boolean { + return this.getTypes(includeTeraType, forDefend, ignoreOverride).some((t) => t === type); } /** * Gets the non-passive ability of the pokemon. This accounts for fusions and ability changing effects. - * This should rarely be called, most of the time {@link hasAbility} or {@link hasAbilityWithAttr} are better used as + * This should rarely be called, most of the time {@linkcode hasAbility} or {@linkcode hasAbilityWithAttr} are better used as * those check both the passive and non-passive abilities and account for ability suppression. - * @see {@link hasAbility} {@link hasAbilityWithAttr} Intended ways to check abilities in most cases - * @param {boolean} ignoreOverride If true, ignore ability changing effects - * @returns {Ability} The non-passive ability of the pokemon + * @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases + * @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false` + * @returns The non-passive {@linkcode Ability} of the pokemon */ - getAbility(ignoreOverride?: boolean): Ability { + public getAbility(ignoreOverride: boolean = false): Ability { if (!ignoreOverride && this.summonData?.ability) { return allAbilities[this.summonData.ability]; } @@ -1352,12 +1359,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Gets the passive ability of the pokemon. This should rarely be called, most of the time - * {@link hasAbility} or {@link hasAbilityWithAttr} are better used as those check both the passive and + * {@linkcode hasAbility} or {@linkcode hasAbilityWithAttr} are better used as those check both the passive and * non-passive abilities and account for ability suppression. - * @see {@link hasAbility} {@link hasAbilityWithAttr} Intended ways to check abilities in most cases - * @returns {Ability} The passive ability of the pokemon + * @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases + * @returns The passive {@linkcode Ability} of the pokemon */ - getPassiveAbility(): Ability { + public getPassiveAbility(): Ability { if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) { return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE]; } @@ -1379,12 +1386,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Gets a list of all instances of a given ability attribute among abilities this pokemon has. * Accounts for all the various effects which can affect whether an ability will be present or * in effect, and both passive and non-passive. - * @param attrType {@linkcode AbAttr} The ability attribute to check for. - * @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active - * @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects - * @returns A list of all the ability attributes on this ability. + * @param attrType - {@linkcode AbAttr} The ability attribute to check for. + * @param canApply - If `false`, it doesn't check whether the ability is currently active; Default `true` + * @param ignoreOverride - If `true`, it ignores ability changing effects; Default `false` + * @returns An array of all the ability attributes on this ability. */ - getAbilityAttrs(attrType: { new(...args: any[]): T }, canApply: boolean = true, ignoreOverride?: boolean): T[] { + public getAbilityAttrs(attrType: { new(...args: any[]): T }, canApply: boolean = true, ignoreOverride: boolean = false): T[] { const abilityAttrs: T[] = []; if (!canApply || this.canApplyAbility()) { @@ -1403,12 +1410,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * - bought with starter candy * - set by override * - is a boss pokemon - * @returns whether or not a pokemon should have a passive + * @returns `true` if the Pokemon has a passive */ - hasPassive(): boolean { + public hasPassive(): boolean { // returns override if valid for current case - if ((Overrides.PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && this.isPlayer()) || - (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && !this.isPlayer())) { + if ((Overrides.PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && this.isPlayer()) + || (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && !this.isPlayer())) { return true; } @@ -1427,12 +1434,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Checks whether an ability of a pokemon can be currently applied. This should rarely be - * directly called, as {@link hasAbility} and {@link hasAbilityWithAttr} already call this. - * @see {@link hasAbility} {@link hasAbilityWithAttr} Intended ways to check abilities in most cases - * @param {boolean} passive If true, check if passive can be applied instead of non-passive - * @returns {Ability} The passive ability of the pokemon + * directly called, as {@linkcode hasAbility} and {@linkcode hasAbilityWithAttr} already call this. + * @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases + * @param passive If true, check if passive can be applied instead of non-passive + * @returns `true` if the ability can be applied */ - canApplyAbility(passive: boolean = false): boolean { + public canApplyAbility(passive: boolean = false): boolean { if (passive && !this.hasPassive()) { return false; } @@ -1461,7 +1468,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } } - return (!!this.hp || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this)); + return (this.hp > 0 || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this)); } /** @@ -1473,7 +1480,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} ignoreOverride If true, it ignores ability changing effects * @returns {boolean} Whether the ability is present and active */ - hasAbility(ability: Abilities, canApply: boolean = true, ignoreOverride?: boolean): boolean { + public hasAbility(ability: Abilities, canApply: boolean = true, ignoreOverride?: boolean): boolean { if (this.getAbility(ignoreOverride).id === ability && (!canApply || this.canApplyAbility())) { return true; } @@ -1493,7 +1500,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} ignoreOverride If true, it ignores ability changing effects * @returns {boolean} Whether an ability with that attribute is present and active */ - hasAbilityWithAttr(attrType: Constructor, canApply: boolean = true, ignoreOverride?: boolean): boolean { + public hasAbilityWithAttr(attrType: Constructor, canApply: boolean = true, ignoreOverride?: boolean): boolean { if ((!canApply || this.canApplyAbility()) && this.getAbility(ignoreOverride).hasAttr(attrType)) { return true; } @@ -1508,7 +1515,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * and then multiplicative modifiers happening after (Heavy Metal and Light Metal) * @returns the kg of the Pokemon (minimum of 0.1) */ - getWeight(): number { + public getWeight(): number { const autotomizedTag = this.getTag(AutotomizedTag); let weightRemoved = 0; if (!Utils.isNullOrUndefined(autotomizedTag)) { @@ -1523,10 +1530,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets the tera-formed type of the pokemon, or UNKNOWN if not present - * @returns the {@linkcode Type} + * @returns The tera-formed type of the pokemon, or {@linkcode Type.UNKNOWN} if not present */ - getTeraType(): Type { + public getTeraType(): Type { // this.scene can be undefined for a fainted mon in doubles if (this.scene !== undefined) { const teraModifier = this.scene.findModifier(m => m instanceof TerastallizeModifier @@ -1540,23 +1546,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return Type.UNKNOWN; } - isTerastallized(): boolean { + public isTerastallized(): boolean { return this.getTeraType() !== Type.UNKNOWN; } - isGrounded(): boolean { + public isGrounded(): boolean { return !!this.getTag(GroundedTag) || (!this.isOfType(Type.FLYING, true, true) && !this.hasAbility(Abilities.LEVITATE) && !this.getTag(BattlerTagType.FLOATING) && !this.getTag(SemiInvulnerableTag)); } /** * Determines whether this Pokemon is prevented from running or switching due * to effects from moves and/or abilities. - * @param trappedAbMessages `string[]` If defined, ability trigger messages + * @param trappedAbMessages - If defined, ability trigger messages * (e.g. from Shadow Tag) are forwarded through this array. - * @param simulated `boolean` if `true`, applies abilities via simulated calls. - * @returns + * @param simulated - If `true`, applies abilities via simulated calls. + * @returns `true` if the pokemon is trapped */ - isTrapped(trappedAbMessages: string[] = [], simulated: boolean = true): boolean { + public isTrapped(trappedAbMessages: string[] = [], simulated: boolean = true): boolean { if (this.isOfType(Type.GHOST)) { return false; } @@ -1564,7 +1570,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const trappedByAbility = new Utils.BooleanHolder(false); const opposingField = this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField(); - opposingField.forEach(opponent => + opposingField.forEach((opponent) => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, opponent, trappedByAbility, this, trappedAbMessages, simulated) ); @@ -1575,11 +1581,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Calculates the type of a move when used by this Pokemon after * type-changing move and ability attributes have applied. - * @param move {@linkcode Move} The move being used. - * @param simulated If `true`, prevents showing abilities applied in this calculation. - * @returns the {@linkcode Type} of the move after attributes are applied + * @param move - {@linkcode Move} The move being used. + * @param simulated - If `true`, prevents showing abilities applied in this calculation. + * @returns The {@linkcode Type} of the move after attributes are applied */ - getMoveType(move: Move, simulated: boolean = true): Type { + public getMoveType(move: Move, simulated: boolean = true): Type { const moveTypeHolder = new Utils.NumberHolder(move.type); applyMoveAttrs(VariableMoveTypeAttr, this, null, move, moveTypeHolder); @@ -1944,13 +1950,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Function that tries to set a Pokemon shiny based on seed. * For manual use only, usually to roll a Pokemon's shiny chance a second time. * - * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / 65536 - * @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) + * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536` + * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} - * @returns true if the Pokemon has been set as a shiny, false otherwise + * @returns `true` if the Pokemon has been set as a shiny, `false` otherwise */ - trySetShinySeed(thresholdOverride?: integer, applyModifiersToOverride?: boolean): boolean { - const shinyThreshold = new Utils.IntegerHolder(BASE_SHINY_CHANCE); + public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { + const shinyThreshold = new Utils.NumberHolder(BASE_SHINY_CHANCE); if (thresholdOverride === undefined || applyModifiersToOverride) { if (thresholdOverride !== undefined && applyModifiersToOverride) { shinyThreshold.value = thresholdOverride; @@ -1975,13 +1981,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Generates a variant - * Has a 10% of returning 2 (epic variant) - * And a 30% of returning 1 (rare variant) - * Returns 0 (basic shiny) if there is no variant or 60% of the time otherwise - * @returns the shiny variant + * Generates a shiny variant + * @returns `0-2`, with the following probabilities: + * - Has a 10% chance of returning `2` (epic variant) + * - Has a 30% chance of returning `1` (rare variant) + * - Has a 60% chance of returning `0` (basic shiny) */ - generateVariant(): Variant { + protected generateShinyVariant(): Variant { const formIndex: number = this.formIndex; let variantDataIndex: string | number = this.species.speciesId; if (this.species.forms.length > 0) { @@ -2007,8 +2013,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - generateFusionSpecies(forStarter?: boolean): void { - const hiddenAbilityChance = new Utils.IntegerHolder(BASE_HIDDEN_ABILITY_CHANCE); + public generateFusionSpecies(forStarter?: boolean): void { + const hiddenAbilityChance = new Utils.NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); } @@ -2057,7 +2063,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.generateName(); } - clearFusionSpecies(): void { + public clearFusionSpecies(): void { this.fusionSpecies = null; this.fusionFormIndex = 0; this.fusionAbilityIndex = 0; @@ -2071,12 +2077,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.calculateStats(); } - generateAndPopulateMoveset(): void { + /** Generates a semi-random moveset for a Pokemon */ + public generateAndPopulateMoveset(): void { this.moveset = []; let movePool: [Moves, number][] = []; const allLevelMoves = this.getLevelMoves(1, true, true); if (!allLevelMoves) { - console.log(this.species.speciesId, "ERROR"); + console.warn("Error encountered trying to generate moveset for:", this.species.name); return; } @@ -2086,15 +2093,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { break; } let weight = levelMove[0]; - if (weight === 0) { // Evo Moves + // Evolution Moves + if (weight === 0) { weight = 50; } - if (weight === 1 && allMoves[levelMove[1]].power >= 80) { // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight + // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight + if (weight === 1 && allMoves[levelMove[1]].power >= 80) { weight = 40; } - if (allMoves[levelMove[1]].name.endsWith(" (N)")) { - weight /= 100; - } // Unimplemented level up moves are possible to generate, but 1% of their normal chance. if (!movePool.some(m => m[0] === levelMove[1])) { movePool.push([ levelMove[1], weight ]); } @@ -2127,7 +2133,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - if (this.level >= 60) { // No egg moves below level 60 + // No egg moves below level 60 + if (this.level >= 60) { for (let i = 0; i < 3; i++) { const moveId = speciesEggMoves[this.species.getRootSpeciesId()][i]; if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { @@ -2135,7 +2142,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } const moveId = speciesEggMoves[this.species.getRootSpeciesId()][3]; - if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) { // No rare egg moves before e4 + // No rare egg moves before e4 + if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) { movePool.push([ moveId, 30 ]); } if (this.fusionSpecies) { @@ -2146,14 +2154,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][3]; - if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) {// No rare egg moves before e4 + // No rare egg moves before e4 + if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) { movePool.push([ moveId, 30 ]); } } } } - if (this.isBoss()) { // Bosses never get self ko moves + // Bosses never get self ko moves + if (this.isBoss()) { movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(SacrificialAttr)); } movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(SacrificialAttrOnHit)); @@ -2182,7 +2192,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk; movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].category === worseCategory ? statRatio : 1) ]); - let weightMultiplier = 0.9; // The higher this is the more the game weights towards higher level moves. At 0 all moves are equal weight. + /** The higher this is the more the game weights towards higher level moves. At `0` all moves are equal weight. */ + let weightMultiplier = 0.9; if (this.hasTrainer()) { weightMultiplier += 0.7; } @@ -2191,7 +2202,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } const baseWeights: [Moves, number][] = movePool.map(m => [ m[0], Math.ceil(Math.pow(m[1], weightMultiplier) * 100) ]); - if (this.hasTrainer() || this.isBoss()) { // Trainers and bosses always force a stab move + // Trainers and bosses always force a stab move + if (this.hasTrainer() || this.isBoss()) { const stabMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS && this.isOfType(allMoves[m[0]].type)); if (stabMovePool.length) { @@ -2221,8 +2233,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // Sqrt the weight of any damaging moves with overlapping types. This is about a 0.05 - 0.1 multiplier. // Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights double if STAB. // Status moves remain unchanged on weight, this encourages 1-2 - movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo?.moveId)).map(m => [ m[0], this.moveset.some(mo => mo?.getMove().category !== MoveCategory.STATUS && mo?.getMove().type === allMoves[m[0]].type) ? Math.ceil(Math.sqrt(m[1])) : allMoves[m[0]].category !== MoveCategory.STATUS ? Math.ceil(m[1] / Math.max(Math.pow(4, this.moveset.filter(mo => (mo?.getMove().power!) > 1).length) / 8, 0.5) * (this.isOfType(allMoves[m[0]].type) ? 2 : 1)) : m[1] ]); // TODO: is this bang correct? - } else { // Non-trainer pokemon just use normal weights + movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo?.moveId)).map((m) => { + let ret: number; + if (this.moveset.some(mo => mo?.getMove().category !== MoveCategory.STATUS && mo?.getMove().type === allMoves[m[0]].type)) { + ret = Math.ceil(Math.sqrt(m[1])); + } else if (allMoves[m[0]].category !== MoveCategory.STATUS) { + ret = Math.ceil(m[1] / Math.max(Math.pow(4, this.moveset.filter(mo => (mo?.getMove().power ?? 0) > 1).length) / 8, 0.5) * (this.isOfType(allMoves[m[0]].type) ? 2 : 1)); + } else { + ret = m[1]; + } + return [ m[0], ret ]; + }); + } else { + // Non-trainer pokemon just use normal weights movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo?.moveId)); } const totalWeight = movePool.reduce((v, m) => v + m[1], 0); @@ -2240,11 +2263,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean { + public trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean { const move = this.getMoveset().length > moveIndex ? this.getMoveset()[moveIndex] : null; - return move?.isUsable(this, ignorePp)!; // TODO: is this bang correct? + return move?.isUsable(this, ignorePp) ?? false; } showInfo(): void { @@ -4576,7 +4599,7 @@ export class EnemyPokemon extends Pokemon { } if (this.shiny) { - this.variant = this.generateVariant(); + this.variant = this.generateShinyVariant(); if (Overrides.OPP_VARIANT_OVERRIDE !== null) { this.variant = Overrides.OPP_VARIANT_OVERRIDE; } diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 1aed61b34ba..c49b8d5aaa9 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -232,7 +232,7 @@ export class LoadingScene extends SceneBase { // Get current lang and load the types atlas for it. English will only load types while all other languages will load types and types_ const lang = i18next.resolvedLanguage; if (lang !== "en") { - if (Utils.verifyLang(lang)) { + if (Utils.hasAllLocalizedSprites(lang)) { this.loadAtlas(`statuses_${lang}`, ""); this.loadAtlas(`types_${lang}`, ""); } else { diff --git a/src/overrides.ts b/src/overrides.ts index 6760db79205..d7a8ee18f15 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -1,20 +1,20 @@ +import { type PokeballCounts } from "#app/battle-scene"; +import { Gender } from "#app/data/gender"; +import { Variant } from "#app/data/variant"; +import { type ModifierOverride } from "#app/modifier/modifier-type"; +import { Unlockables } from "#app/system/unlockables"; import { Abilities } from "#enums/abilities"; import { Biome } from "#enums/biome"; import { EggTier } from "#enums/egg-type"; import { Moves } from "#enums/moves"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; import { StatusEffect } from "#enums/status-effect"; import { TimeOfDay } from "#enums/time-of-day"; import { VariantTier } from "#enums/variant-tier"; import { WeatherType } from "#enums/weather-type"; -import { type PokeballCounts } from "./battle-scene"; -import { Gender } from "./data/gender"; -import { Variant } from "./data/variant"; -import { type ModifierOverride } from "./modifier/modifier-type"; -import { Unlockables } from "./system/unlockables"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; /** * Overrides that are using when testing different in game situations diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 8b4b462380c..4c813a881d3 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -52,11 +52,11 @@ import { HitHealModifier, PokemonMultiHitModifier, } from "#app/modifier/modifier"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; import { BooleanHolder, executeIf, NumberHolder } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import i18next from "i18next"; -import { PokemonPhase } from "./pokemon-phase"; export class MoveEffectPhase extends PokemonPhase { public move: PokemonMove; @@ -517,7 +517,11 @@ export class MoveEffectPhase extends PokemonPhase { return true; } - const user = this.getUserPokemon()!; // TODO: is this bang correct? + const user = this.getUserPokemon(); + + if (!user) { + return false; + } // Hit check only calculated on first hit for multi-hit moves unless flag is set to check all hits. // However, if an ability with the MaxMultiHitAbAttr, namely Skill Link, is present, act as a normal @@ -566,9 +570,9 @@ export class MoveEffectPhase extends PokemonPhase { } /** @returns The {@linkcode Pokemon} using this phase's invoked move */ - public getUserPokemon(): Pokemon | undefined { + public getUserPokemon(): Pokemon | null { if (this.battlerIndex > BattlerIndex.ENEMY_2) { - return this.scene.getPokemonById(this.battlerIndex) ?? undefined; + return this.scene.getPokemonById(this.battlerIndex); } return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; } @@ -596,8 +600,7 @@ export class MoveEffectPhase extends PokemonPhase { /** * Prevents subsequent strikes of this phase's invoked move from occurring - * @param target {@linkcode Pokemon} if defined, only stop subsequent - * strikes against this Pokemon + * @param target - If defined, only stop subsequent strikes against this {@linkcode Pokemon} */ public stopMultiHit(target?: Pokemon): void { // If given a specific target, remove the target from subsequent strikes diff --git a/src/test/abilities/unburden.test.ts b/src/test/abilities/unburden.test.ts index 33604efc534..7cd69f4a075 100644 --- a/src/test/abilities/unburden.test.ts +++ b/src/test/abilities/unburden.test.ts @@ -216,7 +216,7 @@ describe("Abilities - Unburden", () => { .moveset([ Moves.SPLASH ]); await game.classicMode.startBattle([ Species.TREECKO, Species.MEOWTH, Species.WEEZING ]); - const playerPokemon = game.scene.getParty(); + const playerPokemon = game.scene.getPlayerParty(); const treecko = playerPokemon[0]; const weezing = playerPokemon[2]; treecko.abilityIndex = 2; diff --git a/src/utils.ts b/src/utils.ts index b615dcf122b..8b779f32730 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -121,18 +121,6 @@ export function randSeedWeightedItem(items: T[]): T { : Phaser.Math.RND.weightedPick(items); } -export function randSeedEasedWeightedItem(items: T[], easingFunction: string = "Sine.easeIn"): T | null { - if (!items.length) { - return null; - } - if (items.length === 1) { - return items[0]; - } - const value = Phaser.Math.RND.realInRange(0, 1); - const easedValue = Phaser.Tweens.Builders.GetEaseFunction(easingFunction)(value); - return items[Math.floor(easedValue * items.length)]; -} - /** * Shuffle a list using the seeded rng. Utilises the Fisher-Yates algorithm. * @param {Array} items An array of items. @@ -380,18 +368,21 @@ export class NumberHolder { } } +/** @deprecated Use {@linkcode NumberHolder} */ export class IntegerHolder extends NumberHolder { constructor(value: integer) { super(value); } } +/** @deprecated Use {@linkcode NumberHolder}*/ export class FixedInt extends IntegerHolder { constructor(value: integer) { super(value); } } +/** @deprecated */ export function fixedInt(value: integer): integer { return new FixedInt(value) as unknown as integer; } @@ -474,14 +465,15 @@ export function hslToHex(h: number, s: number, l: number): string { return `#${f(0)}${f(8)}${f(4)}`; } -/*This function returns true if the current lang is available for some functions -If the lang is not in the function, it usually means that lang is going to use the default english version -This function is used in: -- summary-ui-handler.ts: If the lang is not available, it'll use types.json (english) -English itself counts as not available +/** + * This function returns `true` if all localized images used by the game have been added for the given language. + * + * If the lang is not in the function, it usually means that lang is going to use the default english version + * + * English itself counts as not available */ -export function verifyLang(lang?: string): boolean { - //IMPORTANT - ONLY ADD YOUR LANG HERE IF YOU'VE ALREADY ADDED ALL THE NECESSARY IMAGES +export function hasAllLocalizedSprites(lang?: string): boolean { + // IMPORTANT - ONLY ADD YOUR LANG HERE IF YOU'VE ALREADY ADDED ALL THE NECESSARY IMAGES if (!lang) { lang = i18next.resolvedLanguage; } @@ -503,7 +495,7 @@ export function verifyLang(lang?: string): boolean { } /** - * Prints the type and name of all game objects in a container for debuggin purposes + * Prints the type and name of all game objects in a container for debugging purposes * @param container container with game objects inside it */ export function printContainerList(container: Phaser.GameObjects.Container): void { @@ -578,17 +570,12 @@ export function capitalizeString(str: string, sep: string, lowerFirstChar: boole return null; } -/** - * Returns if an object is null or undefined - * @param object - */ export function isNullOrUndefined(object: any): object is undefined | null { return null === object || undefined === object; } /** * Capitalizes the first letter of a string - * @param str */ export function capitalizeFirstLetter(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); @@ -614,7 +601,7 @@ export function toDmgValue(value: number, minValue: number = 1) { * @returns the localized sprite key */ export function getLocalizedSpriteKey(baseKey: string) { - return `${baseKey}${verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`; + return `${baseKey}${hasAllLocalizedSprites(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`; } /** @@ -622,7 +609,7 @@ export function getLocalizedSpriteKey(baseKey: string) { * @param num the number to check * @param min the minimum value (included) * @param max the maximum value (included) - * @returns true if number is **inclusive** between min and max + * @returns `true` if number is **inclusive** between min and max */ export function isBetween(num: number, min: number, max: number): boolean { return num >= min && num <= max; From 3a767ed38a4e57e2c070413c635dce982e4502a6 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sun, 3 Nov 2024 19:31:07 -0800 Subject: [PATCH 20/98] [Refactor] Another Audio clean up (#4077) * Cleaned up how cry keys were retrieved * Legendary Pokemon Changes + Cry Cleanup * Spelling mistake * Fixed settings. * correcting typedocs * eslint * eslint cleanup * music pref * gdfafa * Music * Apply suggestions from code review Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> * Update src/battle.ts Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> * Update src/battle.ts Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> * fixed default * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Added changes * Update src/battle-scene.ts Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> * Fix variable usage * Fix `getFormSpriteKey()` and `.concat()` usage Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> * asdsafafaf * i need to do the dishes ughhh --------- Co-authored-by: frutescens Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> Co-authored-by: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> --- src/battle-scene.ts | 23 +- src/battle.ts | 286 ++++++++++-------- .../global-trade-system-encounter.ts | 3 +- src/data/pokemon-species.ts | 6 +- src/field/pokemon.ts | 22 +- src/system/settings/settings.ts | 7 +- src/test/utils/gameManager.ts | 1 + 7 files changed, 187 insertions(+), 161 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 415a8fe9aaf..d82acec1c20 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -13,6 +13,7 @@ import { Arena, ArenaBase } from "#app/field/arena"; import { GameData } from "#app/system/game-data"; import { addTextObject, getTextColor, TextStyle } from "#app/ui/text"; import { allMoves } from "#app/data/move"; +import { MusicPreference } from "#app/system/settings/settings"; import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import AbilityBar from "#app/ui/ability-bar"; import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, applyPostItemLostAbAttrs, BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr, PostItemLostAbAttr } from "#app/data/ability"; @@ -169,7 +170,7 @@ export default class BattleScene extends SceneBase { public uiTheme: UiTheme = UiTheme.DEFAULT; public windowType: integer = 0; public experimentalSprites: boolean = false; - public musicPreference: integer = 0; + public musicPreference: number = MusicPreference.MIXED; public moveAnimations: boolean = true; public expGainsSpeed: ExpGainsSpeed = ExpGainsSpeed.DEFAULT; public skipSeenDialogues: boolean = false; @@ -2998,22 +2999,16 @@ export default class BattleScene extends SceneBase { */ getActiveKeys(): string[] { const keys: string[] = []; - const playerParty = this.getPlayerParty(); - playerParty.forEach(p => { + let activePokemon: (PlayerPokemon | EnemyPokemon)[] = this.getPlayerParty(); + activePokemon = activePokemon.concat(this.getEnemyParty()); + activePokemon.forEach((p) => { keys.push(p.getSpriteKey(true)); - keys.push(p.getBattleSpriteKey(true, true)); - keys.push("cry/" + p.species.getCryKey(p.formIndex)); - if (p.fusionSpecies) { - keys.push("cry/" + p.fusionSpecies.getCryKey(p.fusionFormIndex)); + if (p instanceof PlayerPokemon) { + keys.push(p.getBattleSpriteKey(true, true)); } - }); - // enemyParty has to be operated on separately from playerParty because playerPokemon =/= enemyPokemon - const enemyParty = this.getEnemyParty(); - enemyParty.forEach(p => { - keys.push(p.getSpriteKey(true)); - keys.push("cry/" + p.species.getCryKey(p.formIndex)); + keys.push(p.species.getCryKey(p.formIndex)); if (p.fusionSpecies) { - keys.push("cry/" + p.fusionSpecies.getCryKey(p.fusionFormIndex)); + keys.push(p.fusionSpecies.getCryKey(p.fusionFormIndex)); } }); return keys; diff --git a/src/battle.ts b/src/battle.ts index 6086c2ceb4e..0356772bb07 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -6,11 +6,13 @@ import { GameMode } from "./game-mode"; import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier"; import { PokeballType } from "./data/pokeball"; import { trainerConfigs } from "#app/data/trainer-config"; +import { SpeciesFormKey } from "#enums/species-form-key"; import Pokemon, { EnemyPokemon, PlayerPokemon, QueuedMove } from "#app/field/pokemon"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleSpec } from "#enums/battle-spec"; import { Moves } from "#enums/moves"; import { PlayerGender } from "#enums/player-gender"; +import { MusicPreference } from "#app/system/settings/settings"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import i18next from "#app/plugins/i18n"; @@ -212,7 +214,6 @@ export default class Battle { } getBgmOverride(scene: BattleScene): string | null { - const battlers = this.enemyParty.slice(0, this.getBattlerCount()); if (this.isBattleMysteryEncounter() && this.mysteryEncounter?.encounterMode === MysteryEncounterMode.DEFAULT) { // Music is overridden for MEs during ME onInit() // Should not use any BGM overrides before swapping from DEFAULT mode @@ -221,7 +222,7 @@ export default class Battle { if (!this.started && this.trainer?.config.encounterBgm && this.trainer?.getEncounterMessages()?.length) { return `encounter_${this.trainer?.getEncounterBgm()}`; } - if (scene.musicPreference === 0) { + if (scene.musicPreference === MusicPreference.CONSISTENT) { return this.trainer?.getBattleBgm() ?? null; } else { return this.trainer?.getMixedBattleBgm() ?? null; @@ -229,147 +230,168 @@ export default class Battle { } else if (this.gameMode.isClassic && this.waveIndex > 195 && this.battleSpec !== BattleSpec.FINAL_BOSS) { return "end_summit"; } - for (const pokemon of battlers) { + const wildOpponents = scene.getEnemyParty(); + for (const pokemon of wildOpponents) { if (this.battleSpec === BattleSpec.FINAL_BOSS) { - if (pokemon.formIndex) { + if (pokemon.species.getFormSpriteKey(pokemon.formIndex) === SpeciesFormKey.ETERNAMAX) { return "battle_final"; } return "battle_final_encounter"; } if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) { - if (scene.musicPreference === 0) { - if (pokemon.species.speciesId === Species.REGIROCK || pokemon.species.speciesId === Species.REGICE || pokemon.species.speciesId === Species.REGISTEEL || pokemon.species.speciesId === Species.REGIGIGAS || pokemon.species.speciesId === Species.REGIELEKI || pokemon.species.speciesId === Species.REGIDRAGO) { - return "battle_legendary_regis_g5"; + if (scene.musicPreference === MusicPreference.CONSISTENT) { + switch (pokemon.species.speciesId) { + case Species.REGIROCK: + case Species.REGICE: + case Species.REGISTEEL: + case Species.REGIGIGAS: + case Species.REGIDRAGO: + case Species.REGIELEKI: + return "battle_legendary_regis_g5"; + case Species.KYUREM: + return "battle_legendary_kyurem"; + default: + if (pokemon.species.legendary) { + return "battle_legendary_res_zek"; + } + return "battle_legendary_unova"; } - if (pokemon.species.speciesId === Species.COBALION || pokemon.species.speciesId === Species.TERRAKION || pokemon.species.speciesId === Species.VIRIZION || pokemon.species.speciesId === Species.TORNADUS || pokemon.species.speciesId === Species.THUNDURUS || pokemon.species.speciesId === Species.LANDORUS || pokemon.species.speciesId === Species.KELDEO || pokemon.species.speciesId === Species.MELOETTA || pokemon.species.speciesId === Species.GENESECT) { - return "battle_legendary_unova"; - } - if (pokemon.species.speciesId === Species.KYUREM) { - return "battle_legendary_kyurem"; - } - if (pokemon.species.legendary) { - return "battle_legendary_res_zek"; - } - return "battle_legendary_unova"; - } else { - if (pokemon.species.speciesId === Species.ARTICUNO || pokemon.species.speciesId === Species.ZAPDOS || pokemon.species.speciesId === Species.MOLTRES || pokemon.species.speciesId === Species.MEWTWO || pokemon.species.speciesId === Species.MEW) { - return "battle_legendary_kanto"; - } - if (pokemon.species.speciesId === Species.RAIKOU) { - return "battle_legendary_raikou"; - } - if (pokemon.species.speciesId === Species.ENTEI) { - return "battle_legendary_entei"; - } - if (pokemon.species.speciesId === Species.SUICUNE) { - return "battle_legendary_suicune"; - } - if (pokemon.species.speciesId === Species.LUGIA) { - return "battle_legendary_lugia"; - } - if (pokemon.species.speciesId === Species.HO_OH) { - return "battle_legendary_ho_oh"; - } - if (pokemon.species.speciesId === Species.REGIROCK || pokemon.species.speciesId === Species.REGICE || pokemon.species.speciesId === Species.REGISTEEL || pokemon.species.speciesId === Species.REGIGIGAS || pokemon.species.speciesId === Species.REGIELEKI || pokemon.species.speciesId === Species.REGIDRAGO) { - return "battle_legendary_regis_g6"; - } - if (pokemon.species.speciesId === Species.GROUDON || pokemon.species.speciesId === Species.KYOGRE) { - return "battle_legendary_gro_kyo"; - } - if (pokemon.species.speciesId === Species.RAYQUAZA) { - return "battle_legendary_rayquaza"; - } - if (pokemon.species.speciesId === Species.DEOXYS) { - return "battle_legendary_deoxys"; - } - if (pokemon.species.speciesId === Species.UXIE || pokemon.species.speciesId === Species.MESPRIT || pokemon.species.speciesId === Species.AZELF) { - return "battle_legendary_lake_trio"; - } - if (pokemon.species.speciesId === Species.HEATRAN || pokemon.species.speciesId === Species.CRESSELIA || pokemon.species.speciesId === Species.DARKRAI || pokemon.species.speciesId === Species.SHAYMIN) { - return "battle_legendary_sinnoh"; - } - if (pokemon.species.speciesId === Species.DIALGA || pokemon.species.speciesId === Species.PALKIA) { - if (pokemon.getFormKey() === "") { + } else if (scene.musicPreference === MusicPreference.MIXED) { + switch (pokemon.species.speciesId) { + case Species.ARTICUNO: + case Species.ZAPDOS: + case Species.MOLTRES: + case Species.MEWTWO: + case Species.MEW: + return "battle_legendary_kanto"; + case Species.RAIKOU: + return "battle_legendary_raikou"; + case Species.ENTEI: + return "battle_legendary_entei"; + case Species.SUICUNE: + return "battle_legendary_suicune"; + case Species.LUGIA: + return "battle_legendary_lugia"; + case Species.HO_OH: + return "battle_legendary_ho_oh"; + case Species.REGIROCK: + case Species.REGICE: + case Species.REGISTEEL: + case Species.REGIGIGAS: + case Species.REGIDRAGO: + case Species.REGIELEKI: + return "battle_legendary_regis_g6"; + case Species.GROUDON: + case Species.KYOGRE: + return "battle_legendary_gro_kyo"; + case Species.RAYQUAZA: + return "battle_legendary_rayquaza"; + case Species.DEOXYS: + return "battle_legendary_deoxys"; + case Species.UXIE: + case Species.MESPRIT: + case Species.AZELF: + return "battle_legendary_lake_trio"; + case Species.HEATRAN: + case Species.CRESSELIA: + case Species.DARKRAI: + case Species.SHAYMIN: + return "battle_legendary_sinnoh"; + case Species.DIALGA: + case Species.PALKIA: + if (pokemon.species.getFormSpriteKey(pokemon.formIndex) === SpeciesFormKey.ORIGIN) { + return "battle_legendary_origin_forme"; + } return "battle_legendary_dia_pal"; - } - if (pokemon.getFormKey() === "origin") { - return "battle_legendary_origin_forme"; - } - } - if (pokemon.species.speciesId === Species.GIRATINA) { - return "battle_legendary_giratina"; - } - if (pokemon.species.speciesId === Species.ARCEUS) { - return "battle_legendary_arceus"; - } - if (pokemon.species.speciesId === Species.COBALION || pokemon.species.speciesId === Species.TERRAKION || pokemon.species.speciesId === Species.VIRIZION || pokemon.species.speciesId === Species.TORNADUS || pokemon.species.speciesId === Species.THUNDURUS || pokemon.species.speciesId === Species.LANDORUS || pokemon.species.speciesId === Species.KELDEO || pokemon.species.speciesId === Species.MELOETTA || pokemon.species.speciesId === Species.GENESECT) { - return "battle_legendary_unova"; - } - if (pokemon.species.speciesId === Species.KYUREM) { - return "battle_legendary_kyurem"; - } - if (pokemon.species.speciesId === Species.XERNEAS || pokemon.species.speciesId === Species.YVELTAL || pokemon.species.speciesId === Species.ZYGARDE) { - return "battle_legendary_xern_yvel"; - } - if (pokemon.species.speciesId === Species.TAPU_KOKO || pokemon.species.speciesId === Species.TAPU_LELE || pokemon.species.speciesId === Species.TAPU_BULU || pokemon.species.speciesId === Species.TAPU_FINI) { - return "battle_legendary_tapu"; - } - if ([ Species.COSMOG, Species.COSMOEM, Species.SOLGALEO, Species.LUNALA ].includes(pokemon.species.speciesId)) { - return "battle_legendary_sol_lun"; - } - if (pokemon.species.speciesId === Species.NECROZMA) { - if (pokemon.getFormKey() === "") { + case Species.GIRATINA: + return "battle_legendary_giratina"; + case Species.ARCEUS: + return "battle_legendary_arceus"; + case Species.COBALION: + case Species.TERRAKION: + case Species.VIRIZION: + case Species.KELDEO: + case Species.TORNADUS: + case Species.LANDORUS: + case Species.THUNDURUS: + case Species.MELOETTA: + case Species.GENESECT: + return "battle_legendary_unova"; + case Species.KYUREM: + return "battle_legendary_kyurem"; + case Species.XERNEAS: + case Species.YVELTAL: + case Species.ZYGARDE: + return "battle_legendary_xern_yvel"; + case Species.TAPU_KOKO: + case Species.TAPU_LELE: + case Species.TAPU_BULU: + case Species.TAPU_FINI: + return "battle_legendary_tapu"; + case Species.SOLGALEO: + case Species.LUNALA: return "battle_legendary_sol_lun"; - } - if (pokemon.getFormKey() === "dusk-mane" || pokemon.getFormKey() === "dawn-wings") { - return "battle_legendary_dusk_dawn"; - } - if (pokemon.getFormKey() === "ultra") { - return "battle_legendary_ultra_nec"; - } - } - if ([ Species.NIHILEGO, Species.BUZZWOLE, Species.PHEROMOSA, Species.XURKITREE, Species.CELESTEELA, Species.KARTANA, Species.GUZZLORD, Species.POIPOLE, Species.NAGANADEL, Species.STAKATAKA, Species.BLACEPHALON ].includes(pokemon.species.speciesId)) { - return "battle_legendary_ub"; - } - if (pokemon.species.speciesId === Species.ZACIAN || pokemon.species.speciesId === Species.ZAMAZENTA) { - return "battle_legendary_zac_zam"; - } - if (pokemon.species.speciesId === Species.GLASTRIER || pokemon.species.speciesId === Species.SPECTRIER) { - return "battle_legendary_glas_spec"; - } - if (pokemon.species.speciesId === Species.CALYREX) { - if (pokemon.getFormKey() === "") { + case Species.NECROZMA: + switch (pokemon.getFormKey()) { + case "dusk-mane": + case "dawn-wings": + return "battle_legendary_dusk_dawn"; + case "ultra": + return "battle_legendary_ultra_nec"; + default: + return "battle_legendary_sol_lun"; + } + case Species.NIHILEGO: + case Species.PHEROMOSA: + case Species.BUZZWOLE: + case Species.XURKITREE: + case Species.CELESTEELA: + case Species.KARTANA: + case Species.GUZZLORD: + case Species.POIPOLE: + case Species.NAGANADEL: + case Species.STAKATAKA: + case Species.BLACEPHALON: + return "battle_legendary_ub"; + case Species.ZACIAN: + case Species.ZAMAZENTA: + return "battle_legendary_zac_zam"; + case Species.GLASTRIER: + case Species.SPECTRIER: + return "battle_legendary_glas_spec"; + case Species.CALYREX: + if (pokemon.getFormKey() === "ice" || pokemon.getFormKey() === "shadow") { + return "battle_legendary_riders"; + } return "battle_legendary_calyrex"; - } - if (pokemon.getFormKey() === "ice" || pokemon.getFormKey() === "shadow") { - return "battle_legendary_riders"; - } + case Species.GALAR_ARTICUNO: + case Species.GALAR_ZAPDOS: + case Species.GALAR_MOLTRES: + return "battle_legendary_birds_galar"; + case Species.WO_CHIEN: + case Species.CHIEN_PAO: + case Species.TING_LU: + case Species.CHI_YU: + return "battle_legendary_ruinous"; + case Species.KORAIDON: + case Species.MIRAIDON: + return "battle_legendary_kor_mir"; + case Species.OKIDOGI: + case Species.MUNKIDORI: + case Species.FEZANDIPITI: + return "battle_legendary_loyal_three"; + case Species.OGERPON: + return "battle_legendary_ogerpon"; + case Species.TERAPAGOS: + return "battle_legendary_terapagos"; + case Species.PECHARUNT: + return "battle_legendary_pecharunt"; + default: + if (pokemon.species.legendary) { + return "battle_legendary_res_zek"; + } + return "battle_legendary_unova"; } - if (pokemon.species.speciesId === Species.GALAR_ARTICUNO || pokemon.species.speciesId === Species.GALAR_ZAPDOS || pokemon.species.speciesId === Species.GALAR_MOLTRES) { - return "battle_legendary_birds_galar"; - } - if (pokemon.species.speciesId === Species.WO_CHIEN || pokemon.species.speciesId === Species.CHIEN_PAO || pokemon.species.speciesId === Species.TING_LU || pokemon.species.speciesId === Species.CHI_YU) { - return "battle_legendary_ruinous"; - } - if (pokemon.species.speciesId === Species.KORAIDON || pokemon.species.speciesId === Species.MIRAIDON) { - return "battle_legendary_kor_mir"; - } - if (pokemon.species.speciesId === Species.OKIDOGI || pokemon.species.speciesId === Species.MUNKIDORI || pokemon.species.speciesId === Species.FEZANDIPITI) { - return "battle_legendary_loyal_three"; - } - if (pokemon.species.speciesId === Species.OGERPON) { - return "battle_legendary_ogerpon"; - } - if (pokemon.species.speciesId === Species.TERAPAGOS) { - return "battle_legendary_terapagos"; - } - if (pokemon.species.speciesId === Species.PECHARUNT) { - return "battle_legendary_pecharunt"; - } - if (pokemon.species.legendary) { - return "battle_legendary_res_zek"; - } - return "battle_legendary_unova"; } } } diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 1d65fd6c9a6..90f6ee942be 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -1,6 +1,7 @@ import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { TrainerSlot, } from "#app/data/trainer-config"; import { ModifierTier } from "#app/modifier/modifier-tier"; +import { MusicPreference } from "#app/system/settings/settings"; import { getPlayerModifierTypeOptions, ModifierPoolType, ModifierTypeOption, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; @@ -105,7 +106,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = // Load bgm let bgmKey: string; - if (scene.musicPreference === 0) { + if (scene.musicPreference === MusicPreference.CONSISTENT) { bgmKey = "mystery_encounter_gen_5_gts"; scene.loadBgm(bgmKey, `${bgmKey}.mp3`); } else { diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 107fa5073af..0cdeef10d9e 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -460,7 +460,7 @@ export abstract class PokemonSpeciesForm { break; } } - return ret; + return `cry/${ret}`; } validateStarterMoveset(moveset: StarterMoveset, eggMoves: number): boolean { @@ -488,7 +488,7 @@ export abstract class PokemonSpeciesForm { return new Promise(resolve => { const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant); scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant)); - scene.load.audio(`cry/${this.getCryKey(formIndex)}`, `audio/cry/${this.getCryKey(formIndex)}.m4a`); + scene.load.audio(`${this.getCryKey(formIndex)}`, `audio/${this.getCryKey(formIndex)}.m4a`); scene.load.once(Phaser.Loader.Events.COMPLETE, () => { const originalWarn = console.warn; // Ignore warnings for missing frames, because there will be a lot @@ -546,7 +546,7 @@ export abstract class PokemonSpeciesForm { if (cry?.pendingRemove) { cry = null; } - cry = scene.playSound(`cry/${(cry ?? cryKey)}`, soundConfig); + cry = scene.playSound(cry ?? cryKey, soundConfig); if (ignorePlay) { cry.stop(); } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d0cb0aa1ff6..e0ed96504b1 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3266,7 +3266,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { try { SoundFade.fadeOut(scene, cry, Utils.fixedInt(Math.ceil(duration * 0.2))); fusionCry = this.getFusionSpeciesForm().cry(scene, Object.assign({ seek: Math.max(fusionCry.totalDuration * 0.4, 0) }, soundConfig)); - SoundFade.fadeIn(scene, fusionCry, Utils.fixedInt(Math.ceil(duration * 0.2)), scene.masterVolume * scene.seVolume, 0); + SoundFade.fadeIn(scene, fusionCry, Utils.fixedInt(Math.ceil(duration * 0.2)), scene.masterVolume * scene.fieldVolume, 0); } catch (err) { console.error(err); } @@ -3281,11 +3281,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.fusionFaintCry(callback); } - const key = `cry/${this.species.getCryKey(this.formIndex)}`; - //eslint-disable-next-line @typescript-eslint/no-unused-vars - let i = 0; + const key = this.species.getCryKey(this.formIndex); let rate = 0.85; const cry = this.scene.playSound(key, { rate: rate }) as AnySound; + if (!cry || this.scene.fieldVolume === 0) { + return callback(); + } const sprite = this.getSprite(); const tintSprite = this.getTintSprite(); const delay = Math.max(this.scene.sound.get(key).totalDuration * 50, 25); @@ -3300,7 +3301,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { delay: Utils.fixedInt(delay), repeat: -1, callback: () => { - ++i; frameThreshold = sprite.anims.msPerFrame / rate; frameProgress += delay; while (frameProgress > frameThreshold) { @@ -3339,7 +3339,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } private fusionFaintCry(callback: Function): void { - const key = `cry/${this.species.getCryKey(this.formIndex)}`; + const key = this.species.getCryKey(this.formIndex); let i = 0; let rate = 0.85; const cry = this.scene.playSound(key, { rate: rate }) as AnySound; @@ -3347,12 +3347,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const tintSprite = this.getTintSprite(); let duration = cry.totalDuration * 1000; - const fusionCryKey = `cry/${this.fusionSpecies?.getCryKey(this.fusionFormIndex)}`; + const fusionCryKey = this.fusionSpecies!.getCryKey(this.fusionFormIndex); let fusionCry = this.scene.playSound(fusionCryKey, { rate: rate }) as AnySound; + if (!cry || !fusionCry || this.scene.fieldVolume === 0) { + return callback(); + } fusionCry.stop(); duration = Math.min(duration, fusionCry.totalDuration * 1000); fusionCry.destroy(); - const delay = Math.max(duration * 0.05, 25); let transitionIndex = 0; @@ -3390,10 +3392,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } frameProgress -= frameThreshold; } - if (i === transitionIndex) { + if (i === transitionIndex && fusionCryKey) { SoundFade.fadeOut(this.scene, cry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2))); fusionCry = this.scene.playSound(fusionCryKey, Object.assign({ seek: Math.max(fusionCry.totalDuration * 0.4, 0), rate: rate })); - SoundFade.fadeIn(this.scene, fusionCry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2)), this.scene.masterVolume * this.scene.seVolume, 0); + SoundFade.fadeIn(this.scene, fusionCry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2)), this.scene.masterVolume * this.scene.fieldVolume, 0); } rate *= 0.99; if (cry && !cry.pendingRemove) { diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index e6fb884ffdf..d71edf603dd 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -163,6 +163,11 @@ export const SettingKeys = { Shop_Overlay_Opacity: "SHOP_OVERLAY_OPACITY" }; +export enum MusicPreference { + CONSISTENT, + MIXED +} + /** * All Settings not related to controls */ @@ -634,7 +639,7 @@ export const Setting: Array = [ label: i18next.t("settings:mixed") } ], - default: 0, + default: MusicPreference.MIXED, type: SettingType.AUDIO, requireReload: true }, diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index a7d67911228..fe8d06c2c3b 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -156,6 +156,7 @@ export default class GameManager { this.scene.enableTutorials = false; this.scene.gameData.gender = PlayerGender.MALE; // set initial player gender this.scene.battleStyle = this.settings.battleStyle; + this.scene.fieldVolume = 0; } /** From 618dfbd32442029da04e890feb973744020fc63e Mon Sep 17 00:00:00 2001 From: "Adrian T." <68144167+torranx@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:40:24 +0800 Subject: [PATCH 21/98] [Refactor] Reduce nesting in doEvolution() (#4786) Co-authored-by: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> --- src/phases/evolution-phase.ts | 220 ++++++++++++++++++---------------- 1 file changed, 120 insertions(+), 100 deletions(-) diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index 7b079c3fbf9..dec65e2c945 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -1,12 +1,12 @@ import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import { Phase } from "#app/phase"; -import BattleScene from "#app/battle-scene"; +import BattleScene, { AnySound } from "#app/battle-scene"; import { SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import EvolutionSceneHandler from "#app/ui/evolution-scene-handler"; import * as Utils from "#app/utils"; import { Mode } from "#app/ui/ui"; import { cos, sin } from "#app/field/anims"; -import { PlayerPokemon } from "#app/field/pokemon"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import { getTypeRgb } from "#app/data/type"; import i18next from "i18next"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -17,7 +17,11 @@ export class EvolutionPhase extends Phase { protected pokemon: PlayerPokemon; protected lastLevel: integer; + private preEvolvedPokemonName: string; + private evolution: SpeciesFormEvolution | null; + private evolutionBgm: AnySound; + private evolutionHandler: EvolutionSceneHandler; protected evolutionContainer: Phaser.GameObjects.Container; protected evolutionBaseBg: Phaser.GameObjects.Image; @@ -35,6 +39,8 @@ export class EvolutionPhase extends Phase { this.pokemon = pokemon; this.evolution = evolution; this.lastLevel = lastLevel; + this.evolutionBgm = this.scene.playSoundWithoutBgm("evolution"); + this.preEvolvedPokemonName = getPokemonNameWithAffix(this.pokemon); } validate(): boolean { @@ -117,10 +123,9 @@ export class EvolutionPhase extends Phase { } doEvolution(): void { - const evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler; - const preName = getPokemonNameWithAffix(this.pokemon); + this.evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler; - this.scene.ui.showText(i18next.t("menu:evolving", { pokemonName: preName }), null, () => { + this.scene.ui.showText(i18next.t("menu:evolving", { pokemonName: this.preEvolvedPokemonName }), null, () => { this.pokemon.cry(); this.pokemon.getPossibleEvolution(this.evolution).then(evolvedPokemon => { @@ -140,7 +145,6 @@ export class EvolutionPhase extends Phase { }); this.scene.time.delayedCall(1000, () => { - const evolutionBgm = this.scene.playSoundWithoutBgm("evolution"); this.scene.tweens.add({ targets: this.evolutionBgOverlay, alpha: 1, @@ -174,100 +178,13 @@ export class EvolutionPhase extends Phase { this.scene.time.delayedCall(1500, () => { this.pokemonEvoTintSprite.setScale(0.25); this.pokemonEvoTintSprite.setVisible(true); - evolutionHandler.canCancel = true; + this.evolutionHandler.canCancel = true; this.doCycle(1).then(success => { - if (!success) { - - this.pokemonSprite.setVisible(true); - this.pokemonTintSprite.setScale(1); - this.scene.tweens.add({ - targets: [ this.evolutionBg, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite ], - alpha: 0, - duration: 250, - onComplete: () => { - this.evolutionBg.setVisible(false); - } - }); - - SoundFade.fadeOut(this.scene, evolutionBgm, 100); - - this.scene.unshiftPhase(new EndEvolutionPhase(this.scene)); - - this.scene.ui.showText(i18next.t("menu:stoppedEvolving", { pokemonName: preName }), null, () => { - this.scene.ui.showText(i18next.t("menu:pauseEvolutionsQuestion", { pokemonName: preName }), null, () => { - const end = () => { - this.scene.ui.showText("", 0); - this.scene.playBgm(); - evolvedPokemon.destroy(); - this.end(); - }; - this.scene.ui.setOverlayMode(Mode.CONFIRM, () => { - this.scene.ui.revertMode(); - this.pokemon.pauseEvolutions = true; - this.scene.ui.showText(i18next.t("menu:evolutionsPaused", { pokemonName: preName }), null, end, 3000); - }, () => { - this.scene.ui.revertMode(); - this.scene.time.delayedCall(3000, end); - }); - }); - }, null, true); - return; + if (success) { + this.handleSuccessEvolution(evolvedPokemon); + } else { + this.handleFailedEvolution(evolvedPokemon); } - - this.scene.playSound("se/sparkle"); - this.pokemonEvoSprite.setVisible(true); - this.doCircleInward(); - this.scene.time.delayedCall(900, () => { - evolutionHandler.canCancel = false; - - this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => { - const levelMoves = this.pokemon.getLevelMoves(this.lastLevel + 1, true); - for (const lm of levelMoves) { - this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getPlayerParty().indexOf(this.pokemon), lm[1])); - } - this.scene.unshiftPhase(new EndEvolutionPhase(this.scene)); - - this.scene.playSound("se/shine"); - this.doSpray(); - this.scene.tweens.add({ - targets: this.evolutionOverlay, - alpha: 1, - duration: 250, - easing: "Sine.easeIn", - onComplete: () => { - this.evolutionBgOverlay.setAlpha(1); - this.evolutionBg.setVisible(false); - this.scene.tweens.add({ - targets: [ this.evolutionOverlay, this.pokemonEvoTintSprite ], - alpha: 0, - duration: 2000, - delay: 150, - easing: "Sine.easeIn", - onComplete: () => { - this.scene.tweens.add({ - targets: this.evolutionBgOverlay, - alpha: 0, - duration: 250, - onComplete: () => { - SoundFade.fadeOut(this.scene, evolutionBgm, 100); - this.scene.time.delayedCall(250, () => { - this.pokemon.cry(); - this.scene.time.delayedCall(1250, () => { - this.scene.playSoundWithoutBgm("evolution_fanfare"); - - evolvedPokemon.destroy(); - this.scene.ui.showText(i18next.t("menu:evolutionDone", { pokemonName: preName, evolvedPokemonName: this.pokemon.name }), null, () => this.end(), null, true, Utils.fixedInt(4000)); - this.scene.time.delayedCall(Utils.fixedInt(4250), () => this.scene.playBgm()); - }); - }); - } - }); - } - }); - } - }); - }); - }); }); }); }); @@ -280,6 +197,110 @@ export class EvolutionPhase extends Phase { }, 1000); } + /** + * Handles a failed/stopped evolution + * @param evolvedPokemon - The evolved Pokemon + */ + private handleFailedEvolution(evolvedPokemon: Pokemon): void { + this.pokemonSprite.setVisible(true); + this.pokemonTintSprite.setScale(1); + this.scene.tweens.add({ + targets: [ this.evolutionBg, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite ], + alpha: 0, + duration: 250, + onComplete: () => { + this.evolutionBg.setVisible(false); + } + }); + + SoundFade.fadeOut(this.scene, this.evolutionBgm, 100); + + this.scene.unshiftPhase(new EndEvolutionPhase(this.scene)); + + this.scene.ui.showText(i18next.t("menu:stoppedEvolving", { pokemonName: this.preEvolvedPokemonName }), null, () => { + this.scene.ui.showText(i18next.t("menu:pauseEvolutionsQuestion", { pokemonName: this.preEvolvedPokemonName }), null, () => { + const end = () => { + this.scene.ui.showText("", 0); + this.scene.playBgm(); + evolvedPokemon.destroy(); + this.end(); + }; + this.scene.ui.setOverlayMode(Mode.CONFIRM, () => { + this.scene.ui.revertMode(); + this.pokemon.pauseEvolutions = true; + this.scene.ui.showText(i18next.t("menu:evolutionsPaused", { pokemonName: this.preEvolvedPokemonName }), null, end, 3000); + }, () => { + this.scene.ui.revertMode(); + this.scene.time.delayedCall(3000, end); + }); + }); + }, null, true); + } + + /** + * Handles a successful evolution + * @param evolvedPokemon - The evolved Pokemon + */ + private handleSuccessEvolution(evolvedPokemon: Pokemon): void { + this.scene.playSound("se/sparkle"); + this.pokemonEvoSprite.setVisible(true); + this.doCircleInward(); + + const onEvolutionComplete = () => { + SoundFade.fadeOut(this.scene, this.evolutionBgm, 100); + this.scene.time.delayedCall(250, () => { + this.pokemon.cry(); + this.scene.time.delayedCall(1250, () => { + this.scene.playSoundWithoutBgm("evolution_fanfare"); + + evolvedPokemon.destroy(); + this.scene.ui.showText(i18next.t("menu:evolutionDone", { pokemonName: this.preEvolvedPokemonName, evolvedPokemonName: this.pokemon.name }), null, () => this.end(), null, true, Utils.fixedInt(4000)); + this.scene.time.delayedCall(Utils.fixedInt(4250), () => this.scene.playBgm()); + }); + }); + }; + + this.scene.time.delayedCall(900, () => { + this.evolutionHandler.canCancel = false; + + this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => { + const levelMoves = this.pokemon.getLevelMoves(this.lastLevel + 1, true); + for (const lm of levelMoves) { + this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getPlayerParty().indexOf(this.pokemon), lm[1])); + } + this.scene.unshiftPhase(new EndEvolutionPhase(this.scene)); + + this.scene.playSound("se/shine"); + this.doSpray(); + this.scene.tweens.add({ + targets: this.evolutionOverlay, + alpha: 1, + duration: 250, + easing: "Sine.easeIn", + onComplete: () => { + this.evolutionBgOverlay.setAlpha(1); + this.evolutionBg.setVisible(false); + this.scene.tweens.add({ + targets: [ this.evolutionOverlay, this.pokemonEvoTintSprite ], + alpha: 0, + duration: 2000, + delay: 150, + easing: "Sine.easeIn", + onComplete: () => { + this.scene.tweens.add({ + targets: this.evolutionBgOverlay, + alpha: 0, + duration: 250, + onComplete: onEvolutionComplete + }); + } + }); + } + }); + }); + }); + } + doSpiralUpward() { let f = 0; @@ -320,7 +341,6 @@ export class EvolutionPhase extends Phase { doCycle(l: number, lastCycle: integer = 15): Promise { return new Promise(resolve => { - const evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler; const isLastCycle = l === lastCycle; this.scene.tweens.add({ targets: this.pokemonTintSprite, @@ -336,7 +356,7 @@ export class EvolutionPhase extends Phase { duration: 500 / l, yoyo: !isLastCycle, onComplete: () => { - if (evolutionHandler.cancelled) { + if (this.evolutionHandler.cancelled) { return resolve(false); } if (l < lastCycle) { From a70f0860e1c0a88b29b10d64160e2e2c7511b9fa Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 3 Nov 2024 20:34:26 -0800 Subject: [PATCH 22/98] [Beta Bug] Enemy pokemon shouldn't get unimplemented moves (#4787) --- src/field/pokemon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e0ed96504b1..78f82e929de 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2101,7 +2101,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (weight === 1 && allMoves[levelMove[1]].power >= 80) { weight = 40; } - if (!movePool.some(m => m[0] === levelMove[1])) { + if (!movePool.some(m => m[0] === levelMove[1]) && !allMoves[levelMove[1]].name.endsWith(" (N)")) { movePool.push([ levelMove[1], weight ]); } } From 7a0c88e661cad2d3ea8d83fa3c537a923fe288fb Mon Sep 17 00:00:00 2001 From: flx-sta <50131232+flx-sta@users.noreply.github.com> Date: Mon, 4 Nov 2024 12:57:21 -0800 Subject: [PATCH 23/98] [Refactor] Pokerogue API client (#4583) * start migrating Utils.apiFetch to api class * move dailyranking to api * use api in title-ui-handler * remove: Utils.apiFetch * migrate `updateSystemSavedata` to api * migrate clear session savedata to api * migrate updateAllSavedata to api * migrate `updateSessionSavedata` to api * rename `api` to `pokerogue-api` * migrate unlink discord to pokerogue-api * migrate unlink google to pokerogue-api * update pokerogue-api login * migrate register account to pokerogue-api * remove Utils.apiPost * reset overrides.ts * chore: cleanup * fix env.development * fix circular dependencies with api * fix gamedata verify missing await * fix daily api calls in daily-run-scorebard * fix discord-link request body being empty there was a double `toUrlSearchParams()` call involved * add pokerogue-api test coverge * add test-utils `getApiBaseUrl()` method * add pokerogue-admin-api test coverage * add pokerogue-account-api test coverage * add pokerogue-daily-api test coverage * add pokerogue-savedata-api test coverage * fix some test describes * add pokerogue-session-savedata-api test coverage * add pokerogue-system-savedata-api test coverage * fix tests * fix tryExportData thanks @MokaStitcher * chore: fix menu-ui-handlers.ts * fix admin-ui-handler (types) * extend test-coverage for admin-api * remove outdated code * skip some clowning-around-encounter tests if events are active this is not a permanent solution * Update src/system/game-data.ts Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> * Revert "skip some clowning-around-encounter tests if events are active" This reverts commit a97dafe8b2479e9b2ddd49d4dc9710814d7c7b67. * mark `localServerUrl` and `apiUrl` as deprecated in `utils.ts` --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> --- global.d.ts | 2 +- src/@types/PokerogueAccountApi.ts | 17 ++ src/@types/PokerogueAdminApi.ts | 31 +++ src/@types/PokerogueApi.ts | 4 + src/@types/PokerogueDailyApi.ts | 10 + src/@types/PokerogueSavedataApi.ts | 8 + src/@types/PokerogueSessionSavedataApi.ts | 39 +++ src/@types/PokerogueSystemSavedataApi.ts | 20 ++ src/@types/UserInfo.ts | 7 + src/@types/pokerogue-api.ts | 9 - src/account.ts | 26 +- src/constants.ts | 6 + src/data/daily-run.ts | 12 +- src/phases/game-over-phase.ts | 8 +- src/phases/title-phase.ts | 2 +- src/plugins/api/api-base.ts | 91 +++++++ src/plugins/api/pokerogue-account-api.ts | 101 ++++++++ src/plugins/api/pokerogue-admin-api.ts | 140 +++++++++++ src/plugins/api/pokerogue-api.ts | 83 +++++++ src/plugins/api/pokerogue-daily-api.ts | 57 +++++ src/plugins/api/pokerogue-savedata-api.ts | 41 ++++ .../api/pokerogue-session-savedata-api.ts | 115 +++++++++ .../api/pokerogue-system-savedata-api.ts | 77 ++++++ src/system/game-data.ts | 111 ++++----- src/test/account.test.ts | 33 ++- src/test/daily_mode.test.ts | 15 +- src/test/misc.test.ts | 26 +- .../plugins/api/pokerogue-account-api.test.ts | 157 ++++++++++++ .../plugins/api/pokerogue-admin-api.test.ts | 232 ++++++++++++++++++ src/test/plugins/api/pokerogue-api.test.ts | 97 ++++++++ .../plugins/api/pokerogue-daily-api.test.ts | 89 +++++++ .../api/pokerogue-savedata-api.test.ts | 46 ++++ .../pokerogue-session-savedata-api.test.ts | 199 +++++++++++++++ .../api/pokerogue-system-savedata-api.test.ts | 122 +++++++++ src/test/reload.test.ts | 20 +- src/test/system/game_data.test.ts | 26 +- src/test/utils/gameWrapper.ts | 2 +- src/test/utils/testUtils.ts | 8 + src/test/vitest.setup.ts | 11 +- src/ui/admin-ui-handler.ts | 75 ++++-- src/ui/daily-run-scoreboard.ts | 16 +- src/ui/login-form-ui-handler.ts | 26 +- src/ui/menu-ui-handler.ts | 17 +- src/ui/registration-form-ui-handler.ts | 27 +- src/ui/title-ui-handler.ts | 12 +- src/utils.ts | 52 +--- 46 files changed, 2036 insertions(+), 289 deletions(-) create mode 100644 src/@types/PokerogueAccountApi.ts create mode 100644 src/@types/PokerogueAdminApi.ts create mode 100644 src/@types/PokerogueApi.ts create mode 100644 src/@types/PokerogueDailyApi.ts create mode 100644 src/@types/PokerogueSavedataApi.ts create mode 100644 src/@types/PokerogueSessionSavedataApi.ts create mode 100644 src/@types/PokerogueSystemSavedataApi.ts create mode 100644 src/@types/UserInfo.ts delete mode 100644 src/@types/pokerogue-api.ts create mode 100644 src/plugins/api/api-base.ts create mode 100644 src/plugins/api/pokerogue-account-api.ts create mode 100644 src/plugins/api/pokerogue-admin-api.ts create mode 100644 src/plugins/api/pokerogue-api.ts create mode 100644 src/plugins/api/pokerogue-daily-api.ts create mode 100644 src/plugins/api/pokerogue-savedata-api.ts create mode 100644 src/plugins/api/pokerogue-session-savedata-api.ts create mode 100644 src/plugins/api/pokerogue-system-savedata-api.ts create mode 100644 src/test/plugins/api/pokerogue-account-api.test.ts create mode 100644 src/test/plugins/api/pokerogue-admin-api.test.ts create mode 100644 src/test/plugins/api/pokerogue-api.test.ts create mode 100644 src/test/plugins/api/pokerogue-daily-api.test.ts create mode 100644 src/test/plugins/api/pokerogue-savedata-api.test.ts create mode 100644 src/test/plugins/api/pokerogue-session-savedata-api.test.ts create mode 100644 src/test/plugins/api/pokerogue-system-savedata-api.test.ts diff --git a/global.d.ts b/global.d.ts index f4dfa7d4cb2..c896a4983e4 100644 --- a/global.d.ts +++ b/global.d.ts @@ -10,5 +10,5 @@ declare global { * * To set up your own server in a test see `game_data.test.ts` */ - var i18nServer: SetupServerApi; + var server: SetupServerApi; } diff --git a/src/@types/PokerogueAccountApi.ts b/src/@types/PokerogueAccountApi.ts new file mode 100644 index 00000000000..68d0a5e7730 --- /dev/null +++ b/src/@types/PokerogueAccountApi.ts @@ -0,0 +1,17 @@ +import type { UserInfo } from "#app/@types/UserInfo"; + +export interface AccountInfoResponse extends UserInfo {} + +export interface AccountLoginRequest { + username: string; + password: string; +} + +export interface AccountLoginResponse { + token: string; +} + +export interface AccountRegisterRequest { + username: string; + password: string; +} diff --git a/src/@types/PokerogueAdminApi.ts b/src/@types/PokerogueAdminApi.ts new file mode 100644 index 00000000000..2ee25b560d9 --- /dev/null +++ b/src/@types/PokerogueAdminApi.ts @@ -0,0 +1,31 @@ +export interface LinkAccountToDiscordIdRequest { + username: string; + discordId: string; +} + +export interface UnlinkAccountFromDiscordIdRequest { + username: string; + discordId: string; +} + +export interface LinkAccountToGoogledIdRequest { + username: string; + googleId: string; +} + +export interface UnlinkAccountFromGoogledIdRequest { + username: string; + googleId: string; +} + +export interface SearchAccountRequest { + username: string; +} + +export interface SearchAccountResponse { + username: string; + discordId: string; + googleId: string; + lastLoggedIn: string; + registered: string; +} diff --git a/src/@types/PokerogueApi.ts b/src/@types/PokerogueApi.ts new file mode 100644 index 00000000000..79755b23a54 --- /dev/null +++ b/src/@types/PokerogueApi.ts @@ -0,0 +1,4 @@ +export interface TitleStatsResponse { + playerCount: number; + battleCount: number; +} diff --git a/src/@types/PokerogueDailyApi.ts b/src/@types/PokerogueDailyApi.ts new file mode 100644 index 00000000000..3f3d8eb61ca --- /dev/null +++ b/src/@types/PokerogueDailyApi.ts @@ -0,0 +1,10 @@ +import type { ScoreboardCategory } from "#app/ui/daily-run-scoreboard"; + +export interface GetDailyRankingsRequest { + category: ScoreboardCategory; + page?: number; +} + +export interface GetDailyRankingsPageCountRequest { + category: ScoreboardCategory; +} diff --git a/src/@types/PokerogueSavedataApi.ts b/src/@types/PokerogueSavedataApi.ts new file mode 100644 index 00000000000..a313cd708c7 --- /dev/null +++ b/src/@types/PokerogueSavedataApi.ts @@ -0,0 +1,8 @@ +import type { SessionSaveData, SystemSaveData } from "#app/system/game-data"; + +export interface UpdateAllSavedataRequest { + system: SystemSaveData; + session: SessionSaveData; + sessionSlotId: number; + clientSessionId: string; +} diff --git a/src/@types/PokerogueSessionSavedataApi.ts b/src/@types/PokerogueSessionSavedataApi.ts new file mode 100644 index 00000000000..5fcd8575b15 --- /dev/null +++ b/src/@types/PokerogueSessionSavedataApi.ts @@ -0,0 +1,39 @@ +export class UpdateSessionSavedataRequest { + slot: number; + trainerId: number; + secretId: number; + clientSessionId: string; +} + +/** This is **NOT** similar to {@linkcode ClearSessionSavedataRequest} */ +export interface NewClearSessionSavedataRequest { + slot: number; + clientSessionId: string; +} + +export interface GetSessionSavedataRequest { + slot: number; + clientSessionId: string; +} + +export interface DeleteSessionSavedataRequest { + slot: number; + clientSessionId: string; +} + +/** This is **NOT** similar to {@linkcode NewClearSessionSavedataRequest} */ +export interface ClearSessionSavedataRequest { + slot: number; + trainerId: number; + clientSessionId: string; +} + +/** + * Pokerogue API response for path: `/savedata/session/clear` + */ +export interface ClearSessionSavedataResponse { + /** Contains the error message if any occured */ + error?: string; + /** Is `true` if the request was successfully processed */ + success?: boolean; +} diff --git a/src/@types/PokerogueSystemSavedataApi.ts b/src/@types/PokerogueSystemSavedataApi.ts new file mode 100644 index 00000000000..8ce160a5ec2 --- /dev/null +++ b/src/@types/PokerogueSystemSavedataApi.ts @@ -0,0 +1,20 @@ +import type { SystemSaveData } from "#app/system/game-data"; + +export interface GetSystemSavedataRequest { + clientSessionId: string; +} + +export class UpdateSystemSavedataRequest { + clientSessionId: string; + trainerId?: number; + secretId?: number; +} + +export interface VerifySystemSavedataRequest { + clientSessionId: string; +} + +export interface VerifySystemSavedataResponse { + valid: boolean; + systemData: SystemSaveData; +} diff --git a/src/@types/UserInfo.ts b/src/@types/UserInfo.ts new file mode 100644 index 00000000000..c8a0c6ecb26 --- /dev/null +++ b/src/@types/UserInfo.ts @@ -0,0 +1,7 @@ +export interface UserInfo { + username: string; + lastSessionSlot: number; + discordId: string; + googleId: string; + hasAdminRole: boolean; +} diff --git a/src/@types/pokerogue-api.ts b/src/@types/pokerogue-api.ts deleted file mode 100644 index 892869968bb..00000000000 --- a/src/@types/pokerogue-api.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Pokerogue API response for path: `/savedata/session/clear` - */ -export interface PokerogueApiClearSessionData { - /** Contains the error message if any occured */ - error?: string; - /** Is `true` if the request was successfully processed */ - success?: boolean; -} diff --git a/src/account.ts b/src/account.ts index 692ff2b0d81..316645b38ff 100644 --- a/src/account.ts +++ b/src/account.ts @@ -1,14 +1,8 @@ +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; +import type { UserInfo } from "#app/@types/UserInfo"; import { bypassLogin } from "./battle-scene"; import * as Utils from "./utils"; -export interface UserInfo { - username: string; - lastSessionSlot: integer; - discordId: string; - googleId: string; - hasAdminRole: boolean; -} - export let loggedInUser: UserInfo | null = null; // This is a random string that is used to identify the client session - unique per session (tab or window) so that the game will only save on the one that the server is expecting export const clientSessionId = Utils.randomString(32); @@ -43,18 +37,14 @@ export function updateUserInfo(): Promise<[boolean, integer]> { }); return resolve([ true, 200 ]); } - Utils.apiFetch("account/info", true).then(response => { - if (!response.ok) { - resolve([ false, response.status ]); + pokerogueApi.account.getInfo().then(([ accountInfo, status ]) => { + if (!accountInfo) { + resolve([ false, status ]); return; + } else { + loggedInUser = accountInfo; + resolve([ true, 200 ]); } - return response.json(); - }).then(jsonResponse => { - loggedInUser = jsonResponse; - resolve([ true, 200 ]); - }).catch(err => { - console.error(err); - resolve([ false, 500 ]); }); }); } diff --git a/src/constants.ts b/src/constants.ts index 0b1261ad814..63f00b9f33f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,3 +3,9 @@ export const PLAYER_PARTY_MAX_SIZE: number = 6; /** Whether to use seasonal splash messages in general */ export const USE_SEASONAL_SPLASH_MESSAGES: boolean = false; + +/** Name of the session ID cookie */ +export const SESSION_ID_COOKIE_NAME: string = "pokerogue_sessionId"; + +/** Max value for an integer attribute in {@linkcode SystemSaveData} */ +export const MAX_INT_ATTR_VALUE = 0x80000000; diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index 0decab63f4f..506ea0471c6 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -6,6 +6,7 @@ import { Starter } from "#app/ui/starter-select-ui-handler"; import * as Utils from "#app/utils"; import PokemonSpecies, { PokemonSpeciesForm, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { speciesStarterCosts } from "#app/data/balance/starters"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; export interface DailyRunConfig { seed: integer; @@ -14,14 +15,9 @@ export interface DailyRunConfig { export function fetchDailyRunSeed(): Promise { return new Promise((resolve, reject) => { - Utils.apiFetch("daily/seed").then(response => { - if (!response.ok) { - resolve(null); - return; - } - return response.text(); - }).then(seed => resolve(seed ?? null)) - .catch(err => reject(err)); + pokerogueApi.daily.getSeed().then(dailySeed => { + resolve(dailySeed); + }); }); } diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 1302d8fc652..84fad257897 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -23,6 +23,7 @@ import * as Utils from "#app/utils"; import { PlayerGender } from "#enums/player-gender"; import { TrainerType } from "#enums/trainer-type"; import i18next from "i18next"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; export class GameOverPhase extends BattlePhase { private victory: boolean; @@ -176,10 +177,9 @@ export class GameOverPhase extends BattlePhase { If Online, execute apiFetch as intended If Offline, execute offlineNewClear(), a localStorage implementation of newClear daily run checks */ if (this.victory) { - if (!Utils.isLocal) { - Utils.apiFetch(`savedata/session/newclear?slot=${this.scene.sessionSlotId}&clientSessionId=${clientSessionId}`, true) - .then(response => response.json()) - .then(newClear => doGameOver(newClear)); + if (!Utils.isLocal || Utils.isLocalServerConnected) { + pokerogueApi.savedata.session.newclear({ slot: this.scene.sessionSlotId, clientSessionId }) + .then((success) => doGameOver(!!success)); } else { this.scene.gameData.offlineNewClear(this.scene).then(result => { doGameOver(result); diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 8338d39b81f..88793617776 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -243,7 +243,7 @@ export class TitlePhase extends Phase { }; // If Online, calls seed fetch from db to generate daily run. If Offline, generates a daily run based on current date. - if (!Utils.isLocal) { + if (!Utils.isLocal || Utils.isLocalServerConnected) { fetchDailyRunSeed().then(seed => { if (seed) { generateDaily(seed); diff --git a/src/plugins/api/api-base.ts b/src/plugins/api/api-base.ts new file mode 100644 index 00000000000..0740de4e675 --- /dev/null +++ b/src/plugins/api/api-base.ts @@ -0,0 +1,91 @@ +import { SESSION_ID_COOKIE_NAME } from "#app/constants"; +import { getCookie } from "#app/utils"; + +type DataType = "json" | "form-urlencoded"; + +export abstract class ApiBase { + //#region Fields + + public readonly ERR_GENERIC: string = "There was an error"; + + protected readonly base: string; + + //#region Public + + constructor(base: string) { + this.base = base; + } + + //#region Protected + + /** + * Send a GET request. + * @param path The path to send the request to. + */ + protected async doGet(path: string) { + return this.doFetch(path, { method: "GET" }); + } + + /** + * Send a POST request. + * @param path THe path to send the request to. + * @param bodyData The body-data to send. + * @param dataType The data-type of the {@linkcode bodyData}. + */ + protected async doPost(path: string, bodyData?: D, dataType: DataType = "json") { + let body: string | undefined = undefined; + const headers: HeadersInit = {}; + + if (bodyData) { + if (dataType === "json") { + body = typeof bodyData === "string" ? bodyData : JSON.stringify(bodyData); + headers["Content-Type"] = "application/json"; + } else if (dataType === "form-urlencoded") { + if (bodyData instanceof Object) { + body = this.toUrlSearchParams(bodyData).toString(); + } else { + console.warn("Could not add body data to form-urlencoded!", bodyData); + } + headers["Content-Type"] = "application/x-www-form-urlencoded"; + } else { + console.warn(`Unsupported data type: ${dataType}`); + body = String(bodyData); + headers["Content-Type"] = "text/plain"; + } + } + + return await this.doFetch(path, { method: "POST", body, headers }); + } + + /** + * A generic request helper. + * @param path The path to send the request to. + * @param config The request {@linkcode RequestInit | Configuration}. + */ + protected async doFetch(path: string, config: RequestInit): Promise { + config.headers = { + ...config.headers, + Authorization: getCookie(SESSION_ID_COOKIE_NAME), + "Content-Type": config.headers?.["Content-Type"] ?? "application/json", + }; + + console.log(`Sending ${config.method ?? "GET"} request to: `, this.base + path, config); + + return await fetch(this.base + path, config); + } + + /** + * Helper to transform data to {@linkcode URLSearchParams} + * Any key with a value of `undefined` will be ignored. + * Any key with a value of `null` will be included. + * @param data the data to transform to {@linkcode URLSearchParams} + * @returns a {@linkcode URLSearchParams} representaton of {@linkcode data} + */ + protected toUrlSearchParams>(data: D) { + const arr = Object.entries(data) + .map(([ key, value ]) => (value !== undefined ? [ key, String(value) ] : [ key, "" ])) + .filter(([ , value ]) => value !== ""); + + return new URLSearchParams(arr); + } +} diff --git a/src/plugins/api/pokerogue-account-api.ts b/src/plugins/api/pokerogue-account-api.ts new file mode 100644 index 00000000000..66ab8d67520 --- /dev/null +++ b/src/plugins/api/pokerogue-account-api.ts @@ -0,0 +1,101 @@ +import type { + AccountInfoResponse, + AccountLoginRequest, + AccountLoginResponse, + AccountRegisterRequest, +} from "#app/@types/PokerogueAccountApi"; +import { SESSION_ID_COOKIE_NAME } from "#app/constants"; +import { ApiBase } from "#app/plugins/api/api-base"; +import { removeCookie, setCookie } from "#app/utils"; + +/** + * A wrapper for PokéRogue account API requests. + */ +export class PokerogueAccountApi extends ApiBase { + //#region Public + + /** + * Request the {@linkcode AccountInfoResponse | UserInfo} of the logged in user. + * The user is identified by the {@linkcode SESSION_ID_COOKIE_NAME | session cookie}. + */ + public async getInfo(): Promise<[data: AccountInfoResponse | null, status: number]> { + try { + const response = await this.doGet("/account/info"); + + if (response.ok) { + const resData = (await response.json()) as AccountInfoResponse; + return [ resData, response.status ]; + } else { + console.warn("Could not get account info!", response.status, response.statusText); + return [ null, response.status ]; + } + } catch (err) { + console.warn("Could not get account info!", err); + return [ null, 500 ]; + } + } + + /** + * Register a new account. + * @param registerData The {@linkcode AccountRegisterRequest} to send + * @returns An error message if something went wrong + */ + public async register(registerData: AccountRegisterRequest) { + try { + const response = await this.doPost("/account/register", registerData, "form-urlencoded"); + + if (response.ok) { + return null; + } else { + return response.text(); + } + } catch (err) { + console.warn("Register failed!", err); + } + + return "Unknown error!"; + } + + /** + * Send a login request. + * Sets the session cookie on success. + * @param loginData The {@linkcode AccountLoginRequest} to send + * @returns An error message if something went wrong + */ + public async login(loginData: AccountLoginRequest) { + try { + const response = await this.doPost("/account/login", loginData, "form-urlencoded"); + + if (response.ok) { + const loginResponse = (await response.json()) as AccountLoginResponse; + setCookie(SESSION_ID_COOKIE_NAME, loginResponse.token); + return null; + } else { + console.warn("Login failed!", response.status, response.statusText); + return response.text(); + } + } catch (err) { + console.warn("Login failed!", err); + } + + return "Unknown error!"; + } + + /** + * Send a logout request. + * **Always** (no matter if failed or not) removes the session cookie. + */ + public async logout() { + try { + const response = await this.doGet("/account/logout"); + + if (!response.ok) { + throw new Error(`${response.status}: ${response.statusText}`); + } + } catch (err) { + console.warn("Log out failed!", err); + } + + removeCookie(SESSION_ID_COOKIE_NAME); // we are always clearing the cookie. + } +} diff --git a/src/plugins/api/pokerogue-admin-api.ts b/src/plugins/api/pokerogue-admin-api.ts new file mode 100644 index 00000000000..eeba5319adc --- /dev/null +++ b/src/plugins/api/pokerogue-admin-api.ts @@ -0,0 +1,140 @@ +import type { + LinkAccountToDiscordIdRequest, + LinkAccountToGoogledIdRequest, + SearchAccountRequest, + SearchAccountResponse, + UnlinkAccountFromDiscordIdRequest, + UnlinkAccountFromGoogledIdRequest, +} from "#app/@types/PokerogueAdminApi"; +import { ApiBase } from "#app/plugins/api/api-base"; + +export class PokerogueAdminApi extends ApiBase { + public readonly ERR_USERNAME_NOT_FOUND: string = "Username not found!"; + + /** + * Links an account to a discord id. + * @param params The {@linkcode LinkAccountToDiscordIdRequest} to send + * @returns `null` if successful, error message if not + */ + public async linkAccountToDiscord(params: LinkAccountToDiscordIdRequest) { + try { + const response = await this.doPost("/admin/account/discordLink", params, "form-urlencoded"); + + if (response.ok) { + return null; + } else { + console.warn("Could not link account with discord!", response.status, response.statusText); + + if (response.status === 404) { + return this.ERR_USERNAME_NOT_FOUND; + } + } + } catch (err) { + console.warn("Could not link account with discord!", err); + } + + return this.ERR_GENERIC; + } + + /** + * Unlinks an account from a discord id. + * @param params The {@linkcode UnlinkAccountFromDiscordIdRequest} to send + * @returns `null` if successful, error message if not + */ + public async unlinkAccountFromDiscord(params: UnlinkAccountFromDiscordIdRequest) { + try { + const response = await this.doPost("/admin/account/discordUnlink", params, "form-urlencoded"); + + if (response.ok) { + return null; + } else { + console.warn("Could not unlink account from discord!", response.status, response.statusText); + + if (response.status === 404) { + return this.ERR_USERNAME_NOT_FOUND; + } + } + } catch (err) { + console.warn("Could not unlink account from discord!", err); + } + + return this.ERR_GENERIC; + } + + /** + * Links an account to a google id. + * @param params The {@linkcode LinkAccountToGoogledIdRequest} to send + * @returns `null` if successful, error message if not + */ + public async linkAccountToGoogleId(params: LinkAccountToGoogledIdRequest) { + try { + const response = await this.doPost("/admin/account/googleLink", params, "form-urlencoded"); + + if (response.ok) { + return null; + } else { + console.warn("Could not link account with google!", response.status, response.statusText); + + if (response.status === 404) { + return this.ERR_USERNAME_NOT_FOUND; + } + } + } catch (err) { + console.warn("Could not link account with google!", err); + } + + return this.ERR_GENERIC; + } + + /** + * Unlinks an account from a google id. + * @param params The {@linkcode UnlinkAccountFromGoogledIdRequest} to send + * @returns `null` if successful, error message if not + */ + public async unlinkAccountFromGoogleId(params: UnlinkAccountFromGoogledIdRequest) { + try { + const response = await this.doPost("/admin/account/googleUnlink", params, "form-urlencoded"); + + if (response.ok) { + return null; + } else { + console.warn("Could not unlink account from google!", response.status, response.statusText); + + if (response.status === 404) { + return this.ERR_USERNAME_NOT_FOUND; + } + } + } catch (err) { + console.warn("Could not unlink account from google!", err); + } + + return this.ERR_GENERIC; + } + + /** + * Search an account. + * @param params The {@linkcode SearchAccountRequest} to send + * @returns an array of {@linkcode SearchAccountResponse} and error. Both can be `undefined` + */ + public async searchAccount(params: SearchAccountRequest): Promise<[data?: SearchAccountResponse, error?: string]> { + try { + const urlSearchParams = this.toUrlSearchParams(params); + const response = await this.doGet(`/admin/account/adminSearch?${urlSearchParams}`); + + if (response.ok) { + const resData: SearchAccountResponse = await response.json(); + return [ resData, undefined ]; + } else { + console.warn("Could not find account!", response.status, response.statusText); + + if (response.status === 404) { + return [ undefined, this.ERR_USERNAME_NOT_FOUND ]; + } + } + } catch (err) { + console.warn("Could not find account!", err); + } + + return [ undefined, this.ERR_GENERIC ]; + } +} diff --git a/src/plugins/api/pokerogue-api.ts b/src/plugins/api/pokerogue-api.ts new file mode 100644 index 00000000000..92d0ff1bbbb --- /dev/null +++ b/src/plugins/api/pokerogue-api.ts @@ -0,0 +1,83 @@ +import type { TitleStatsResponse } from "#app/@types/PokerogueApi"; +import { ApiBase } from "#app/plugins/api/api-base"; +import { PokerogueAccountApi } from "#app/plugins/api/pokerogue-account-api"; +import { PokerogueAdminApi } from "#app/plugins/api/pokerogue-admin-api"; +import { PokerogueDailyApi } from "#app/plugins/api/pokerogue-daily-api"; +import { PokerogueSavedataApi } from "#app/plugins/api/pokerogue-savedata-api"; + +/** + * A wrapper for PokéRogue API requests. + */ +export class PokerogueApi extends ApiBase { + //#region Fields∏ + + public readonly account: PokerogueAccountApi; + public readonly daily: PokerogueDailyApi; + public readonly admin: PokerogueAdminApi; + public readonly savedata: PokerogueSavedataApi; + + //#region Public + + constructor(base: string) { + super(base); + this.account = new PokerogueAccountApi(base); + this.daily = new PokerogueDailyApi(base); + this.admin = new PokerogueAdminApi(base); + this.savedata = new PokerogueSavedataApi(base); + } + + /** + * Request game title-stats. + */ + public async getGameTitleStats() { + try { + const response = await this.doGet("/game/titlestats"); + return (await response.json()) as TitleStatsResponse; + } catch (err) { + console.warn("Could not get game title stats!", err); + return null; + } + } + + /** + * Unlink the currently logged in user from Discord. + * @returns `true` if unlinking was successful, `false` if not + */ + public async unlinkDiscord() { + try { + const response = await this.doPost("/auth/discord/logout"); + if (response.ok) { + return true; + } else { + console.warn(`Discord unlink failed (${response.status}: ${response.statusText})`); + } + } catch (err) { + console.warn("Could not unlink Discord!", err); + } + + return false; + } + + /** + * Unlink the currently logged in user from Google. + * @returns `true` if unlinking was successful, `false` if not + */ + public async unlinkGoogle() { + try { + const response = await this.doPost("/auth/google/logout"); + if (response.ok) { + return true; + } else { + console.warn(`Google unlink failed (${response.status}: ${response.statusText})`); + } + } catch (err) { + console.warn("Could not unlink Google!", err); + } + + return false; + } + + //#endregion +} + +export const pokerogueApi = new PokerogueApi(import.meta.env.VITE_SERVER_URL ?? "http://localhost:8001"); diff --git a/src/plugins/api/pokerogue-daily-api.ts b/src/plugins/api/pokerogue-daily-api.ts new file mode 100644 index 00000000000..c9319ae7fdc --- /dev/null +++ b/src/plugins/api/pokerogue-daily-api.ts @@ -0,0 +1,57 @@ +import type { GetDailyRankingsPageCountRequest, GetDailyRankingsRequest } from "#app/@types/PokerogueDailyApi"; +import { ApiBase } from "#app/plugins/api/api-base"; +import type { RankingEntry } from "#app/ui/daily-run-scoreboard"; + +/** + * A wrapper for daily-run PokéRogue API requests. + */ +export class PokerogueDailyApi extends ApiBase { + //#region Public + + /** + * Request the daily-run seed. + * @returns The active daily-run seed as `string`. + */ + public async getSeed() { + try { + const response = await this.doGet("/daily/seed"); + return response.text(); + } catch (err) { + console.warn("Could not get daily-run seed!", err); + return null; + } + } + + /** + * Get the daily rankings for a {@linkcode ScoreboardCategory}. + * @param params The {@linkcode GetDailyRankingsRequest} to send + */ + public async getRankings(params: GetDailyRankingsRequest) { + try { + const urlSearchParams = this.toUrlSearchParams(params); + const response = await this.doGet(`/daily/rankings?${urlSearchParams}`); + + return (await response.json()) as RankingEntry[]; + } catch (err) { + console.warn("Could not get daily rankings!", err); + return null; + } + } + + /** + * Get the page count of the daily rankings for a {@linkcode ScoreboardCategory}. + * @param params The {@linkcode GetDailyRankingsPageCountRequest} to send. + */ + public async getRankingsPageCount(params: GetDailyRankingsPageCountRequest) { + try { + const urlSearchParams = this.toUrlSearchParams(params); + const response = await this.doGet(`/daily/rankingpagecount?${urlSearchParams}`); + const json = await response.json(); + + return Number(json); + } catch (err) { + console.warn("Could not get daily rankings page count!", err); + return 1; + } + } +} diff --git a/src/plugins/api/pokerogue-savedata-api.ts b/src/plugins/api/pokerogue-savedata-api.ts new file mode 100644 index 00000000000..184bfbb4bdb --- /dev/null +++ b/src/plugins/api/pokerogue-savedata-api.ts @@ -0,0 +1,41 @@ +import type { UpdateAllSavedataRequest } from "#app/@types/PokerogueSavedataApi"; +import { MAX_INT_ATTR_VALUE } from "#app/constants"; +import { ApiBase } from "#app/plugins/api/api-base"; +import { PokerogueSessionSavedataApi } from "#app/plugins/api/pokerogue-session-savedata-api"; +import { PokerogueSystemSavedataApi } from "#app/plugins/api/pokerogue-system-savedata-api"; + +/** + * A wrapper for PokéRogue savedata API requests. + */ +export class PokerogueSavedataApi extends ApiBase { + //#region Fields + + public readonly system: PokerogueSystemSavedataApi; + public readonly session: PokerogueSessionSavedataApi; + + //#region Public + + constructor(base: string) { + super(base); + this.system = new PokerogueSystemSavedataApi(base); + this.session = new PokerogueSessionSavedataApi(base); + } + + /** + * Update all savedata + * @param bodyData The {@linkcode UpdateAllSavedataRequest | request data} to send + * @returns An error message if something went wrong + */ + public async updateAll(bodyData: UpdateAllSavedataRequest) { + try { + const rawBodyData = JSON.stringify(bodyData, (_k: any, v: any) => + typeof v === "bigint" ? (v <= MAX_INT_ATTR_VALUE ? Number(v) : v.toString()) : v + ); + const response = await this.doPost("/savedata/updateall", rawBodyData); + return await response.text(); + } catch (err) { + console.warn("Could not update all savedata!", err); + return "Unknown error"; + } + } +} diff --git a/src/plugins/api/pokerogue-session-savedata-api.ts b/src/plugins/api/pokerogue-session-savedata-api.ts new file mode 100644 index 00000000000..44a7b463849 --- /dev/null +++ b/src/plugins/api/pokerogue-session-savedata-api.ts @@ -0,0 +1,115 @@ +import type { + ClearSessionSavedataRequest, + ClearSessionSavedataResponse, + DeleteSessionSavedataRequest, + GetSessionSavedataRequest, + NewClearSessionSavedataRequest, + UpdateSessionSavedataRequest, +} from "#app/@types/PokerogueSessionSavedataApi"; +import { ApiBase } from "#app/plugins/api/api-base"; +import type { SessionSaveData } from "#app/system/game-data"; + +/** + * A wrapper for PokéRogue session savedata API requests. + */ +export class PokerogueSessionSavedataApi extends ApiBase { + //#region Public + + /** + * Mark a session as cleared aka "newclear".\ + * *This is **NOT** the same as {@linkcode clear | clear()}.* + * @param params The {@linkcode NewClearSessionSavedataRequest} to send + * @returns The raw savedata as `string`. + */ + public async newclear(params: NewClearSessionSavedataRequest) { + try { + const urlSearchParams = this.toUrlSearchParams(params); + const response = await this.doGet(`/savedata/session/newclear?${urlSearchParams}`); + const json = await response.json(); + + return Boolean(json); + } catch (err) { + console.warn("Could not newclear session!", err); + return false; + } + } + + /** + * Get a session savedata. + * @param params The {@linkcode GetSessionSavedataRequest} to send + * @returns The session as `string` + */ + public async get(params: GetSessionSavedataRequest) { + try { + const urlSearchParams = this.toUrlSearchParams(params); + const response = await this.doGet(`/savedata/session/get?${urlSearchParams}`); + + return await response.text(); + } catch (err) { + console.warn("Could not get session savedata!", err); + return null; + } + } + + /** + * Update a session savedata. + * @param params The {@linkcode UpdateSessionSavedataRequest} to send + * @param rawSavedata The raw savedata (as `string`) + * @returns An error message if something went wrong + */ + public async update(params: UpdateSessionSavedataRequest, rawSavedata: string) { + try { + const urlSearchParams = this.toUrlSearchParams(params); + const response = await this.doPost(`/savedata/session/update?${urlSearchParams}`, rawSavedata); + + return await response.text(); + } catch (err) { + console.warn("Could not update session savedata!", err); + } + + return "Unknown Error!"; + } + + /** + * Delete a session savedata slot. + * @param params The {@linkcode DeleteSessionSavedataRequest} to send + * @returns An error message if something went wrong + */ + public async delete(params: DeleteSessionSavedataRequest) { + try { + const urlSearchParams = this.toUrlSearchParams(params); + const response = await this.doGet(`/savedata/session/delete?${urlSearchParams}`); + + if (response.ok) { + return null; + } else { + return await response.text(); + } + } catch (err) { + console.warn("Could not delete session savedata!", err); + return "Unknown error"; + } + } + + /** + * Clears the session savedata of the given slot.\ + * *This is **NOT** the same as {@linkcode newclear | newclear()}.* + * @param params The {@linkcode ClearSessionSavedataRequest} to send + * @param sessionData The {@linkcode SessionSaveData} object + */ + public async clear(params: ClearSessionSavedataRequest, sessionData: SessionSaveData) { + try { + const urlSearchParams = this.toUrlSearchParams(params); + const response = await this.doPost(`/savedata/session/clear?${urlSearchParams}`, sessionData); + + return (await response.json()) as ClearSessionSavedataResponse; + } catch (err) { + console.warn("Could not clear session savedata!", err); + } + + return { + error: "Unknown error", + success: false, + } as ClearSessionSavedataResponse; + } +} diff --git a/src/plugins/api/pokerogue-system-savedata-api.ts b/src/plugins/api/pokerogue-system-savedata-api.ts new file mode 100644 index 00000000000..659584776c4 --- /dev/null +++ b/src/plugins/api/pokerogue-system-savedata-api.ts @@ -0,0 +1,77 @@ +import type { + GetSystemSavedataRequest, + UpdateSystemSavedataRequest, + VerifySystemSavedataRequest, + VerifySystemSavedataResponse, +} from "#app/@types/PokerogueSystemSavedataApi"; +import { ApiBase } from "#app/plugins/api/api-base"; + +/** + * A wrapper for PokéRogue system savedata API requests. + */ +export class PokerogueSystemSavedataApi extends ApiBase { + //#region Public + + /** + * Get a system savedata. + * @param params The {@linkcode GetSystemSavedataRequest} to send + * @returns The system savedata as `string` or `null` on error + */ + public async get(params: GetSystemSavedataRequest) { + try { + const urlSearchParams = this.toUrlSearchParams(params); + const response = await this.doGet(`/savedata/system/get?${urlSearchParams}`); + const rawSavedata = await response.text(); + + return rawSavedata; + } catch (err) { + console.warn("Could not get system savedata!", err); + return null; + } + } + + /** + * Verify if the session is valid. + * If not the {@linkcode SystemSaveData} is returned. + * @param params The {@linkcode VerifySystemSavedataRequest} to send + * @returns A {@linkcode SystemSaveData} if **NOT** valid, otherwise `null`. + * + * TODO: add handling for errors + */ + public async verify(params: VerifySystemSavedataRequest) { + const urlSearchParams = this.toUrlSearchParams(params); + const response = await this.doGet(`/savedata/system/verify?${urlSearchParams}`); + + if (response.ok) { + const verifySavedata = (await response.json()) as VerifySystemSavedataResponse; + + if (!verifySavedata.valid) { + console.warn("Invalid system savedata!"); + return verifySavedata.systemData; + } + } else { + console.warn("System savedata verification failed!", response.status, response.statusText); + } + + return null; + } + + /** + * Update a system savedata. + * @param params The {@linkcode UpdateSystemSavedataRequest} to send + * @param rawSystemData The raw {@linkcode SystemSaveData} + * @returns An error message if something went wrong + */ + public async update(params: UpdateSystemSavedataRequest, rawSystemData: string) { + try { + const urSearchParams = this.toUrlSearchParams(params); + const response = await this.doPost(`/savedata/system/update?${urSearchParams}`, rawSystemData); + + return await response.text(); + } catch (err) { + console.warn("Could not update system savedata!", err); + } + + return "Unknown Error"; + } +} diff --git a/src/system/game-data.ts b/src/system/game-data.ts index e252e03afaf..4b7eeedfe77 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -48,7 +48,7 @@ import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler"; import { applySessionVersionMigration, applySystemVersionMigration, applySettingsVersionMigration } from "./version_migration/version_converter"; import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { PokerogueApiClearSessionData } from "#app/@types/pokerogue-api"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import { ArenaTrapTag } from "#app/data/arena-tag"; export const defaultStarterSpecies: Species[] = [ @@ -397,8 +397,7 @@ export class GameData { localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemData, bypassLogin)); if (!bypassLogin) { - Utils.apiPost(`savedata/system/update?clientSessionId=${clientSessionId}`, systemData, undefined, true) - .then(response => response.text()) + pokerogueApi.savedata.system.update({ clientSessionId }, systemData) .then(error => { this.scene.ui.savingIcon.hide(); if (error) { @@ -428,23 +427,22 @@ export class GameData { } if (!bypassLogin) { - Utils.apiFetch(`savedata/system/get?clientSessionId=${clientSessionId}`, true) - .then(response => response.text()) - .then(response => { - if (!response.length || response[0] !== "{") { - if (response.startsWith("sql: no rows in result set")) { + pokerogueApi.savedata.system.get({ clientSessionId }) + .then(saveDataOrErr => { + if (!saveDataOrErr || saveDataOrErr.length === 0 || saveDataOrErr[0] !== "{") { + if (saveDataOrErr?.startsWith("sql: no rows in result set")) { this.scene.queueMessage("Save data could not be found. If this is a new account, you can safely ignore this message.", null, true); return resolve(true); - } else if (response.indexOf("Too many connections") > -1) { + } else if (saveDataOrErr?.includes("Too many connections")) { this.scene.queueMessage("Too many people are trying to connect and the server is overloaded. Please try again later.", null, true); return resolve(false); } - console.error(response); + console.error(saveDataOrErr); return resolve(false); } const cachedSystem = localStorage.getItem(`data_${loggedInUser?.username}`); - this.initSystem(response, cachedSystem ? AES.decrypt(cachedSystem, saveKey).toString(enc.Utf8) : undefined).then(resolve); + this.initSystem(saveDataOrErr, cachedSystem ? AES.decrypt(cachedSystem, saveKey).toString(enc.Utf8) : undefined).then(resolve); }); } else { this.initSystem(decrypt(localStorage.getItem(`data_${loggedInUser?.username}`)!, bypassLogin)).then(resolve); // TODO: is this bang correct? @@ -580,6 +578,7 @@ export class GameData { if (!Utils.isLocal) { /** * Networking Code DO NOT DELETE! + * Note: Might have to be migrated to `pokerogue-api.ts` * const response = await Utils.apiFetch("savedata/runHistory", true); const data = await response.json(); @@ -660,6 +659,7 @@ export class GameData { return false; } } + NOTE: should be adopted to `pokerogue-api.ts` */ return true; } @@ -704,12 +704,11 @@ export class GameData { return true; } - const response = await Utils.apiFetch(`savedata/system/verify?clientSessionId=${clientSessionId}`, true) - .then(response => response.json()); + const systemData = await pokerogueApi.savedata.system.verify({ clientSessionId }); - if (!response.valid) { + if (systemData) { this.scene.clearPhaseQueue(); - this.scene.unshiftPhase(new ReloadSessionPhase(this.scene, JSON.stringify(response.systemData))); + this.scene.unshiftPhase(new ReloadSessionPhase(this.scene, JSON.stringify(systemData))); this.clearLocalData(); return false; } @@ -984,10 +983,9 @@ export class GameData { }; if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) { - Utils.apiFetch(`savedata/session/get?slot=${slotId}&clientSessionId=${clientSessionId}`, true) - .then(response => response.text()) + pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }) .then(async response => { - if (!response.length || response[0] !== "{") { + if (!response || response?.length === 0 || response?.[0] !== "{") { console.error(response); return resolve(null); } @@ -1149,14 +1147,7 @@ export class GameData { if (success !== null && !success) { return resolve(false); } - Utils.apiFetch(`savedata/session/delete?slot=${slotId}&clientSessionId=${clientSessionId}`, true).then(response => { - if (response.ok) { - loggedInUser!.lastSessionSlot = -1; // TODO: is the bang correct? - localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); - resolve(true); - } - return response.text(); - }).then(error => { + pokerogueApi.savedata.session.delete({ slot: slotId, clientSessionId }).then(error => { if (error) { if (error.startsWith("session out of date")) { this.scene.clearPhaseQueue(); @@ -1164,8 +1155,15 @@ export class GameData { } console.error(error); resolve(false); + } else { + if (loggedInUser) { + loggedInUser.lastSessionSlot = -1; + } + + localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); + resolve(true); + } - resolve(true); }); }); }); @@ -1215,17 +1213,15 @@ export class GameData { result = [ true, true ]; } else { const sessionData = this.getSessionSaveData(scene); - const response = await Utils.apiPost(`savedata/session/clear?slot=${slotId}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`, JSON.stringify(sessionData), undefined, true); + const { trainerId } = this; + const jsonResponse = await pokerogueApi.savedata.session.clear({ slot: slotId, trainerId, clientSessionId }, sessionData); - if (response.ok) { - loggedInUser!.lastSessionSlot = -1; // TODO: is the bang correct? - localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); - } - - const jsonResponse: PokerogueApiClearSessionData = await response.json(); - - if (!jsonResponse.error) { - result = [ true, jsonResponse.success ?? false ]; + if (!jsonResponse?.error) { + result = [ true, jsonResponse?.success ?? false ]; + if (loggedInUser) { + loggedInUser!.lastSessionSlot = -1; + } + localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); } else { if (jsonResponse && jsonResponse.error?.startsWith("session out of date")) { this.scene.clearPhaseQueue(); @@ -1342,8 +1338,7 @@ export class GameData { console.debug("Session data saved"); if (!bypassLogin && sync) { - Utils.apiPost("savedata/updateall", JSON.stringify(request, (k: any, v: any) => typeof v === "bigint" ? v <= maxIntAttrValue ? Number(v) : v.toString() : v), undefined, true) - .then(response => response.text()) + pokerogueApi.savedata.updateAll(request) .then(error => { if (sync) { this.scene.lastSavePlayTime = 0; @@ -1387,18 +1382,24 @@ export class GameData { link.remove(); }; if (!bypassLogin && dataType < GameDataType.SETTINGS) { - Utils.apiFetch(`savedata/${dataType === GameDataType.SYSTEM ? "system" : "session"}/get?clientSessionId=${clientSessionId}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ""}`, true) - .then(response => response.text()) - .then(response => { - if (!response.length || response[0] !== "{") { - console.error(response); - resolve(false); - return; - } + let promise: Promise = Promise.resolve(null); - handleData(response); - resolve(true); - }); + if (dataType === GameDataType.SYSTEM) { + promise = pokerogueApi.savedata.system.get({ clientSessionId }); + } else if (dataType === GameDataType.SESSION) { + promise = pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }); + } + + promise.then(response => { + if (!response?.length || response[0] !== "{") { + console.error(response); + resolve(false); + return; + } + + handleData(response); + resolve(true); + }); } else { const data = localStorage.getItem(dataKey); if (data) { @@ -1477,14 +1478,14 @@ export class GameData { if (!success[0]) { return displayError(`Could not contact the server. Your ${dataName} data could not be imported.`); } - let url: string; + const { trainerId, secretId } = this; + let updatePromise: Promise; if (dataType === GameDataType.SESSION) { - url = `savedata/session/update?slot=${slotId}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`; + updatePromise = pokerogueApi.savedata.session.update({ slot: slotId, trainerId, secretId, clientSessionId }, dataStr); } else { - url = `savedata/system/update?trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`; + updatePromise = pokerogueApi.savedata.system.update({ trainerId, secretId, clientSessionId }, dataStr); } - Utils.apiPost(url, dataStr, undefined, true) - .then(response => response.text()) + updatePromise .then(error => { if (error) { console.error(error); diff --git a/src/test/account.test.ts b/src/test/account.test.ts index 8c36f2cd953..0f49014c377 100644 --- a/src/test/account.test.ts +++ b/src/test/account.test.ts @@ -1,7 +1,7 @@ import * as battleScene from "#app/battle-scene"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import { describe, expect, it, vi } from "vitest"; import { initLoggedInUser, loggedInUser, updateUserInfo } from "../account"; -import * as utils from "../utils"; describe("account", () => { describe("initLoggedInUser", () => { @@ -27,17 +27,16 @@ describe("account", () => { it("should fetch user info from the API if bypassLogin is false", async () => { vi.spyOn(battleScene, "bypassLogin", "get").mockReturnValue(false); - vi.spyOn(utils, "apiFetch").mockResolvedValue( - new Response( - JSON.stringify({ - username: "test", - lastSessionSlot: 99, - }), - { - status: 200, - } - ) - ); + vi.spyOn(pokerogueApi.account, "getInfo").mockResolvedValue([ + { + username: "test", + lastSessionSlot: 99, + discordId: "", + googleId: "", + hasAdminRole: false, + }, + 200, + ]); const [ success, status ] = await updateUserInfo(); @@ -49,9 +48,7 @@ describe("account", () => { it("should handle resolved API errors", async () => { vi.spyOn(battleScene, "bypassLogin", "get").mockReturnValue(false); - vi.spyOn(utils, "apiFetch").mockResolvedValue( - new Response(null, { status: 401 }) - ); + vi.spyOn(pokerogueApi.account, "getInfo").mockResolvedValue([ null, 401 ]); const [ success, status ] = await updateUserInfo(); @@ -59,16 +56,14 @@ describe("account", () => { expect(status).toBe(401); }); - it("should handle rejected API errors", async () => { - const consoleErrorSpy = vi.spyOn(console, "error"); + it("should handle 500 API errors", async () => { vi.spyOn(battleScene, "bypassLogin", "get").mockReturnValue(false); - vi.spyOn(utils, "apiFetch").mockRejectedValue(new Error("Api failed!")); + vi.spyOn(pokerogueApi.account, "getInfo").mockResolvedValue([ null, 500 ]); const [ success, status ] = await updateUserInfo(); expect(success).toBe(false); expect(status).toBe(500); - expect(consoleErrorSpy).toHaveBeenCalled(); }); }); }); diff --git a/src/test/daily_mode.test.ts b/src/test/daily_mode.test.ts index 2a88ce10ae7..3e70cc2d8a7 100644 --- a/src/test/daily_mode.test.ts +++ b/src/test/daily_mode.test.ts @@ -1,11 +1,12 @@ -import { MapModifier } from "#app/modifier/modifier"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import GameManager from "./utils/gameManager"; -import { Moves } from "#app/enums/moves"; import { Biome } from "#app/enums/biome"; -import { Mode } from "#app/ui/ui"; +import { Moves } from "#app/enums/moves"; +import { MapModifier } from "#app/modifier/modifier"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import { Species } from "#enums/species"; +import { Mode } from "#app/ui/ui"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import GameManager from "#app/test/utils/gameManager"; //const TIMEOUT = 20 * 1000; @@ -21,6 +22,7 @@ describe("Daily Mode", () => { beforeEach(() => { game = new GameManager(phaserGame); + vi.spyOn(pokerogueApi.daily, "getSeed").mockResolvedValue("test-seed"); }); afterEach(() => { @@ -32,7 +34,7 @@ describe("Daily Mode", () => { const party = game.scene.getPlayerParty(); expect(party).toHaveLength(3); - party.forEach(pkm => { + party.forEach((pkm) => { expect(pkm.level).toBe(20); expect(pkm.moveset.length).toBeGreaterThan(0); }); @@ -63,6 +65,7 @@ describe("Shop modifications", async () => { game.modifiers .addCheck("EVIOLITE") .addCheck("MINI_BLACK_HOLE"); + vi.spyOn(pokerogueApi.daily, "getSeed").mockResolvedValue("test-seed"); }); afterEach(() => { diff --git a/src/test/misc.test.ts b/src/test/misc.test.ts index 3335c4c5523..ae91a5014d9 100644 --- a/src/test/misc.test.ts +++ b/src/test/misc.test.ts @@ -1,4 +1,4 @@ -import { apiFetch } from "#app/utils"; +// import { apiFetch } from "#app/utils"; import GameManager from "#test/utils/gameManager"; import { waitUntil } from "#test/utils/gameManagerUtils"; import Phaser from "phaser"; @@ -35,18 +35,18 @@ describe("Test misc", () => { expect(spy).toHaveBeenCalled(); }); - it("test apifetch mock async", async () => { - const spy = vi.fn(); - await apiFetch("https://localhost:8080/account/info").then(response => { - expect(response.status).toBe(200); - expect(response.ok).toBe(true); - return response.json(); - }).then(data => { - spy(); // Call the spy function - expect(data).toEqual({ "username": "greenlamp", "lastSessionSlot": 0 }); - }); - expect(spy).toHaveBeenCalled(); - }); + // it.skip("test apifetch mock async", async () => { + // const spy = vi.fn(); + // await apiFetch("https://localhost:8080/account/info").then(response => { + // expect(response.status).toBe(200); + // expect(response.ok).toBe(true); + // return response.json(); + // }).then(data => { + // spy(); // Call the spy function + // expect(data).toEqual({ "username": "greenlamp", "lastSessionSlot": 0 }); + // }); + // expect(spy).toHaveBeenCalled(); + // }); it("test fetch mock sync", async () => { const response = await fetch("https://localhost:8080/account/info"); diff --git a/src/test/plugins/api/pokerogue-account-api.test.ts b/src/test/plugins/api/pokerogue-account-api.test.ts new file mode 100644 index 00000000000..90a7d3639ad --- /dev/null +++ b/src/test/plugins/api/pokerogue-account-api.test.ts @@ -0,0 +1,157 @@ +import type { AccountInfoResponse } from "#app/@types/PokerogueAccountApi"; +import { SESSION_ID_COOKIE_NAME } from "#app/constants"; +import { PokerogueAccountApi } from "#app/plugins/api/pokerogue-account-api"; +import { getApiBaseUrl } from "#app/test/utils/testUtils"; +import * as Utils from "#app/utils"; +import { http, HttpResponse } from "msw"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const apiBase = getApiBaseUrl(); +const accountApi = new PokerogueAccountApi(apiBase); +const { server } = global; + +afterEach(() => { + server.resetHandlers(); +}); + +describe("Pokerogue Account API", () => { + beforeEach(() => { + vi.spyOn(console, "warn"); + }); + + describe("Get Info", () => { + it("should return account-info & 200 on SUCCESS", async () => { + const expectedAccountInfo: AccountInfoResponse = { + username: "test", + lastSessionSlot: -1, + discordId: "23235353543535", + googleId: "1ed1d1d11d1d1d1d1d1", + hasAdminRole: false, + }; + server.use(http.get(`${apiBase}/account/info`, () => HttpResponse.json(expectedAccountInfo))); + + const [ accountInfo, status ] = await accountApi.getInfo(); + + expect(accountInfo).toEqual(expectedAccountInfo); + expect(status).toBe(200); + }); + + it("should return null + status-code anad report a warning on FAILURE", async () => { + server.use(http.get(`${apiBase}/account/info`, () => new HttpResponse("", { status: 401 }))); + + const [ accountInfo, status ] = await accountApi.getInfo(); + + expect(accountInfo).toBeNull(); + expect(status).toBe(401); + expect(console.warn).toHaveBeenCalledWith("Could not get account info!", 401, "Unauthorized"); + }); + + it("should return null + 500 anad report a warning on ERROR", async () => { + server.use(http.get(`${apiBase}/account/info`, () => HttpResponse.error())); + + const [ accountInfo, status ] = await accountApi.getInfo(); + + expect(accountInfo).toBeNull(); + expect(status).toBe(500); + expect(console.warn).toHaveBeenCalledWith("Could not get account info!", expect.any(Error)); + }); + }); + + describe("Register", () => { + const registerParams = { username: "test", password: "test" }; + + it("should return null on SUCCESS", async () => { + server.use(http.post(`${apiBase}/account/register`, () => HttpResponse.text())); + + const error = await accountApi.register(registerParams); + + expect(error).toBeNull(); + }); + + it("should return error message on FAILURE", async () => { + server.use( + http.post(`${apiBase}/account/register`, () => new HttpResponse("Username is already taken", { status: 400 })) + ); + + const error = await accountApi.register(registerParams); + + expect(error).toBe("Username is already taken"); + }); + + it("should return \"Unknown error\" and report a warning on ERROR", async () => { + server.use(http.post(`${apiBase}/account/register`, () => HttpResponse.error())); + + const error = await accountApi.register(registerParams); + + expect(error).toBe("Unknown error!"); + expect(console.warn).toHaveBeenCalledWith("Register failed!", expect.any(Error)); + }); + }); + + describe("Login", () => { + const loginParams = { username: "test", password: "test" }; + + it("should return null and set the cookie on SUCCESS", async () => { + vi.spyOn(Utils, "setCookie"); + server.use(http.post(`${apiBase}/account/login`, () => HttpResponse.json({ token: "abctest" }))); + + const error = await accountApi.login(loginParams); + + expect(error).toBeNull(); + expect(Utils.setCookie).toHaveBeenCalledWith(SESSION_ID_COOKIE_NAME, "abctest"); + }); + + it("should return error message and report a warning on FAILURE", async () => { + server.use( + http.post(`${apiBase}/account/login`, () => new HttpResponse("Password is incorrect", { status: 401 })) + ); + + const error = await accountApi.login(loginParams); + + expect(error).toBe("Password is incorrect"); + expect(console.warn).toHaveBeenCalledWith("Login failed!", 401, "Unauthorized"); + }); + + it("should return \"Unknown error\" and report a warning on ERROR", async () => { + server.use(http.post(`${apiBase}/account/login`, () => HttpResponse.error())); + + const error = await accountApi.login(loginParams); + + expect(error).toBe("Unknown error!"); + expect(console.warn).toHaveBeenCalledWith("Login failed!", expect.any(Error)); + }); + }); + + describe("Logout", () => { + beforeEach(() => { + vi.spyOn(Utils, "removeCookie"); + }); + + it("should remove cookie on success", async () => { + vi.spyOn(Utils, "setCookie"); + server.use(http.get(`${apiBase}/account/logout`, () => new HttpResponse("", { status: 200 }))); + + await accountApi.logout(); + + expect(Utils.removeCookie).toHaveBeenCalledWith(SESSION_ID_COOKIE_NAME); + }); + + it("should report a warning on and remove cookie on FAILURE", async () => { + server.use(http.get(`${apiBase}/account/logout`, () => new HttpResponse("", { status: 401 }))); + + await accountApi.logout(); + + expect(Utils.removeCookie).toHaveBeenCalledWith(SESSION_ID_COOKIE_NAME); + expect(console.warn).toHaveBeenCalledWith("Log out failed!", expect.any(Error)); + }); + + it("should report a warning on and remove cookie on ERROR", async () => { + server.use(http.get(`${apiBase}/account/logout`, () => HttpResponse.error())); + + await accountApi.logout(); + + expect(Utils.removeCookie).toHaveBeenCalledWith(SESSION_ID_COOKIE_NAME); + expect(console.warn).toHaveBeenCalledWith("Log out failed!", expect.any(Error)); + }); + }); +}); diff --git a/src/test/plugins/api/pokerogue-admin-api.test.ts b/src/test/plugins/api/pokerogue-admin-api.test.ts new file mode 100644 index 00000000000..5ae46abfcc8 --- /dev/null +++ b/src/test/plugins/api/pokerogue-admin-api.test.ts @@ -0,0 +1,232 @@ +import type { + LinkAccountToDiscordIdRequest, + LinkAccountToGoogledIdRequest, + SearchAccountRequest, + SearchAccountResponse, + UnlinkAccountFromDiscordIdRequest, + UnlinkAccountFromGoogledIdRequest, +} from "#app/@types/PokerogueAdminApi"; +import { PokerogueAdminApi } from "#app/plugins/api/pokerogue-admin-api"; +import { getApiBaseUrl } from "#app/test/utils/testUtils"; +import { http, HttpResponse } from "msw"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const apiBase = getApiBaseUrl(); +const adminApi = new PokerogueAdminApi(apiBase); +const { server } = global; + +afterEach(() => { + server.resetHandlers(); +}); + +describe("Pokerogue Admin API", () => { + beforeEach(() => { + vi.spyOn(console, "warn"); + }); + + describe("Link Account to Discord", () => { + const params: LinkAccountToDiscordIdRequest = { username: "test", discordId: "test-12575756" }; + + it("should return null on SUCCESS", async () => { + server.use(http.post(`${apiBase}/admin/account/discordLink`, () => HttpResponse.json(true))); + + const success = await adminApi.linkAccountToDiscord(params); + + expect(success).toBeNull(); + }); + + it("should return a ERR_GENERIC and report a warning on FAILURE", async () => { + server.use(http.post(`${apiBase}/admin/account/discordLink`, () => new HttpResponse("", { status: 400 }))); + + const success = await adminApi.linkAccountToDiscord(params); + + expect(success).toBe(adminApi.ERR_GENERIC); + expect(console.warn).toHaveBeenCalledWith("Could not link account with discord!", 400, "Bad Request"); + }); + + it("should return a ERR_USERNAME_NOT_FOUND and report a warning on 404", async () => { + server.use(http.post(`${apiBase}/admin/account/discordLink`, () => new HttpResponse("", { status: 404 }))); + + const success = await adminApi.linkAccountToDiscord(params); + + expect(success).toBe(adminApi.ERR_USERNAME_NOT_FOUND); + expect(console.warn).toHaveBeenCalledWith("Could not link account with discord!", 404, "Not Found"); + }); + + it("should return a ERR_GENERIC and report a warning on ERROR", async () => { + server.use(http.post(`${apiBase}/admin/account/discordLink`, () => HttpResponse.error())); + + const success = await adminApi.linkAccountToDiscord(params); + + expect(success).toBe(adminApi.ERR_GENERIC); + expect(console.warn).toHaveBeenCalledWith("Could not link account with discord!", expect.any(Error)); + }); + }); + + describe("Unlink Account from Discord", () => { + const params: UnlinkAccountFromDiscordIdRequest = { username: "test", discordId: "test-12575756" }; + + it("should return null on SUCCESS", async () => { + server.use(http.post(`${apiBase}/admin/account/discordUnlink`, () => HttpResponse.json(true))); + + const success = await adminApi.unlinkAccountFromDiscord(params); + + expect(success).toBeNull(); + }); + + it("should return a ERR_GENERIC and report a warning on FAILURE", async () => { + server.use(http.post(`${apiBase}/admin/account/discordUnlink`, () => new HttpResponse("", { status: 400 }))); + + const success = await adminApi.unlinkAccountFromDiscord(params); + + expect(success).toBe(adminApi.ERR_GENERIC); + expect(console.warn).toHaveBeenCalledWith("Could not unlink account from discord!", 400, "Bad Request"); + }); + + it("should return a ERR_USERNAME_NOT_FOUND and report a warning on 404", async () => { + server.use(http.post(`${apiBase}/admin/account/discordUnlink`, () => new HttpResponse("", { status: 404 }))); + + const success = await adminApi.unlinkAccountFromDiscord(params); + + expect(success).toBe(adminApi.ERR_USERNAME_NOT_FOUND); + expect(console.warn).toHaveBeenCalledWith("Could not unlink account from discord!", 404, "Not Found"); + }); + + it("should return a ERR_GENERIC and report a warning on ERROR", async () => { + server.use(http.post(`${apiBase}/admin/account/discordUnlink`, () => HttpResponse.error())); + + const success = await adminApi.unlinkAccountFromDiscord(params); + + expect(success).toBe(adminApi.ERR_GENERIC); + expect(console.warn).toHaveBeenCalledWith("Could not unlink account from discord!", expect.any(Error)); + }); + }); + + describe("Link Account to Google", () => { + const params: LinkAccountToGoogledIdRequest = { username: "test", googleId: "test-12575756" }; + + it("should return null on SUCCESS", async () => { + server.use(http.post(`${apiBase}/admin/account/googleLink`, () => HttpResponse.json(true))); + + const success = await adminApi.linkAccountToGoogleId(params); + + expect(success).toBeNull(); + }); + + it("should return a ERR_GENERIC and report a warning on FAILURE", async () => { + server.use(http.post(`${apiBase}/admin/account/googleLink`, () => new HttpResponse("", { status: 400 }))); + + const success = await adminApi.linkAccountToGoogleId(params); + + expect(success).toBe(adminApi.ERR_GENERIC); + expect(console.warn).toHaveBeenCalledWith("Could not link account with google!", 400, "Bad Request"); + }); + + it("should return a ERR_USERNAME_NOT_FOUND and report a warning on 404", async () => { + server.use(http.post(`${apiBase}/admin/account/googleLink`, () => new HttpResponse("", { status: 404 }))); + + const success = await adminApi.linkAccountToGoogleId(params); + + expect(success).toBe(adminApi.ERR_USERNAME_NOT_FOUND); + expect(console.warn).toHaveBeenCalledWith("Could not link account with google!", 404, "Not Found"); + }); + + it("should return a ERR_GENERIC and report a warning on ERROR", async () => { + server.use(http.post(`${apiBase}/admin/account/googleLink`, () => HttpResponse.error())); + + const success = await adminApi.linkAccountToGoogleId(params); + + expect(success).toBe(adminApi.ERR_GENERIC); + expect(console.warn).toHaveBeenCalledWith("Could not link account with google!", expect.any(Error)); + }); + }); + + describe("Unlink Account from Google", () => { + const params: UnlinkAccountFromGoogledIdRequest = { username: "test", googleId: "test-12575756" }; + + it("should return null on SUCCESS", async () => { + server.use(http.post(`${apiBase}/admin/account/googleUnlink`, () => HttpResponse.json(true))); + + const success = await adminApi.unlinkAccountFromGoogleId(params); + + expect(success).toBeNull(); + }); + + it("should return a ERR_GENERIC and report a warning on FAILURE", async () => { + server.use(http.post(`${apiBase}/admin/account/googleUnlink`, () => new HttpResponse("", { status: 400 }))); + + const success = await adminApi.unlinkAccountFromGoogleId(params); + + expect(success).toBe(adminApi.ERR_GENERIC); + expect(console.warn).toHaveBeenCalledWith("Could not unlink account from google!", 400, "Bad Request"); + }); + + it("should return a ERR_USERNAME_NOT_FOUND and report a warning on 404", async () => { + server.use(http.post(`${apiBase}/admin/account/googleUnlink`, () => new HttpResponse("", { status: 404 }))); + + const success = await adminApi.unlinkAccountFromGoogleId(params); + + expect(success).toBe(adminApi.ERR_USERNAME_NOT_FOUND); + expect(console.warn).toHaveBeenCalledWith("Could not unlink account from google!", 404, "Not Found"); + }); + + it("should return a ERR_GENERIC and report a warning on ERROR", async () => { + server.use(http.post(`${apiBase}/admin/account/googleUnlink`, () => HttpResponse.error())); + + const success = await adminApi.unlinkAccountFromGoogleId(params); + + expect(success).toBe(adminApi.ERR_GENERIC); + expect(console.warn).toHaveBeenCalledWith("Could not unlink account from google!", expect.any(Error)); + }); + }); + + describe("Search Account", () => { + const params: SearchAccountRequest = { username: "test" }; + + it("should return [data, undefined] on SUCCESS", async () => { + const responseData: SearchAccountResponse = { + username: "test", + discordId: "discord-test-123", + googleId: "google-test-123", + lastLoggedIn: "2022-01-01", + registered: "2022-01-01", + }; + server.use(http.get(`${apiBase}/admin/account/adminSearch`, () => HttpResponse.json(responseData))); + + const [ data, err ] = await adminApi.searchAccount(params); + + expect(data).toStrictEqual(responseData); + expect(err).toBeUndefined(); + }); + + it("should return [undefined, ERR_GENERIC] and report a warning on on FAILURE", async () => { + server.use(http.get(`${apiBase}/admin/account/adminSearch`, () => new HttpResponse("", { status: 400 }))); + + const [ data, err ] = await adminApi.searchAccount(params); + + expect(data).toBeUndefined(); + expect(err).toBe(adminApi.ERR_GENERIC); + expect(console.warn).toHaveBeenCalledWith("Could not find account!", 400, "Bad Request"); + }); + + it("should return [undefined, ERR_USERNAME_NOT_FOUND] and report a warning on on 404", async () => { + server.use(http.get(`${apiBase}/admin/account/adminSearch`, () => new HttpResponse("", { status: 404 }))); + + const [ data, err ] = await adminApi.searchAccount(params); + + expect(data).toBeUndefined(); + expect(err).toBe(adminApi.ERR_USERNAME_NOT_FOUND); + expect(console.warn).toHaveBeenCalledWith("Could not find account!", 404, "Not Found"); + }); + + it("should return [undefined, ERR_GENERIC] and report a warning on on ERROR", async () => { + server.use(http.get(`${apiBase}/admin/account/adminSearch`, () => HttpResponse.error())); + + const [ data, err ] = await adminApi.searchAccount(params); + + expect(data).toBeUndefined(); + expect(err).toBe(adminApi.ERR_GENERIC); + expect(console.warn).toHaveBeenCalledWith("Could not find account!", expect.any(Error)); + }); + }); +}); diff --git a/src/test/plugins/api/pokerogue-api.test.ts b/src/test/plugins/api/pokerogue-api.test.ts new file mode 100644 index 00000000000..a62174c226d --- /dev/null +++ b/src/test/plugins/api/pokerogue-api.test.ts @@ -0,0 +1,97 @@ +import type { TitleStatsResponse } from "#app/@types/PokerogueApi"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; +import { getApiBaseUrl } from "#app/test/utils/testUtils"; +import { http, HttpResponse } from "msw"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const apiBase = getApiBaseUrl(); +const { server } = global; + +afterEach(() => { + server.resetHandlers(); +}); + +describe("Pokerogue API", () => { + beforeEach(() => { + vi.spyOn(console, "warn"); + }); + + describe("Game Title Stats", () => { + const expectedTitleStats: TitleStatsResponse = { + playerCount: 9999999, + battleCount: 9999999, + }; + + it("should return the stats on SUCCESS", async () => { + server.use(http.get(`${apiBase}/game/titlestats`, () => HttpResponse.json(expectedTitleStats))); + + const titleStats = await pokerogueApi.getGameTitleStats(); + + expect(titleStats).toEqual(expectedTitleStats); + }); + + it("should return null and report a warning on ERROR", async () => { + server.use(http.get(`${apiBase}/game/titlestats`, () => HttpResponse.error())); + const titleStats = await pokerogueApi.getGameTitleStats(); + + expect(titleStats).toBeNull(); + expect(console.warn).toHaveBeenCalledWith("Could not get game title stats!", expect.any(Error)); + }); + }); + + describe("Unlink Discord", () => { + it("should return true on SUCCESS", async () => { + server.use(http.post(`${apiBase}/auth/discord/logout`, () => new HttpResponse("", { status: 200 }))); + + const success = await pokerogueApi.unlinkDiscord(); + + expect(success).toBe(true); + }); + + it("should return false and report a warning on FAILURE", async () => { + server.use(http.post(`${apiBase}/auth/discord/logout`, () => new HttpResponse("", { status: 401 }))); + + const success = await pokerogueApi.unlinkDiscord(); + + expect(success).toBe(false); + expect(console.warn).toHaveBeenCalledWith("Discord unlink failed (401: Unauthorized)"); + }); + + it("should return false and report a warning on ERROR", async () => { + server.use(http.post(`${apiBase}/auth/discord/logout`, () => HttpResponse.error())); + + const success = await pokerogueApi.unlinkDiscord(); + + expect(success).toBe(false); + expect(console.warn).toHaveBeenCalledWith("Could not unlink Discord!", expect.any(Error)); + }); + }); + + describe("Unlink Google", () => { + it("should return true on SUCCESS", async () => { + server.use(http.post(`${apiBase}/auth/google/logout`, () => new HttpResponse("", { status: 200 }))); + + const success = await pokerogueApi.unlinkGoogle(); + + expect(success).toBe(true); + }); + + it("should return false and report a warning on FAILURE", async () => { + server.use(http.post(`${apiBase}/auth/google/logout`, () => new HttpResponse("", { status: 401 }))); + + const success = await pokerogueApi.unlinkGoogle(); + + expect(success).toBe(false); + expect(console.warn).toHaveBeenCalledWith("Google unlink failed (401: Unauthorized)"); + }); + + it("should return false and report a warning on ERROR", async () => { + server.use(http.post(`${apiBase}/auth/google/logout`, () => HttpResponse.error())); + + const success = await pokerogueApi.unlinkGoogle(); + + expect(success).toBe(false); + expect(console.warn).toHaveBeenCalledWith("Could not unlink Google!", expect.any(Error)); + }); + }); +}); diff --git a/src/test/plugins/api/pokerogue-daily-api.test.ts b/src/test/plugins/api/pokerogue-daily-api.test.ts new file mode 100644 index 00000000000..569e7cbb15d --- /dev/null +++ b/src/test/plugins/api/pokerogue-daily-api.test.ts @@ -0,0 +1,89 @@ +import type { GetDailyRankingsPageCountRequest, GetDailyRankingsRequest } from "#app/@types/PokerogueDailyApi"; +import { PokerogueDailyApi } from "#app/plugins/api/pokerogue-daily-api"; +import { getApiBaseUrl } from "#app/test/utils/testUtils"; +import { ScoreboardCategory, type RankingEntry } from "#app/ui/daily-run-scoreboard"; +import { http, HttpResponse } from "msw"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const apiBase = getApiBaseUrl(); +const dailyApi = new PokerogueDailyApi(apiBase); +const { server } = global; + +afterEach(() => { + server.resetHandlers(); +}); + +describe("Pokerogue Daily API", () => { + beforeEach(() => { + vi.spyOn(console, "warn"); + }); + + describe("Get Seed", () => { + it("should return seed string on SUCCESS", async () => { + server.use(http.get(`${apiBase}/daily/seed`, () => HttpResponse.text("this-is-a-test-seed"))); + + const seed = await dailyApi.getSeed(); + + expect(seed).toBe("this-is-a-test-seed"); + }); + + it("should return null and report a warning on ERROR", async () => { + server.use(http.get(`${apiBase}/daily/seed`, () => HttpResponse.error())); + + const seed = await dailyApi.getSeed(); + + expect(seed).toBeNull(); + expect(console.warn).toHaveBeenCalledWith("Could not get daily-run seed!", expect.any(Error)); + }); + }); + + describe("Get Rankings", () => { + const params: GetDailyRankingsRequest = { + category: ScoreboardCategory.DAILY, + }; + + it("should return ranking entries on SUCCESS", async () => { + const expectedRankings: RankingEntry[] = [ + { rank: 1, score: 999, username: "Player 1", wave: 200 }, + { rank: 2, score: 10, username: "Player 2", wave: 1 }, + ]; + server.use(http.get(`${apiBase}/daily/rankings`, () => HttpResponse.json(expectedRankings))); + + const rankings = await dailyApi.getRankings(params); + + expect(rankings).toStrictEqual(expectedRankings); + }); + + it("should return null and report a warning on ERROR", async () => { + server.use(http.get(`${apiBase}/daily/rankings`, () => HttpResponse.error())); + + const rankings = await dailyApi.getRankings(params); + + expect(rankings).toBeNull(); + expect(console.warn).toHaveBeenCalledWith("Could not get daily rankings!", expect.any(Error)); + }); + }); + + describe("Get Rankings Page Count", () => { + const params: GetDailyRankingsPageCountRequest = { + category: ScoreboardCategory.DAILY, + }; + + it("should return a number on SUCCESS", async () => { + server.use(http.get(`${apiBase}/daily/rankingpagecount`, () => HttpResponse.json(5))); + + const pageCount = await dailyApi.getRankingsPageCount(params); + + expect(pageCount).toBe(5); + }); + + it("should return 1 and report a warning on ERROR", async () => { + server.use(http.get(`${apiBase}/daily/rankingpagecount`, () => HttpResponse.error())); + + const pageCount = await dailyApi.getRankingsPageCount(params); + + expect(pageCount).toBe(1); + expect(console.warn).toHaveBeenCalledWith("Could not get daily rankings page count!", expect.any(Error)); + }); + }); +}); diff --git a/src/test/plugins/api/pokerogue-savedata-api.test.ts b/src/test/plugins/api/pokerogue-savedata-api.test.ts new file mode 100644 index 00000000000..6dd402206e5 --- /dev/null +++ b/src/test/plugins/api/pokerogue-savedata-api.test.ts @@ -0,0 +1,46 @@ +import type { UpdateAllSavedataRequest } from "#app/@types/PokerogueSavedataApi"; +import { PokerogueSavedataApi } from "#app/plugins/api/pokerogue-savedata-api"; +import { getApiBaseUrl } from "#app/test/utils/testUtils"; +import { http, HttpResponse } from "msw"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const apiBase = getApiBaseUrl(); +const savedataApi = new PokerogueSavedataApi(apiBase); +const { server } = global; + +afterEach(() => { + server.resetHandlers(); +}); + +describe("Pokerogue Savedata API", () => { + beforeEach(() => { + vi.spyOn(console, "warn"); + }); + + describe("Update All", () => { + it("should return an empty string on SUCCESS", async () => { + server.use(http.post(`${apiBase}/savedata/updateall`, () => HttpResponse.text(null))); + + const error = await savedataApi.updateAll({} as UpdateAllSavedataRequest); + + expect(error).toBe(""); + }); + + it("should return an error message on FAILURE", async () => { + server.use(http.post(`${apiBase}/savedata/updateall`, () => HttpResponse.text("Failed to update all!"))); + + const error = await savedataApi.updateAll({} as UpdateAllSavedataRequest); + + expect(error).toBe("Failed to update all!"); + }); + + it("should return 'Unknown error' and report a warning on ERROR", async () => { + server.use(http.post(`${apiBase}/savedata/updateall`, () => HttpResponse.error())); + + const error = await savedataApi.updateAll({} as UpdateAllSavedataRequest); + + expect(error).toBe("Unknown error"); + expect(console.warn).toHaveBeenCalledWith("Could not update all savedata!", expect.any(Error)); + }); + }); +}); diff --git a/src/test/plugins/api/pokerogue-session-savedata-api.test.ts b/src/test/plugins/api/pokerogue-session-savedata-api.test.ts new file mode 100644 index 00000000000..d9f6216c4cf --- /dev/null +++ b/src/test/plugins/api/pokerogue-session-savedata-api.test.ts @@ -0,0 +1,199 @@ +import type { + ClearSessionSavedataRequest, + ClearSessionSavedataResponse, + DeleteSessionSavedataRequest, + GetSessionSavedataRequest, + NewClearSessionSavedataRequest, + UpdateSessionSavedataRequest, +} from "#app/@types/PokerogueSessionSavedataApi"; +import { PokerogueSessionSavedataApi } from "#app/plugins/api/pokerogue-session-savedata-api"; +import type { SessionSaveData } from "#app/system/game-data"; +import { getApiBaseUrl } from "#app/test/utils/testUtils"; +import { http, HttpResponse } from "msw"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const apiBase = getApiBaseUrl(); +const sessionSavedataApi = new PokerogueSessionSavedataApi(apiBase); +const { server } = global; + +afterEach(() => { + server.resetHandlers(); +}); + +describe("Pokerogue Session Savedata API", () => { + beforeEach(() => { + vi.spyOn(console, "warn"); + }); + + describe("Newclear", () => { + const params: NewClearSessionSavedataRequest = { + clientSessionId: "test-session-id", + slot: 3, + }; + + it("should return true on SUCCESS", async () => { + server.use(http.get(`${apiBase}/savedata/session/newclear`, () => HttpResponse.json(true))); + + const success = await sessionSavedataApi.newclear(params); + + expect(success).toBe(true); + }); + + it("should return false on FAILURE", async () => { + server.use(http.get(`${apiBase}/savedata/session/newclear`, () => HttpResponse.json(false))); + + const success = await sessionSavedataApi.newclear(params); + + expect(success).toBe(false); + }); + + it("should return false and report a warning on ERROR", async () => { + server.use(http.get(`${apiBase}/savedata/session/newclear`, () => HttpResponse.error())); + + const success = await sessionSavedataApi.newclear(params); + + expect(success).toBe(false); + expect(console.warn).toHaveBeenCalledWith("Could not newclear session!", expect.any(Error)); + }); + }); + + describe("Get ", () => { + const params: GetSessionSavedataRequest = { + clientSessionId: "test-session-id", + slot: 3, + }; + + it("should return session-savedata string on SUCCESS", async () => { + server.use(http.get(`${apiBase}/savedata/session/get`, () => HttpResponse.text("TEST SESSION SAVEDATA"))); + + const savedata = await sessionSavedataApi.get(params); + + expect(savedata).toBe("TEST SESSION SAVEDATA"); + }); + + it("should return null and report a warning on ERROR", async () => { + server.use(http.get(`${apiBase}/savedata/session/get`, () => HttpResponse.error())); + + const savedata = await sessionSavedataApi.get(params); + + expect(savedata).toBeNull(); + expect(console.warn).toHaveBeenCalledWith("Could not get session savedata!", expect.any(Error)); + }); + }); + + describe("Update", () => { + const params: UpdateSessionSavedataRequest = { + clientSessionId: "test-session-id", + slot: 3, + secretId: 9876543321, + trainerId: 123456789, + }; + + it("should return an empty string on SUCCESS", async () => { + server.use(http.post(`${apiBase}/savedata/session/update`, () => HttpResponse.text(null))); + + const error = await sessionSavedataApi.update(params, "UPDATED SESSION SAVEDATA"); + + expect(error).toBe(""); + }); + + it("should return an error string on FAILURE", async () => { + server.use(http.post(`${apiBase}/savedata/session/update`, () => HttpResponse.text("Failed to update!"))); + + const error = await sessionSavedataApi.update(params, "UPDATED SESSION SAVEDATA"); + + expect(error).toBe("Failed to update!"); + }); + + it("should return 'Unknown Error!' and report a warning on ERROR", async () => { + server.use(http.post(`${apiBase}/savedata/session/update`, () => HttpResponse.error())); + + const error = await sessionSavedataApi.update(params, "UPDATED SESSION SAVEDATA"); + + expect(error).toBe("Unknown Error!"); + expect(console.warn).toHaveBeenCalledWith("Could not update session savedata!", expect.any(Error)); + }); + }); + + describe("Delete", () => { + const params: DeleteSessionSavedataRequest = { + clientSessionId: "test-session-id", + slot: 3, + }; + + it("should return null on SUCCESS", async () => { + server.use(http.get(`${apiBase}/savedata/session/delete`, () => HttpResponse.text(null))); + + const error = await sessionSavedataApi.delete(params); + + expect(error).toBeNull(); + }); + + it("should return an error string on FAILURE", async () => { + server.use( + http.get(`${apiBase}/savedata/session/delete`, () => new HttpResponse("Failed to delete!", { status: 400 })) + ); + + const error = await sessionSavedataApi.delete(params); + + expect(error).toBe("Failed to delete!"); + }); + + it("should return 'Unknown error' and report a warning on ERROR", async () => { + server.use(http.get(`${apiBase}/savedata/session/delete`, () => HttpResponse.error())); + + const error = await sessionSavedataApi.delete(params); + + expect(error).toBe("Unknown error"); + expect(console.warn).toHaveBeenCalledWith("Could not delete session savedata!", expect.any(Error)); + }); + }); + + describe("Clear", () => { + const params: ClearSessionSavedataRequest = { + clientSessionId: "test-session-id", + slot: 3, + trainerId: 123456789, + }; + + it("should return sucess=true on SUCCESS", async () => { + server.use( + http.post(`${apiBase}/savedata/session/clear`, () => + HttpResponse.json({ + success: true, + }) + ) + ); + + const { success, error } = await sessionSavedataApi.clear(params, {} as SessionSaveData); + + expect(success).toBe(true); + expect(error).toBeUndefined(); + }); + + it("should return sucess=false & an error string on FAILURE", async () => { + server.use( + http.post(`${apiBase}/savedata/session/clear`, () => + HttpResponse.json({ + success: false, + error: "Failed to clear!", + }) + ) + ); + + const { success, error } = await sessionSavedataApi.clear(params, {} as SessionSaveData); + + expect(error).toBe("Failed to clear!"); + expect(success).toBe(false); + }); + + it("should return success=false & error='Unknown error' and report a warning on ERROR", async () => { + server.use(http.post(`${apiBase}/savedata/session/clear`, () => HttpResponse.error())); + + const { success, error } = await sessionSavedataApi.clear(params, {} as SessionSaveData); + + expect(error).toBe("Unknown error"); + expect(success).toBe(false); + }); + }); +}); diff --git a/src/test/plugins/api/pokerogue-system-savedata-api.test.ts b/src/test/plugins/api/pokerogue-system-savedata-api.test.ts new file mode 100644 index 00000000000..af377762b77 --- /dev/null +++ b/src/test/plugins/api/pokerogue-system-savedata-api.test.ts @@ -0,0 +1,122 @@ +import type { + GetSystemSavedataRequest, + UpdateSystemSavedataRequest, + VerifySystemSavedataRequest, + VerifySystemSavedataResponse, +} from "#app/@types/PokerogueSystemSavedataApi"; +import { PokerogueSystemSavedataApi } from "#app/plugins/api/pokerogue-system-savedata-api"; +import type { SystemSaveData } from "#app/system/game-data"; +import { getApiBaseUrl } from "#app/test/utils/testUtils"; +import { http, HttpResponse } from "msw"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const apiBase = getApiBaseUrl(); +const systemSavedataApi = new PokerogueSystemSavedataApi(getApiBaseUrl()); +const { server } = global; + +afterEach(() => { + server.resetHandlers(); +}); + +describe("Pokerogue System Savedata API", () => { + beforeEach(() => { + vi.spyOn(console, "warn"); + }); + + describe("Get", () => { + const params: GetSystemSavedataRequest = { + clientSessionId: "test-session-id", + }; + + it("should return system-savedata string on SUCCESS", async () => { + server.use(http.get(`${apiBase}/savedata/system/get`, () => HttpResponse.text("TEST SYSTEM SAVEDATA"))); + + const savedata = await systemSavedataApi.get(params); + + expect(savedata).toBe("TEST SYSTEM SAVEDATA"); + }); + + it("should return null and report a warning on ERROR", async () => { + server.use(http.get(`${apiBase}/savedata/system/get`, () => HttpResponse.error())); + + const savedata = await systemSavedataApi.get(params); + + expect(savedata).toBeNull(); + expect(console.warn).toHaveBeenCalledWith("Could not get system savedata!", expect.any(Error)); + }); + }); + + describe("Verify", () => { + const params: VerifySystemSavedataRequest = { + clientSessionId: "test-session-id", + }; + + it("should return null on SUCCESS", async () => { + server.use( + http.get(`${apiBase}/savedata/system/verify`, () => + HttpResponse.json({ + systemData: { + trainerId: 123456789, + } as SystemSaveData, + valid: true, + }) + ) + ); + + const savedata = await systemSavedataApi.verify(params); + + expect(savedata).toBeNull(); + }); + + it("should return system-savedata and report a warning on FAILURE", async () => { + server.use( + http.get(`${apiBase}/savedata/system/verify`, () => + HttpResponse.json({ + systemData: { + trainerId: 123456789, + } as SystemSaveData, + valid: false, + }) + ) + ); + + const savedata = await systemSavedataApi.verify(params); + + expect(savedata?.trainerId).toBe(123456789); + expect(console.warn).toHaveBeenCalledWith("Invalid system savedata!"); + }); + }); + + describe("Update", () => { + const params: UpdateSystemSavedataRequest = { + clientSessionId: "test-session-id", + secretId: 9876543321, + trainerId: 123456789, + }; + + it("should return an empty string on SUCCESS", async () => { + server.use(http.post(`${apiBase}/savedata/system/update`, () => HttpResponse.text(null))); + + const error = await systemSavedataApi.update(params, "UPDATED SYSTEM SAVEDATA"); + + expect(error).toBe(""); + }); + + it("should return an error string on FAILURE", async () => { + server.use(http.post(`${apiBase}/savedata/system/update`, () => HttpResponse.text("Failed to update!"))); + + const error = await systemSavedataApi.update(params, "UPDATED SYSTEM SAVEDATA"); + + expect(error).toBe("Failed to update!"); + }); + + it("should return 'Unknown Error' and report a warning on ERROR", async () => { + server.use(http.post(`${apiBase}/savedata/system/update`, () => HttpResponse.error())); + + const error = await systemSavedataApi.update(params, "UPDATED SYSTEM SAVEDATA"); + + expect(error).toBe("Unknown Error"); + expect(console.warn).toHaveBeenCalledWith("Could not update system savedata!", expect.any(Error)); + }); + }); +}); diff --git a/src/test/reload.test.ts b/src/test/reload.test.ts index b15e9691ed6..3b29cadf8e7 100644 --- a/src/test/reload.test.ts +++ b/src/test/reload.test.ts @@ -1,4 +1,5 @@ import { GameModes } from "#app/game-mode"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; import { Mode } from "#app/ui/ui"; import { Biome } from "#enums/biome"; @@ -7,7 +8,7 @@ import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import { MockClock } from "#test/utils/mocks/mockClock"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Reload", () => { let phaserGame: Phaser.Game; @@ -25,6 +26,8 @@ describe("Reload", () => { beforeEach(() => { game = new GameManager(phaserGame); + vi.spyOn(pokerogueApi, "getGameTitleStats").mockResolvedValue({ battleCount: -1, playerCount: -1 }); + vi.spyOn(pokerogueApi.daily, "getSeed").mockResolvedValue("test-seed"); }); it("should not have RNG inconsistencies in a Classic run", async () => { @@ -110,8 +113,7 @@ describe("Reload", () => { }, 20000); it("should not have RNG inconsistencies at a Daily run double battle", async () => { - game.override - .battleType("double"); + game.override.battleType("double"); await game.dailyMode.startBattle(); const preReloadRngState = Phaser.Math.RND.state(); @@ -124,9 +126,7 @@ describe("Reload", () => { }, 20000); it("should not have RNG inconsistencies at a Daily run Gym Leader fight", async () => { - game.override - .battleType("single") - .startingWave(40); + game.override.battleType("single").startingWave(40); await game.dailyMode.startBattle(); const preReloadRngState = Phaser.Math.RND.state(); @@ -139,9 +139,7 @@ describe("Reload", () => { }, 20000); it("should not have RNG inconsistencies at a Daily run regular trainer fight", async () => { - game.override - .battleType("single") - .startingWave(45); + game.override.battleType("single").startingWave(45); await game.dailyMode.startBattle(); const preReloadRngState = Phaser.Math.RND.state(); @@ -154,9 +152,7 @@ describe("Reload", () => { }, 20000); it("should not have RNG inconsistencies at a Daily run wave 50 Boss fight", async () => { - game.override - .battleType("single") - .startingWave(50); + game.override.battleType("single").startingWave(50); await game.runToFinalBossEncounter([ Species.BULBASAUR ], GameModes.DAILY); const preReloadRngState = Phaser.Math.RND.state(); diff --git a/src/test/system/game_data.test.ts b/src/test/system/game_data.test.ts index fcb7e9067a3..1e349470302 100644 --- a/src/test/system/game_data.test.ts +++ b/src/test/system/game_data.test.ts @@ -1,36 +1,23 @@ import * as BattleScene from "#app/battle-scene"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import { SessionSaveData } from "#app/system/game-data"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import GameManager from "#test/utils/gameManager"; -import { http, HttpResponse } from "msw"; -import { setupServer } from "msw/node"; import Phaser from "phaser"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import * as account from "../../account"; -const apiBase = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8001"; - -/** We need a custom server. For some reasons I can't extend the listeners of {@linkcode global.i18nServer} with {@linkcode global.i18nServer.use} */ -const server = setupServer(); - describe("System - Game Data", () => { let phaserGame: Phaser.Game; let game: GameManager; beforeAll(() => { - global.i18nServer.close(); - server.listen(); phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, }); }); - afterAll(() => { - server.close(); - global.i18nServer.listen(); - }); - beforeEach(() => { game = new GameManager(phaserGame); game.override @@ -41,7 +28,6 @@ describe("System - Game Data", () => { }); afterEach(() => { - server.resetHandlers(); game.phaseInterceptor.restoreOg(); }); @@ -61,7 +47,7 @@ describe("System - Game Data", () => { }); it("should return [true, true] if successful", async () => { - server.use(http.post(`${apiBase}/savedata/session/clear`, () => HttpResponse.json({ success: true }))); + vi.spyOn(pokerogueApi.savedata.session, "clear").mockResolvedValue({ success: true }); const result = await game.scene.gameData.tryClearSession(game.scene, 0); @@ -70,7 +56,7 @@ describe("System - Game Data", () => { }); it("should return [true, false] if not successful", async () => { - server.use(http.post(`${apiBase}/savedata/session/clear`, () => HttpResponse.json({ success: false }))); + vi.spyOn(pokerogueApi.savedata.session, "clear").mockResolvedValue({ success: false }); const result = await game.scene.gameData.tryClearSession(game.scene, 0); @@ -79,9 +65,7 @@ describe("System - Game Data", () => { }); it("should return [false, false] session is out of date", async () => { - server.use( - http.post(`${apiBase}/savedata/session/clear`, () => HttpResponse.json({ error: "session out of date" })) - ); + vi.spyOn(pokerogueApi.savedata.session, "clear").mockResolvedValue({ error: "session out of date" }); const result = await game.scene.gameData.tryClearSession(game.scene, 0); diff --git a/src/test/utils/gameWrapper.ts b/src/test/utils/gameWrapper.ts index 22517502a05..ca5a67f901a 100644 --- a/src/test/utils/gameWrapper.ts +++ b/src/test/utils/gameWrapper.ts @@ -79,7 +79,7 @@ export default class GameWrapper { constructor(phaserGame: Phaser.Game, bypassLogin: boolean) { Phaser.Math.RND.sow([ 'test' ]); - vi.spyOn(Utils, "apiFetch", "get").mockReturnValue(fetch); + // vi.spyOn(Utils, "apiFetch", "get").mockReturnValue(fetch); if (bypassLogin) { vi.spyOn(battleScene, "bypassLogin", "get").mockReturnValue(true); } diff --git a/src/test/utils/testUtils.ts b/src/test/utils/testUtils.ts index b922fc9c61c..a8410f8ba40 100644 --- a/src/test/utils/testUtils.ts +++ b/src/test/utils/testUtils.ts @@ -21,3 +21,11 @@ export function mockI18next() { export function arrayOfRange(start: integer, end: integer) { return Array.from({ length: end - start }, (_v, k) => k + start); } + +/** + * Utility to get the API base URL from the environment variable (or the default/fallback). + * @returns the API base URL + */ +export function getApiBaseUrl() { + return import.meta.env.VITE_SERVER_URL ?? "http://localhost:8001"; +} diff --git a/src/test/vitest.setup.ts b/src/test/vitest.setup.ts index 8438f607db2..0b83d112522 100644 --- a/src/test/vitest.setup.ts +++ b/src/test/vitest.setup.ts @@ -38,7 +38,7 @@ vi.mock("i18next", async (importOriginal) => { const { setupServer } = await import("msw/node"); const { http, HttpResponse } = await import("msw"); - global.i18nServer = setupServer( + global.server = setupServer( http.get("/locales/en/*", async (req) => { const filename = req.params[0]; @@ -50,9 +50,12 @@ vi.mock("i18next", async (importOriginal) => { console.log(`Failed to load locale ${filename}!`, err); return HttpResponse.json({}); } - }) + }), + http.get("https://fonts.googleapis.com/*", () => { + return HttpResponse.text(""); + }), ); - global.i18nServer.listen({ onUnhandledRequest: "error" }); + global.server.listen({ onUnhandledRequest: "error" }); console.log("i18n MSW server listening!"); return await importOriginal(); @@ -83,6 +86,6 @@ beforeAll(() => { }); afterAll(() => { - global.i18nServer.close(); + global.server.close(); console.log("Closing i18n MSW server!"); }); diff --git a/src/ui/admin-ui-handler.ts b/src/ui/admin-ui-handler.ts index 6249e54d8c3..269b5ac5096 100644 --- a/src/ui/admin-ui-handler.ts +++ b/src/ui/admin-ui-handler.ts @@ -1,10 +1,14 @@ import BattleScene from "#app/battle-scene"; -import { ModalConfig } from "./modal-ui-handler"; -import { Mode } from "./ui"; -import * as Utils from "../utils"; -import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; import { Button } from "#app/enums/buttons"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; +import { formatText } from "#app/utils"; +import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; +import { ModalConfig } from "./modal-ui-handler"; import { TextStyle } from "./text"; +import { Mode } from "./ui"; + +type AdminUiHandlerService = "discord" | "google"; +type AdminUiHandlerServiceMode = "Link" | "Unlink"; export default class AdminUiHandler extends FormModalUiHandler { @@ -17,17 +21,15 @@ export default class AdminUiHandler extends FormModalUiHandler { private readonly httpUserNotFoundErrorCode: number = 404; private readonly ERR_REQUIRED_FIELD = (field: string) => { if (field === "username") { - return `${Utils.formatText(field)} is required`; + return `${formatText(field)} is required`; } else { - return `${Utils.formatText(field)} Id is required`; + return `${formatText(field)} Id is required`; } }; // returns a string saying whether a username has been successfully linked/unlinked to discord/google private readonly SUCCESS_SERVICE_MODE = (service: string, mode: string) => { return `Username and ${service} successfully ${mode.toLowerCase()}ed`; }; - private readonly ERR_USERNAME_NOT_FOUND: string = "Username not found!"; - private readonly ERR_GENERIC_ERROR: string = "There was an error"; constructor(scene: BattleScene, mode: Mode | null = null) { super(scene, mode); @@ -148,7 +150,6 @@ export default class AdminUiHandler extends FormModalUiHandler { } else if (this.adminMode === AdminMode.ADMIN) { this.updateAdminPanelInfo(adminSearchResult, AdminMode.SEARCH); } - return false; }; return true; } @@ -196,7 +197,7 @@ export default class AdminUiHandler extends FormModalUiHandler { this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error return this.showMessage(validFields.errorMessage ?? "", adminResult, true); } - this.adminLinkUnlink(this.convertInputsToAdmin(), service, mode).then(response => { // attempts to link/unlink depending on the service + this.adminLinkUnlink(this.convertInputsToAdmin(), service as AdminUiHandlerService, mode).then(response => { // attempts to link/unlink depending on the service if (response.error) { this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); return this.showMessage(response.errorType, adminResult, true); // fail @@ -276,12 +277,11 @@ export default class AdminUiHandler extends FormModalUiHandler { private async adminSearch(adminSearchResult: AdminSearchInfo) { try { - const adminInfo = await Utils.apiFetch(`admin/account/adminSearch?username=${encodeURIComponent(adminSearchResult.username)}`, true); - if (!adminInfo.ok) { // error - if adminInfo.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db - return { adminSearchResult: adminSearchResult, error: true, errorType: adminInfo.status === this.httpUserNotFoundErrorCode ? this.ERR_USERNAME_NOT_FOUND : this.ERR_GENERIC_ERROR }; + const [ adminInfo, errorType ] = await pokerogueApi.admin.searchAccount({ username: adminSearchResult.username }); + if (errorType || !adminInfo) { // error - if adminInfo.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db + return { adminSearchResult: adminSearchResult, error: true, errorType }; } else { // success - const adminInfoJson: AdminSearchInfo = await adminInfo.json(); - return { adminSearchResult: adminInfoJson, error: false }; + return { adminSearchResult: adminInfo, error: false }; } } catch (err) { console.error(err); @@ -289,12 +289,47 @@ export default class AdminUiHandler extends FormModalUiHandler { } } - private async adminLinkUnlink(adminSearchResult: AdminSearchInfo, service: string, mode: string) { + private async adminLinkUnlink(adminSearchResult: AdminSearchInfo, service: AdminUiHandlerService, mode: AdminUiHandlerServiceMode) { try { - const response = await Utils.apiPost(`admin/account/${service}${mode}`, `username=${encodeURIComponent(adminSearchResult.username)}&${service}Id=${encodeURIComponent(service === "discord" ? adminSearchResult.discordId : adminSearchResult.googleId)}`, "application/x-www-form-urlencoded", true); - if (!response.ok) { // error - if response.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db - return { adminSearchResult: adminSearchResult, error: true, errorType: response.status === this.httpUserNotFoundErrorCode ? this.ERR_USERNAME_NOT_FOUND : this.ERR_GENERIC_ERROR }; - } else { // success! + let errorType: string | null = null; + + if (service === "discord") { + if (mode === "Link") { + errorType = await pokerogueApi.admin.linkAccountToDiscord({ + discordId: adminSearchResult.discordId, + username: adminSearchResult.username, + }); + } else if (mode === "Unlink") { + errorType = await pokerogueApi.admin.unlinkAccountFromDiscord({ + discordId: adminSearchResult.discordId, + username: adminSearchResult.username, + }); + } else { + console.warn("Unknown mode", mode, "for service", service); + } + } else if (service === "google") { + if (mode === "Link") { + errorType = await pokerogueApi.admin.linkAccountToGoogleId({ + googleId: adminSearchResult.googleId, + username: adminSearchResult.username, + }); + } else if (mode === "Unlink") { + errorType = await pokerogueApi.admin.unlinkAccountFromGoogleId({ + googleId: adminSearchResult.googleId, + username: adminSearchResult.username, + }); + } else { + console.warn("Unknown mode", mode, "for service", service); + } + } else { + console.warn("Unknown service", service); + } + + if (errorType) { + // error - if response.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db + return { adminSearchResult: adminSearchResult, error: true, errorType }; + } else { + // success! return { adminSearchResult: adminSearchResult, error: false }; } } catch (err) { diff --git a/src/ui/daily-run-scoreboard.ts b/src/ui/daily-run-scoreboard.ts index b9c1c6ea49a..bb93b1fb1f5 100644 --- a/src/ui/daily-run-scoreboard.ts +++ b/src/ui/daily-run-scoreboard.ts @@ -3,8 +3,9 @@ import BattleScene from "../battle-scene"; import * as Utils from "../utils"; import { TextStyle, addTextObject } from "./text"; import { WindowVariant, addWindow } from "./ui-theme"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; -interface RankingEntry { +export interface RankingEntry { rank: integer, username: string, score: integer, @@ -12,7 +13,7 @@ interface RankingEntry { } // Don't forget to update translations when adding a new category -enum ScoreboardCategory { +export enum ScoreboardCategory { DAILY, WEEKLY } @@ -191,18 +192,17 @@ export class DailyRunScoreboard extends Phaser.GameObjects.Container { } Utils.executeIf(category !== this.category || this.pageCount === undefined, - () => Utils.apiFetch(`daily/rankingpagecount?category=${category}`).then(response => response.json()).then(count => this.pageCount = count) + () => pokerogueApi.daily.getRankingsPageCount({ category }).then(count => this.pageCount = count) ).then(() => { - Utils.apiFetch(`daily/rankings?category=${category}&page=${page}`) - .then(response => response.json()) - .then(jsonResponse => { + pokerogueApi.daily.getRankings({ category, page }) + .then(rankings => { this.page = page; this.category = category; this.titleLabel.setText(`${i18next.t(`menu:${ScoreboardCategory[category].toLowerCase()}Rankings`)}`); this.pageNumberLabel.setText(page.toString()); - if (jsonResponse) { + if (rankings) { this.loadingLabel.setVisible(false); - this.updateRankings(jsonResponse); + this.updateRankings(rankings); } else { this.loadingLabel.setText(i18next.t("menu:noRankings")); } diff --git a/src/ui/login-form-ui-handler.ts b/src/ui/login-form-ui-handler.ts index 26a2a225ec6..bba900aef0a 100644 --- a/src/ui/login-form-ui-handler.ts +++ b/src/ui/login-form-ui-handler.ts @@ -7,6 +7,7 @@ import BattleScene from "#app/battle-scene"; import { addTextObject, TextStyle } from "./text"; import { addWindow } from "./ui-theme"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; interface BuildInteractableImageOpts { scale?: number; @@ -135,21 +136,16 @@ export default class LoginFormUiHandler extends FormModalUiHandler { if (!this.inputs[0].text) { return onFail(i18next.t("menu:emptyUsername")); } - Utils.apiPost("account/login", `username=${encodeURIComponent(this.inputs[0].text)}&password=${encodeURIComponent(this.inputs[1].text)}`, "application/x-www-form-urlencoded") - .then(response => { - if (!response.ok) { - return response.text(); - } - return response.json(); - }) - .then(response => { - if (response.hasOwnProperty("token")) { - Utils.setCookie(Utils.sessionIdKey, response.token); - originalLoginAction && originalLoginAction(); - } else { - onFail(response); - } - }); + + const [ usernameInput, passwordInput ] = this.inputs; + + pokerogueApi.account.login({ username: usernameInput.text, password: passwordInput.text }).then(error => { + if (!error) { + originalLoginAction && originalLoginAction(); + } else { + onFail(error); + } + }); }; return true; diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index fea0a70af91..3ce3f3b7cf0 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -14,6 +14,7 @@ import BgmBar from "#app/ui/bgm-bar"; import AwaitableUiHandler from "./awaitable-ui-handler"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { AdminMode, getAdminModeName } from "./admin-ui-handler"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; enum MenuOptions { GAME_SETTINGS, @@ -539,10 +540,7 @@ export default class MenuUiHandler extends MessageUiHandler { window.open(discordUrl, "_self"); return true; } else { - Utils.apiPost("/auth/discord/logout", undefined, undefined, true).then(res => { - if (!res.ok) { - console.error(`Unlink failed (${res.status}: ${res.statusText})`); - } + pokerogueApi.unlinkDiscord().then(_isSuccess => { updateUserInfo().then(() => this.scene.reset(true, true)); }); return true; @@ -560,10 +558,7 @@ export default class MenuUiHandler extends MessageUiHandler { window.open(googleUrl, "_self"); return true; } else { - Utils.apiPost("/auth/google/logout", undefined, undefined, true).then(res => { - if (!res.ok) { - console.error(`Unlink failed (${res.status}: ${res.statusText})`); - } + pokerogueApi.unlinkGoogle().then(_isSuccess => { updateUserInfo().then(() => this.scene.reset(true, true)); }); return true; @@ -612,11 +607,7 @@ export default class MenuUiHandler extends MessageUiHandler { success = true; const doLogout = () => { ui.setMode(Mode.LOADING, { - buttonActions: [], fadeOut: () => Utils.apiFetch("account/logout", true).then(res => { - if (!res.ok) { - console.error(`Log out failed (${res.status}: ${res.statusText})`); - } - Utils.removeCookie(Utils.sessionIdKey); + buttonActions: [], fadeOut: () => pokerogueApi.account.logout().then(() => { updateUserInfo().then(() => this.scene.reset(true, true)); }) }); diff --git a/src/ui/registration-form-ui-handler.ts b/src/ui/registration-form-ui-handler.ts index 2c35ff8ee7f..892f78bd1ba 100644 --- a/src/ui/registration-form-ui-handler.ts +++ b/src/ui/registration-form-ui-handler.ts @@ -1,9 +1,9 @@ import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; import { ModalConfig } from "./modal-ui-handler"; -import * as Utils from "../utils"; import { Mode } from "./ui"; import { TextStyle, addTextObject } from "./text"; import i18next from "i18next"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; interface LanguageSetting { @@ -110,27 +110,20 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler { if (this.inputs[1].text !== this.inputs[2].text) { return onFail(i18next.t("menu:passwordNotMatchingConfirmPassword")); } - Utils.apiPost("account/register", `username=${encodeURIComponent(this.inputs[0].text)}&password=${encodeURIComponent(this.inputs[1].text)}`, "application/x-www-form-urlencoded") - .then(response => response.text()) - .then(response => { - if (!response) { - Utils.apiPost("account/login", `username=${encodeURIComponent(this.inputs[0].text)}&password=${encodeURIComponent(this.inputs[1].text)}`, "application/x-www-form-urlencoded") - .then(response => { - if (!response.ok) { - return response.text(); - } - return response.json(); - }) - .then(response => { - if (response.hasOwnProperty("token")) { - Utils.setCookie(Utils.sessionIdKey, response.token); + const [ usernameInput, passwordInput ] = this.inputs; + pokerogueApi.account.register({ username: usernameInput.text, password: passwordInput.text }) + .then(registerError => { + if (!registerError) { + pokerogueApi.account.login({ username: usernameInput.text, password: passwordInput.text }) + .then(loginError => { + if (!loginError) { originalRegistrationAction && originalRegistrationAction(); } else { - onFail(response); + onFail(loginError); } }); } else { - onFail(response); + onFail(registerError); } }); }; diff --git a/src/ui/title-ui-handler.ts b/src/ui/title-ui-handler.ts index 3bfba71ef08..aec80f049c9 100644 --- a/src/ui/title-ui-handler.ts +++ b/src/ui/title-ui-handler.ts @@ -7,6 +7,7 @@ import { getSplashMessages } from "../data/splash-messages"; import i18next from "i18next"; import { TimedEventDisplay } from "#app/timed-event-manager"; import { version } from "../../package.json"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; export default class TitleUiHandler extends OptionSelectUiHandler { /** If the stats can not be retrieved, use this fallback value */ @@ -78,12 +79,13 @@ export default class TitleUiHandler extends OptionSelectUiHandler { } updateTitleStats(): void { - Utils.apiFetch("game/titlestats") - .then(request => request.json()) + pokerogueApi.getGameTitleStats() .then(stats => { - this.playerCountLabel.setText(`${stats.playerCount} ${i18next.t("menu:playersOnline")}`); - if (this.splashMessage === "splashMessages:battlesWon") { - this.splashMessageText.setText(i18next.t(this.splashMessage, { count: stats.battleCount })); + if (stats) { + this.playerCountLabel.setText(`${stats.playerCount} ${i18next.t("menu:playersOnline")}`); + if (this.splashMessage === "splashMessages:battlesWon") { + this.splashMessageText.setText(i18next.t(this.splashMessage, { count: stats.battleCount })); + } } }) .catch(err => { diff --git a/src/utils.ts b/src/utils.ts index 8b779f32730..be0aec84ecd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ import { MoneyFormat } from "#enums/money-format"; import { Moves } from "#enums/moves"; import i18next from "i18next"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; export type nil = null | undefined; @@ -258,9 +259,16 @@ export const isLocal = ( /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/.test(window.location.hostname)) && window.location.port !== "") || window.location.hostname === ""; +/** + * @deprecated Refer to [pokerogue-api.ts](./plugins/api/pokerogue-api.ts) instead + */ export const localServerUrl = import.meta.env.VITE_SERVER_URL ?? `http://${window.location.hostname}:${window.location.port + 1}`; -// Set the server URL based on whether it's local or not +/** + * Set the server URL based on whether it's local or not + * + * @deprecated Refer to [pokerogue-api.ts](./plugins/api/pokerogue-api.ts) instead + */ export const apiUrl = localServerUrl ?? "https://api.pokerogue.net"; // used to disable api calls when isLocal is true and a server is not found export let isLocalServerConnected = true; @@ -307,48 +315,14 @@ export function getCookie(cName: string): string { * with a GET request to verify if a server is running, * sets isLocalServerConnected based on results */ -export function localPing() { +export async function localPing() { if (isLocal) { - apiFetch("game/titlestats") - .then(resolved => isLocalServerConnected = true, - rejected => isLocalServerConnected = false - ); + const titleStats = await pokerogueApi.getGameTitleStats(); + isLocalServerConnected = !!titleStats; + console.log("isLocalServerConnected:", isLocalServerConnected); } } -export function apiFetch(path: string, authed: boolean = false): Promise { - return (isLocal && isLocalServerConnected) || !isLocal ? new Promise((resolve, reject) => { - const request = {}; - if (authed) { - const sId = getCookie(sessionIdKey); - if (sId) { - request["headers"] = { "Authorization": sId }; - } - } - fetch(`${apiUrl}/${path}`, request) - .then(response => resolve(response)) - .catch(err => reject(err)); - }) : new Promise(() => {}); -} - -export function apiPost(path: string, data?: any, contentType: string = "application/json", authed: boolean = false): Promise { - return (isLocal && isLocalServerConnected) || !isLocal ? new Promise((resolve, reject) => { - const headers = { - "Accept": contentType, - "Content-Type": contentType, - }; - if (authed) { - const sId = getCookie(sessionIdKey); - if (sId) { - headers["Authorization"] = sId; - } - } - fetch(`${apiUrl}/${path}`, { method: "POST", headers: headers, body: data }) - .then(response => resolve(response)) - .catch(err => reject(err)); - }) : new Promise(() => {}); -} - /** Alias for the constructor of a class */ export type Constructor = new(...args: unknown[]) => T; From 433d4b4fc93a27579a36ab2cf6e8a20d20385d3e Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Tue, 5 Nov 2024 02:24:17 +0100 Subject: [PATCH 24/98] [P3][UI][QoL] Fix tooltips going out of screen on mobile & other improvements (#4792) * [ui] Fix tooltip placement when using touchscreen * [ui] make candy friendship tooltip hitbox bigger --- src/ui/starter-select-ui-handler.ts | 79 ++++++++++++++--------------- src/ui/summary-ui-handler.ts | 4 +- src/ui/ui.ts | 35 ++++++++++--- 3 files changed, 68 insertions(+), 50 deletions(-) diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 3a29f9431e7..bb4dc74d44e 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -242,6 +242,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { private pokemonEggMoveContainers: Phaser.GameObjects.Container[]; private pokemonEggMoveBgs: Phaser.GameObjects.NineSlice[]; private pokemonEggMoveLabels: Phaser.GameObjects.Text[]; + private pokemonCandyContainer: Phaser.GameObjects.Container; private pokemonCandyIcon: Phaser.GameObjects.Sprite; private pokemonCandyDarknessOverlay: Phaser.GameObjects.Sprite; private pokemonCandyOverlayIcon: Phaser.GameObjects.Sprite; @@ -686,31 +687,36 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonLuckText.setOrigin(0, 0); this.starterSelectContainer.add(this.pokemonLuckText); - this.pokemonCandyIcon = this.scene.add.sprite(4.5, 18, "candy"); + // Candy icon and count + this.pokemonCandyContainer = this.scene.add.container(4.5, 18); + + this.pokemonCandyIcon = this.scene.add.sprite(0, 0, "candy"); this.pokemonCandyIcon.setScale(0.5); this.pokemonCandyIcon.setOrigin(0, 0); - this.starterSelectContainer.add(this.pokemonCandyIcon); + this.pokemonCandyContainer.add(this.pokemonCandyIcon); - this.pokemonFormText = addTextObject(this.scene, 6, 42, "Form", TextStyle.WINDOW_ALT, { fontSize: "42px" }); - this.pokemonFormText.setOrigin(0, 0); - this.starterSelectContainer.add(this.pokemonFormText); - - this.pokemonCandyOverlayIcon = this.scene.add.sprite(4.5, 18, "candy_overlay"); + this.pokemonCandyOverlayIcon = this.scene.add.sprite(0, 0, "candy_overlay"); this.pokemonCandyOverlayIcon.setScale(0.5); this.pokemonCandyOverlayIcon.setOrigin(0, 0); - this.starterSelectContainer.add(this.pokemonCandyOverlayIcon); + this.pokemonCandyContainer.add(this.pokemonCandyOverlayIcon); - this.pokemonCandyDarknessOverlay = this.scene.add.sprite(4.5, 18, "candy"); + this.pokemonCandyDarknessOverlay = this.scene.add.sprite(0, 0, "candy"); this.pokemonCandyDarknessOverlay.setScale(0.5); this.pokemonCandyDarknessOverlay.setOrigin(0, 0); this.pokemonCandyDarknessOverlay.setTint(0x000000); this.pokemonCandyDarknessOverlay.setAlpha(0.50); - this.pokemonCandyDarknessOverlay.setInteractive(new Phaser.Geom.Rectangle(0, 0, 16, 16), Phaser.Geom.Rectangle.Contains); - this.starterSelectContainer.add(this.pokemonCandyDarknessOverlay); + this.pokemonCandyContainer.add(this.pokemonCandyDarknessOverlay); - this.pokemonCandyCountText = addTextObject(this.scene, 14, 18, "x0", TextStyle.WINDOW_ALT, { fontSize: "56px" }); + this.pokemonCandyCountText = addTextObject(this.scene, 9.5, 0, "x0", TextStyle.WINDOW_ALT, { fontSize: "56px" }); this.pokemonCandyCountText.setOrigin(0, 0); - this.starterSelectContainer.add(this.pokemonCandyCountText); + this.pokemonCandyContainer.add(this.pokemonCandyCountText); + + this.pokemonCandyContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 30, 20), Phaser.Geom.Rectangle.Contains); + this.starterSelectContainer.add(this.pokemonCandyContainer); + + this.pokemonFormText = addTextObject(this.scene, 6, 42, "Form", TextStyle.WINDOW_ALT, { fontSize: "42px" }); + this.pokemonFormText.setOrigin(0, 0); + this.starterSelectContainer.add(this.pokemonFormText); this.pokemonCaughtHatchedContainer = this.scene.add.container(2, 25); this.pokemonCaughtHatchedContainer.setScale(0.5); @@ -2822,10 +2828,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonShinyIcon.setY(135); this.pokemonShinyIcon.setFrame(getVariantIcon(variant)); [ - this.pokemonCandyIcon, - this.pokemonCandyOverlayIcon, - this.pokemonCandyDarknessOverlay, - this.pokemonCandyCountText, + this.pokemonCandyContainer, this.pokemonHatchedIcon, this.pokemonHatchedCountText ].map(c => c.setVisible(false)); @@ -2834,31 +2837,26 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonCaughtHatchedContainer.setY(25); this.pokemonShinyIcon.setY(117); this.pokemonCandyIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[0]))); - this.pokemonCandyIcon.setVisible(true); this.pokemonCandyOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[1]))); - this.pokemonCandyOverlayIcon.setVisible(true); - this.pokemonCandyDarknessOverlay.setVisible(true); this.pokemonCandyCountText.setText(`x${this.scene.gameData.starterData[species.speciesId].candyCount}`); - this.pokemonCandyCountText.setVisible(true); + this.pokemonCandyContainer.setVisible(true); this.pokemonFormText.setY(42); this.pokemonHatchedIcon.setVisible(true); this.pokemonHatchedCountText.setVisible(true); const { currentFriendship, friendshipCap } = this.getFriendship(this.lastSpecies.speciesId); const candyCropY = 16 - (16 * (currentFriendship / friendshipCap)); - - if (this.pokemonCandyDarknessOverlay.visible) { - this.pokemonCandyDarknessOverlay.on("pointerover", () => { - this.scene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true); - this.activeTooltip = "CANDY"; - }); - this.pokemonCandyDarknessOverlay.on("pointerout", () => { - this.scene.ui.hideTooltip(); - this.activeTooltip = undefined; - }); - } - this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY); + + this.pokemonCandyContainer.on("pointerover", () => { + this.scene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true); + this.activeTooltip = "CANDY"; + }); + this.pokemonCandyContainer.on("pointerout", () => { + this.scene.ui.hideTooltip(); + this.activeTooltip = undefined; + }); + } @@ -2934,10 +2932,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonPassiveLabelText.setVisible(false); this.pokemonNatureLabelText.setVisible(false); this.pokemonCaughtHatchedContainer.setVisible(false); - this.pokemonCandyIcon.setVisible(false); - this.pokemonCandyOverlayIcon.setVisible(false); - this.pokemonCandyDarknessOverlay.setVisible(false); - this.pokemonCandyCountText.setVisible(false); + this.pokemonCandyContainer.setVisible(false); this.pokemonFormText.setVisible(false); const defaultDexAttr = this.scene.gameData.getSpeciesDefaultDexAttr(species, true, true); @@ -2971,10 +2966,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonPassiveLabelText.setVisible(false); this.pokemonNatureLabelText.setVisible(false); this.pokemonCaughtHatchedContainer.setVisible(false); - this.pokemonCandyIcon.setVisible(false); - this.pokemonCandyOverlayIcon.setVisible(false); - this.pokemonCandyDarknessOverlay.setVisible(false); - this.pokemonCandyCountText.setVisible(false); + this.pokemonCandyContainer.setVisible(false); this.pokemonFormText.setVisible(false); this.setSpeciesDetails(species!, { // TODO: is this bang correct? @@ -3005,7 +2997,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { || !isNullOrUndefined(formIndex) || !isNullOrUndefined(shiny) || !isNullOrUndefined(variant); if (this.activeTooltip === "CANDY") { - if (this.lastSpecies) { + if (this.lastSpecies && this.pokemonCandyContainer.visible) { const { currentFriendship, friendshipCap } = this.getFriendship(this.lastSpecies.speciesId); this.scene.ui.editTooltip("", `${currentFriendship}/${friendshipCap}`); } else { @@ -3230,6 +3222,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonPassiveLockedIcon.setVisible(!isUnlocked); this.pokemonPassiveLockedIcon.setPosition(iconPosition.x, iconPosition.y); + } else if (this.activeTooltip === "PASSIVE") { + // No passive and passive tooltip is active > hide it + this.scene.ui.hideTooltip(); } this.pokemonNatureText.setText(getNatureName(natureIndex as unknown as Nature, true, true, false, this.scene.uiTheme)); diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 9a61dd0f688..4b36867571e 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -184,7 +184,7 @@ export default class SummaryUiHandler extends UiHandler { this.candyShadow.setTint(0x000000); this.candyShadow.setAlpha(0.50); this.candyShadow.setScale(0.8); - this.candyShadow.setInteractive(new Phaser.Geom.Rectangle(0, 0, 16, 16), Phaser.Geom.Rectangle.Contains); + this.candyShadow.setInteractive(new Phaser.Geom.Rectangle(0, 0, 30, 16), Phaser.Geom.Rectangle.Contains); this.summaryContainer.add(this.candyShadow); this.candyCountText = addTextObject(this.scene, 20, -146, "x0", TextStyle.WINDOW_ALT, { fontSize: "76px" }); @@ -203,7 +203,7 @@ export default class SummaryUiHandler extends UiHandler { this.friendshipShadow.setTint(0x000000); this.friendshipShadow.setAlpha(0.50); this.friendshipShadow.setScale(0.8); - this.friendshipShadow.setInteractive(new Phaser.Geom.Rectangle(0, 0, 16, 16), Phaser.Geom.Rectangle.Contains); + this.friendshipShadow.setInteractive(new Phaser.Geom.Rectangle(0, 0, 50, 16), Phaser.Geom.Rectangle.Contains); this.summaryContainer.add(this.friendshipShadow); this.friendshipText = addTextObject(this.scene, 20, -66, "x0", TextStyle.WINDOW_ALT, { fontSize: "76px" }); diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 63cd48ab1cd..fc8fa94c848 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -391,6 +391,7 @@ export default class UI extends Phaser.GameObjects.Container { this.tooltipContent.y = title ? 16 : 4; this.tooltipBg.width = Math.min(Math.max(this.tooltipTitle.displayWidth, this.tooltipContent.displayWidth) + 12, 838); this.tooltipBg.height = (title ? 31 : 19) + 10.5 * (wrappedContent.split("\n").length - 1); + this.tooltipTitle.x = this.tooltipBg.width / 2; } hideTooltip(): void { @@ -400,12 +401,34 @@ export default class UI extends Phaser.GameObjects.Container { update(): void { if (this.tooltipContainer.visible) { - const xReverse = this.scene.game.input.mousePointer && this.scene.game.input.mousePointer.x >= this.scene.game.canvas.width - this.tooltipBg.width * 6 - 12; - const yReverse = this.scene.game.input.mousePointer && this.scene.game.input.mousePointer.y >= this.scene.game.canvas.height - this.tooltipBg.height * 6 - 12; - this.tooltipContainer.setPosition( - !xReverse ? this.scene.game.input.mousePointer!.x / 6 + 2 : this.scene.game.input.mousePointer!.x / 6 - this.tooltipBg.width - 2, - !yReverse ? this.scene.game.input.mousePointer!.y / 6 + 2 : this.scene.game.input.mousePointer!.y / 6 - this.tooltipBg.height - 2, - ); + const isTouch = (this.scene as BattleScene).inputMethod === "touch"; + const pointerX = this.scene.game.input.activePointer.x; + const pointerY = this.scene.game.input.activePointer.y; + const tooltipWidth = this.tooltipBg.width; + const tooltipHeight = this.tooltipBg.height; + const padding = 2; + + // Default placement is top left corner of the screen on mobile. Otherwise below the cursor, to the right + let x = isTouch ? padding : pointerX / 6 + padding; + let y = isTouch ? padding : pointerY / 6 + padding; + + if (isTouch) { + // If we are in the top left quadrant on mobile, move the tooltip to the top right corner + if (pointerX <= this.scene.game.canvas.width / 2 && pointerY <= this.scene.game.canvas.height / 2) { + x = this.scene.game.canvas.width / 6 - tooltipWidth - padding; + } + } else { + // If the tooltip would go offscreen on the right, or is close to it, move to the left of the cursor + if (x + tooltipWidth + padding > this.scene.game.canvas.width / 6) { + x = Math.max(padding, pointerX / 6 - tooltipWidth - padding); + } + // If the tooltip would go offscreen at the bottom, or is close to it, move above the cursor + if (y + tooltipHeight + padding > this.scene.game.canvas.height / 6) { + y = Math.max(padding, pointerY / 6 - tooltipHeight - padding); + } + } + + this.tooltipContainer.setPosition(x, y); } } From ab6d15ee8a31bfdeacf92ab9fc2624b31c7eb527 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:31:40 -0800 Subject: [PATCH 25/98] [Move] Improve Future Sight & Doom Desire (still partial) (#4545) * Fix behavior of Future Sight and Doom Desire Add test for Future Sight Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Prevent crash if Future Sight target is missing Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> * Add `partial()` comments, update `DelayedAttackAttr` return --------- Co-authored-by: Gianluca Fuoco Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Co-authored-by: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> --- src/data/arena-tag.ts | 9 ++--- src/data/move.ts | 24 ++++++++++--- src/phases/move-effect-phase.ts | 18 ++++++---- src/phases/move-phase.ts | 55 +++++++++++++++++++++++++++-- src/test/moves/future_sight.test.ts | 45 +++++++++++++++++++++++ 5 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 src/test/moves/future_sight.test.ts diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 3dd526676a0..73f8e16d573 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -780,13 +780,14 @@ class ToxicSpikesTag extends ArenaTrapTag { * Delays the attack's effect by a set amount of turns, usually 3 (including the turn the move is used), * and deals damage after the turn count is reached. */ -class DelayedAttackTag extends ArenaTag { +export class DelayedAttackTag extends ArenaTag { public targetIndex: BattlerIndex; - constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex) { - super(tagType, 3, sourceMove, sourceId); + constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH) { + super(tagType, 3, sourceMove, sourceId, side); this.targetIndex = targetIndex; + this.side = side; } lapse(arena: Arena): boolean { @@ -1250,7 +1251,7 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove return new ToxicSpikesTag(sourceId, side); case ArenaTagType.FUTURE_SIGHT: case ArenaTagType.DOOM_DESIRE: - return new DelayedAttackTag(tagType, sourceMove, sourceId, targetIndex!); // TODO:questionable bang + return new DelayedAttackTag(tagType, sourceMove, sourceId, targetIndex!, side); // TODO:questionable bang case ArenaTagType.WISH: return new WishTag(turnCount, sourceId, side); case ArenaTagType.STEALTH_ROCK: diff --git a/src/data/move.ts b/src/data/move.ts index 6e350315e65..5ae37c3b092 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2785,6 +2785,14 @@ export class OverrideMoveEffectAttr extends MoveAttr { } } +/** + * Attack Move that doesn't hit the turn it is played and doesn't allow for multiple + * uses on the same target. Examples are Future Sight or Doom Desire. + * @extends OverrideMoveEffectAttr + * @param tagType The {@linkcode ArenaTagType} that will be placed on the field when the move is used + * @param chargeAnim The {@linkcode ChargeAnim | Charging Animation} used for the move + * @param chargeText The text to display when the move is used + */ export class DelayedAttackAttr extends OverrideMoveEffectAttr { public tagType: ArenaTagType; public chargeAnim: ChargeAnim; @@ -2799,13 +2807,18 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { + // Edge case for the move applied on a pokemon that has fainted + if (!target) { + return Promise.resolve(true); + } + const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; return new Promise(resolve => { if (args.length < 2 || !args[1]) { new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => { (args[0] as Utils.BooleanHolder).value = true; user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user))); user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER }); - user.scene.arena.addTag(this.tagType, 3, move.id, user.id, ArenaTagSide.BOTH, false, target.getBattlerIndex()); + user.scene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex()); resolve(true); }); @@ -5534,7 +5547,8 @@ export class AddArenaTagAttr extends MoveEffectAttr { } if ((move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) { - user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY); + const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, side); return true; } @@ -8297,7 +8311,8 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .ballBombMove(), new AttackMove(Moves.FUTURE_SIGHT, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2) - .partial() // Complete buggy mess + .partial() // cannot be used on multiple Pokemon on the same side in a double battle, hits immediately when called by Metronome/etc + .ignoresProtect() .attr(DelayedAttackAttr, ArenaTagType.FUTURE_SIGHT, ChargeAnim.FUTURE_SIGHT_CHARGING, i18next.t("moveTriggers:foresawAnAttack", { pokemonName: "{USER}" })), new AttackMove(Moves.ROCK_SMASH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2) .attr(StatStageChangeAttr, [ Stat.DEF ], -1), @@ -8604,7 +8619,8 @@ export function initMoves() { .attr(ConfuseAttr) .pulseMove(), new AttackMove(Moves.DOOM_DESIRE, Type.STEEL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 3) - .partial() // Complete buggy mess + .partial() // cannot be used on multiple Pokemon on the same side in a double battle, hits immediately when called by Metronome/etc + .ignoresProtect() .attr(DelayedAttackAttr, ArenaTagType.DOOM_DESIRE, ChargeAnim.DOOM_DESIRE_CHARGING, i18next.t("moveTriggers:choseDoomDesireAsDestiny", { pokemonName: "{USER}" })), new AttackMove(Moves.PSYCHO_BOOST, Type.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true), diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 4c813a881d3..62ece3769b5 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -25,6 +25,7 @@ import { applyFilteredMoveAttrs, applyMoveAttrs, AttackMove, + DelayedAttackAttr, FixedDamageAttr, HitsTagAttr, MissEffectAttr, @@ -85,8 +86,13 @@ export class MoveEffectPhase extends PokemonPhase { /** All Pokemon targeted by this phase's invoked move */ const targets = this.getTargets(); - /** If the user was somehow removed from the field, end this phase */ - if (!user?.isOnField()) { + if (!user) { + return super.end(); + } + + 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() && !isDelayedAttack) { return super.end(); } @@ -142,9 +148,9 @@ export class MoveEffectPhase extends PokemonPhase { const hasActiveTargets = targets.some(t => t.isActive(true)); /** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */ - const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) - && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) - && !targets[0].getTag(SemiInvulnerableTag); + const isImmune = targets[0]?.hasAbilityWithAttr(TypeImmunityAbAttr) + && (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) + && !targets[0]?.getTag(SemiInvulnerableTag); /** * If no targets are left for the move to hit (FAIL), or the invoked move is single-target @@ -156,7 +162,7 @@ export class MoveEffectPhase extends PokemonPhase { if (hasActiveTargets) { this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" })); moveHistoryEntry.result = MoveResult.MISS; - applyMoveAttrs(MissEffectAttr, user, null, move); + applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove()); } else { this.scene.queueMessage(i18next.t("battle:attackFailed")); moveHistoryEntry.result = MoveResult.FAIL; diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 5c6c339ffa5..b11a7d9b711 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -1,9 +1,31 @@ import { BattlerIndex } from "#app/battle"; import BattleScene from "#app/battle-scene"; -import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability"; +import { + applyAbAttrs, + applyPostMoveUsedAbAttrs, + applyPreAttackAbAttrs, + BlockRedirectAbAttr, + IncreasePpAbAttr, + PokemonTypeChangeAbAttr, + PostMoveUsedAbAttr, + RedirectMoveAbAttr, + ReduceStatusEffectDurationAbAttr +} from "#app/data/ability"; +import { DelayedAttackTag } from "#app/data/arena-tag"; import { CommonAnim } from "#app/data/battle-anims"; import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags"; -import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, frenzyMissFunc, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move"; +import { + allMoves, + applyMoveAttrs, + BypassRedirectAttr, + BypassSleepAttr, + CopyMoveAttr, + DelayedAttackAttr, + frenzyMissFunc, + HealStatusEffectAttr, + MoveFlags, + PreMoveMessageAttr +} from "#app/data/move"; import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect"; import { Type } from "#app/data/type"; @@ -14,16 +36,17 @@ import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; import { BattlePhase } from "#app/phases/battle-phase"; import { CommonAnimPhase } from "#app/phases/common-anim-phase"; +import { MoveChargePhase } from "#app/phases/move-charge-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { BooleanHolder, NumberHolder } from "#app/utils"; import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; -import { MoveChargePhase } from "#app/phases/move-charge-phase"; export class MovePhase extends BattlePhase { protected _pokemon: Pokemon; @@ -227,6 +250,32 @@ export class MovePhase extends BattlePhase { // form changes happen even before we know that the move wll execute. this.scene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger); + const isDelayedAttack = this.move.getMove().hasAttr(DelayedAttackAttr); + if (isDelayedAttack) { + // Check the player side arena if future sight is active + const futureSightTags = this.scene.arena.findTags(t => t.tagType === ArenaTagType.FUTURE_SIGHT); + const doomDesireTags = this.scene.arena.findTags(t => t.tagType === ArenaTagType.DOOM_DESIRE); + let fail = false; + const currentTargetIndex = targets[0].getBattlerIndex(); + for (const tag of futureSightTags) { + if ((tag as DelayedAttackTag).targetIndex === currentTargetIndex) { + fail = true; + break; + } + } + for (const tag of doomDesireTags) { + if ((tag as DelayedAttackTag).targetIndex === currentTargetIndex) { + fail = true; + break; + } + } + if (fail) { + this.showMoveText(); + this.showFailedText(); + return this.end(); + } + } + this.showMoveText(); if (moveQueue.length > 0) { diff --git a/src/test/moves/future_sight.test.ts b/src/test/moves/future_sight.test.ts new file mode 100644 index 00000000000..d0110a87202 --- /dev/null +++ b/src/test/moves/future_sight.test.ts @@ -0,0 +1,45 @@ +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"; + +describe("Moves - Future Sight", () => { + 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 + .startingLevel(50) + .moveset([ Moves.FUTURE_SIGHT, Moves.SPLASH ]) + .battleType("single") + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.STURDY) + .enemyMoveset(Moves.SPLASH); + }); + + it("hits 2 turns after use, ignores user switch out", async () => { + await game.classicMode.startBattle([ Species.FEEBAS, Species.MILOTIC ]); + + game.move.select(Moves.FUTURE_SIGHT); + await game.toNextTurn(); + game.doSwitchPokemon(1); + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(game.scene.getEnemyPokemon()!.isFullHp()).toBe(false); + }); +}); From 13841765f177a2e6626683967457adcfc965b631 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:33:15 -0500 Subject: [PATCH 26/98] [Dev] Update test save with everything unlocked (#4137) * Add test save with all egg moves unlocked * Move to everything.prsv --------- Co-authored-by: Lugiad Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/test/utils/saves/everything.prsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/utils/saves/everything.prsv b/src/test/utils/saves/everything.prsv index 1985a6649ca..f9106ce0e35 100644 --- a/src/test/utils/saves/everything.prsv +++ b/src/test/utils/saves/everything.prsv @@ -1 +1 @@ -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 \ No newline at end of file +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 From 543a97076bc2ccb32f6978e9e26b17877a309414 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:19:20 -0500 Subject: [PATCH 27/98] [P1][Beta] Fix FormChangePhase being broken by EvolutionPhase refactor (#4795) * [P1][Beta] Fix FormChangePhase being broken by EvolutionPhase refactor * Apply suggested changes to added test --- src/phases/evolution-phase.ts | 11 ++-- src/test/phases/form-change-phase.test.ts | 60 +++++++++++++++++++++ src/test/utils/helpers/classicModeHelper.ts | 2 +- src/test/utils/phaseInterceptor.ts | 19 +++++-- 4 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 src/test/phases/form-change-phase.test.ts diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index dec65e2c945..01994263688 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -39,8 +39,6 @@ export class EvolutionPhase extends Phase { this.pokemon = pokemon; this.evolution = evolution; this.lastLevel = lastLevel; - this.evolutionBgm = this.scene.playSoundWithoutBgm("evolution"); - this.preEvolvedPokemonName = getPokemonNameWithAffix(this.pokemon); } validate(): boolean { @@ -62,9 +60,9 @@ export class EvolutionPhase extends Phase { this.scene.fadeOutBgm(undefined, false); - const evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler; + this.evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler; - this.evolutionContainer = evolutionHandler.evolutionContainer; + this.evolutionContainer = this.evolutionHandler.evolutionContainer; this.evolutionBaseBg = this.scene.add.image(0, 0, "default_bg"); this.evolutionBaseBg.setOrigin(0, 0); @@ -117,14 +115,12 @@ export class EvolutionPhase extends Phase { sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k]; }); }); - + this.preEvolvedPokemonName = getPokemonNameWithAffix(this.pokemon); this.doEvolution(); }); } doEvolution(): void { - this.evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler; - this.scene.ui.showText(i18next.t("menu:evolving", { pokemonName: this.preEvolvedPokemonName }), null, () => { this.pokemon.cry(); @@ -145,6 +141,7 @@ export class EvolutionPhase extends Phase { }); this.scene.time.delayedCall(1000, () => { + this.evolutionBgm = this.scene.playSoundWithoutBgm("evolution"); this.scene.tweens.add({ targets: this.evolutionBgOverlay, alpha: 1, diff --git a/src/test/phases/form-change-phase.test.ts b/src/test/phases/form-change-phase.test.ts new file mode 100644 index 00000000000..70ca429672f --- /dev/null +++ b/src/test/phases/form-change-phase.test.ts @@ -0,0 +1,60 @@ +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 { Type } from "#app/data/type"; +import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { modifierTypes } from "#app/modifier/modifier-type"; + +describe("Form Change Phase", () => { + 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 ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("Zacian should successfully change into Crowned form", async () => { + await game.classicMode.startBattle([ Species.ZACIAN ]); + + // Before the form change: Should be Hero form + const zacian = game.scene.getPlayerParty()[0]; + expect(zacian.getFormKey()).toBe("hero-of-many-battles"); + expect(zacian.getTypes()).toStrictEqual([ Type.FAIRY ]); + expect(zacian.calculateBaseStats()).toStrictEqual([ 92, 120, 115, 80, 115, 138 ]); + + // Give Zacian a Rusted Sword + const rustedSwordType = generateModifierType(game.scene, modifierTypes.RARE_FORM_CHANGE_ITEM)!; + const rustedSword = rustedSwordType.newModifier(zacian); + await game.scene.addModifier(rustedSword); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + // After the form change: Should be Crowned form + expect(game.phaseInterceptor.log.includes("FormChangePhase")).toBe(true); + expect(zacian.getFormKey()).toBe("crowned"); + expect(zacian.getTypes()).toStrictEqual([ Type.FAIRY, Type.STEEL ]); + expect(zacian.calculateBaseStats()).toStrictEqual([ 92, 150, 115, 80, 115, 148 ]); + }); +}); diff --git a/src/test/utils/helpers/classicModeHelper.ts b/src/test/utils/helpers/classicModeHelper.ts index 80d0b86de7b..41a21a52b72 100644 --- a/src/test/utils/helpers/classicModeHelper.ts +++ b/src/test/utils/helpers/classicModeHelper.ts @@ -35,7 +35,7 @@ export class ClassicModeHelper extends GameManagerHelper { selectStarterPhase.initBattle(starters); }); - await this.game.phaseInterceptor.run(EncounterPhase); + await this.game.phaseInterceptor.to(EncounterPhase); if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) { this.game.removeEnemyHeldItems(); } diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index ec9309e2405..17b6c7a6c81 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -12,6 +12,7 @@ import { EndEvolutionPhase } from "#app/phases/end-evolution-phase"; import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; import { EvolutionPhase } from "#app/phases/evolution-phase"; import { FaintPhase } from "#app/phases/faint-phase"; +import { FormChangePhase } from "#app/phases/form-change-phase"; import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { LevelCapPhase } from "#app/phases/level-cap-phase"; import { LoginPhase } from "#app/phases/login-phase"; @@ -67,7 +68,6 @@ type PhaseClass = | typeof LoginPhase | typeof TitlePhase | typeof SelectGenderPhase - | typeof EncounterPhase | typeof NewBiomeEncounterPhase | typeof SelectStarterPhase | typeof PostSummonPhase @@ -102,6 +102,7 @@ type PhaseClass = | typeof SwitchPhase | typeof SwitchSummonPhase | typeof PartyHealPhase + | typeof FormChangePhase | typeof EvolutionPhase | typeof EndEvolutionPhase | typeof LevelCapPhase @@ -114,13 +115,13 @@ type PhaseClass = | typeof PostMysteryEncounterPhase | typeof ModifierRewardPhase | typeof PartyExpPhase - | typeof ExpPhase; + | typeof ExpPhase + | typeof EncounterPhase; type PhaseString = | "LoginPhase" | "TitlePhase" | "SelectGenderPhase" - | "EncounterPhase" | "NewBiomeEncounterPhase" | "SelectStarterPhase" | "PostSummonPhase" @@ -155,6 +156,7 @@ type PhaseString = | "SwitchPhase" | "SwitchSummonPhase" | "PartyHealPhase" + | "FormChangePhase" | "EvolutionPhase" | "EndEvolutionPhase" | "LevelCapPhase" @@ -167,7 +169,8 @@ type PhaseString = | "PostMysteryEncounterPhase" | "ModifierRewardPhase" | "PartyExpPhase" - | "ExpPhase"; + | "ExpPhase" + | "EncounterPhase"; type PhaseInterceptorPhase = PhaseClass | PhaseString; @@ -187,12 +190,16 @@ export default class PhaseInterceptor { /** * List of phases with their corresponding start methods. + * + * CAUTION: If a phase and its subclasses (if any) both appear in this list, + * make sure that this list contains said phase AFTER all of its subclasses. + * This way, the phase's `prototype.start` is properly preserved during + * `initPhases()` so that its subclasses can use `super.start()` properly. */ private PHASES = [ [ LoginPhase, this.startPhase ], [ TitlePhase, this.startPhase ], [ SelectGenderPhase, this.startPhase ], - [ EncounterPhase, this.startPhase ], [ NewBiomeEncounterPhase, this.startPhase ], [ SelectStarterPhase, this.startPhase ], [ PostSummonPhase, this.startPhase ], @@ -227,6 +234,7 @@ export default class PhaseInterceptor { [ SwitchPhase, this.startPhase ], [ SwitchSummonPhase, this.startPhase ], [ PartyHealPhase, this.startPhase ], + [ FormChangePhase, this.startPhase ], [ EvolutionPhase, this.startPhase ], [ EndEvolutionPhase, this.startPhase ], [ LevelCapPhase, this.startPhase ], @@ -240,6 +248,7 @@ export default class PhaseInterceptor { [ ModifierRewardPhase, this.startPhase ], [ PartyExpPhase, this.startPhase ], [ ExpPhase, this.startPhase ], + [ EncounterPhase, this.startPhase ], ]; private endBySetMode = [ From 88789c685e376e810f590713890c79bc1c2fd3ba Mon Sep 17 00:00:00 2001 From: "Adrian T." <68144167+torranx@users.noreply.github.com> Date: Wed, 6 Nov 2024 00:08:55 +0800 Subject: [PATCH 28/98] [Documentation] Flag Gulp Missile with edge case (#4800) --- src/data/ability.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 8eeb5af52b8..e8a242ae3ea 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -6099,7 +6099,8 @@ export function initAbilities() { .attr(NoFusionAbilityAbAttr) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .bypassFaint(), + .bypassFaint() + .edgeCase(), // Soft-locks the game if a form-changed Cramorant and its attacker both faint at the same time (ex. using Self-Destruct) new Ability(Abilities.STALWART, 8) .attr(BlockRedirectAbAttr), new Ability(Abilities.STEAM_ENGINE, 8) From 6fd3ba284c0df0926460b81855c68e4c380685f6 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:35:43 -0800 Subject: [PATCH 29/98] [Move][Ability] Implement Commander and Order Up (#4670) * Implement Order Up (mostly untested) * Commander unit tests + bug fixes * Implement Order Up (misnamed the other commit...) * Order Up unit tests * applying Temp's suggestions + other bugfixes * add TODO comment * Reviver Seed reapplies Commander * ESLint woes * Some animation fixes * Update locales * Order Up now uses attr option * Prevent semi-invulnerability lapsing out while Commanding * semi-invulnerability test * Add `edgeCase` --- src/battle-scene.ts | 3 + src/data/ability.ts | 42 +++- src/data/battler-tags.ts | 33 +++ src/data/move.ts | 57 ++++- .../the-winstrate-challenge-encounter.ts | 2 + src/enums/battler-tag-type.ts | 3 +- src/enums/pokemon-anim-type.ts | 12 +- src/field/pokemon.ts | 7 + src/modifier/modifier.ts | 7 + src/phases/command-phase.ts | 16 +- src/phases/encounter-phase.ts | 2 + src/phases/enemy-command-phase.ts | 7 + src/phases/move-effect-phase.ts | 5 +- src/phases/mystery-encounter-phases.ts | 1 + src/phases/pokemon-anim-phase.ts | 132 ++++++++++- src/phases/post-summon-phase.ts | 5 +- src/test/abilities/commander.test.ts | 224 ++++++++++++++++++ src/test/moves/order_up.test.ts | 85 +++++++ 18 files changed, 623 insertions(+), 20 deletions(-) create mode 100644 src/test/abilities/commander.test.ts create mode 100644 src/test/moves/order_up.test.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index d82acec1c20..d17c62c6e0e 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -96,6 +96,7 @@ import { ExpPhase } from "#app/phases/exp-phase"; import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { ExpGainsSpeed } from "#enums/exp-gains-speed"; +import { BattlerTagType } from "#enums/battler-tag-type"; import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; @@ -1278,6 +1279,8 @@ export default class BattleScene extends SceneBase { if (resetArenaState) { this.arena.resetArenaEffects(); + playerField.forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED)); + playerField.forEach((pokemon, p) => { if (pokemon.isOnField()) { this.pushPhase(new ReturnPhase(this, p)); diff --git a/src/data/ability.ts b/src/data/ability.ts index e8a242ae3ea..7d54459d7d4 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4,7 +4,7 @@ import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import { getPokemonNameWithAffix } from "../messages"; import { Weather, WeatherType } from "./weather"; -import { BattlerTag, GroundedTag } from "./battler-tags"; +import { BattlerTag, BattlerTagLapseType, GroundedTag } from "./battler-tags"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { Gender } from "./gender"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; @@ -35,6 +35,7 @@ import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { NewBattlePhase } from "#app/phases/new-battle-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; +import { PokemonAnimType } from "#enums/pokemon-anim-type"; export class Ability implements Localizable { public id: Abilities; @@ -2591,6 +2592,42 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr { } } +/** + * Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander}. + * When the source of an ability with this attribute detects a Dondozo as their active ally, the source "jumps + * into the Dondozo's mouth," sharply boosting the Dondozo's stats, cancelling the source's moves, and + * causing attacks that target the source to always miss. + */ +export class CommanderAbAttr extends AbAttr { + constructor() { + super(true); + } + + override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: null, args: any[]): boolean { + // TODO: Should this work with X + Dondozo fusions? + if (pokemon.scene.currentBattle?.double && pokemon.getAlly()?.species.speciesId === Species.DONDOZO) { + // If the ally Dondozo is fainted or was previously "commanded" by + // another Pokemon, this effect cannot apply. + if (pokemon.getAlly().isFainted() || pokemon.getAlly().getTag(BattlerTagType.COMMANDED)) { + return false; + } + + if (!simulated) { + // Lapse the source's semi-invulnerable tags (to avoid visual inconsistencies) + pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); + // Play an animation of the source jumping into the ally Dondozo's mouth + pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_APPLY); + // Apply boosts from this effect to the ally Dondozo + pokemon.getAlly().addTag(BattlerTagType.COMMANDED, 0, Moves.NONE, pokemon.id); + // Cancel the source Pokemon's next move (if a move is queued) + pokemon.scene.tryRemovePhase((phase) => phase instanceof MovePhase && phase.pokemon === pokemon); + } + return true; + } + return false; + } +} + export class PreSwitchOutAbAttr extends AbAttr { constructor() { super(true); @@ -6243,9 +6280,10 @@ export function initAbilities() { .attr(PreSwitchOutFormChangeAbAttr, (pokemon) => !pokemon.isFainted() ? 1 : pokemon.formIndex) .bypassFaint(), new Ability(Abilities.COMMANDER, 9) + .attr(CommanderAbAttr) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .unimplemented(), + .edgeCase(), // Encore, Frenzy, and other non-`TURN_END` tags don't lapse correctly on the commanding Pokemon. new Ability(Abilities.ELECTROMORPHOSIS, 9) .attr(PostDefendApplyBattlerTagAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, BattlerTagType.CHARGED), new Ability(Abilities.PROTOSYNTHESIS, 9) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 15bc745ec9d..2012c4330cc 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2091,6 +2091,37 @@ export class IceFaceBlockDamageTag extends FormBlockDamageTag { } } +/** + * Battler tag indicating a Tatsugiri with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander} + * has entered the tagged Pokemon's mouth. + */ +export class CommandedTag extends BattlerTag { + private _tatsugiriFormKey: string; + + constructor(sourceId: number) { + super(BattlerTagType.COMMANDED, BattlerTagLapseType.CUSTOM, 0, Moves.NONE, sourceId); + } + + public get tatsugiriFormKey(): string { + return this._tatsugiriFormKey; + } + + /** Caches the Tatsugiri's form key and sharply boosts the tagged Pokemon's stats */ + override onAdd(pokemon: Pokemon): void { + this._tatsugiriFormKey = this.getSourcePokemon(pokemon.scene)?.getFormKey() ?? "curly"; + pokemon.scene.unshiftPhase(new StatStageChangePhase( + pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2 + )); + } + + /** Triggers an {@linkcode PokemonAnimType | animation} of the tagged Pokemon "spitting out" Tatsugiri */ + override onRemove(pokemon: Pokemon): void { + if (this.getSourcePokemon(pokemon.scene)?.isActive(true)) { + pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_REMOVE); + } + } +} + /** * Battler tag enabling the Stockpile mechanic. This tag handles: * - Stack tracking, including max limit enforcement (which is replicated in Stockpile for redundancy). @@ -2932,6 +2963,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new IceFaceBlockDamageTag(tagType); case BattlerTagType.DISGUISE: return new FormBlockDamageTag(tagType); + case BattlerTagType.COMMANDED: + return new CommandedTag(sourceId); case BattlerTagType.STOCKPILING: return new StockpilingTag(sourceMove); case BattlerTagType.OCTOLOCK: diff --git a/src/data/move.ts b/src/data/move.ts index 5ae37c3b092..c97749e0357 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,5 +1,5 @@ import { ChargeAnim, initMoveAnim, loadMoveAnimAssets, MoveChargeAnim } from "./battle-anims"; -import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags"; +import { CommandedTag, EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; import { getNonVolatileStatusEffects, getStatusEffectHealText, isNonVolatileStatusEffect, StatusEffect } from "./status-effect"; @@ -714,6 +714,10 @@ export default class Move implements Localizable { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { let score = 0; + if (target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon(target.scene) === target) { + return 20 * (target.isPlayer() === user.isPlayer() ? -1 : 1); // always -20 with how the AI handles this score + } + for (const attr of this.attrs) { // conditionals to check if the move is self targeting (if so then you are applying the move to yourself, not the target) score += attr.getTargetBenefitScore(user, !attr.selfTarget ? target : user, move) * (target !== user && attr.selfTarget ? -1 : 1); @@ -3245,6 +3249,41 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr { } } +/** + * Attribute implementing the stat boosting effect of {@link https://bulbapedia.bulbagarden.net/wiki/Order_Up_(move) | Order Up}. + * If the user has a Pokemon with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander} in their mouth, + * one of the user's stats are increased by 1 stage, depending on the "commanding" Pokemon's form. This effect does not respect + * effect chance, but Order Up itself may be boosted by Sheer Force. + */ +export class OrderUpStatBoostAttr extends MoveEffectAttr { + constructor() { + super(true, { trigger: MoveEffectTrigger.HIT }); + } + + override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { + const commandedTag = user.getTag(CommandedTag); + if (!commandedTag) { + return false; + } + + let increasedStat: EffectiveStat = Stat.ATK; + switch (commandedTag.tatsugiriFormKey) { + case "curly": + increasedStat = Stat.ATK; + break; + case "droopy": + increasedStat = Stat.DEF; + break; + case "stretchy": + increasedStat = Stat.SPD; + break; + } + + user.scene.unshiftPhase(new StatStageChangePhase(user.scene, user.getBattlerIndex(), this.selfTarget, [ increasedStat ], 1)); + return true; + } +} + export class CopyStatsAttr extends MoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (!super.apply(user, target, move, args)) { @@ -5852,7 +5891,13 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return false; } + /** The {@linkcode Pokemon} to be switched out with this effect */ const switchOutTarget = this.selfSwitch ? user : target; + + // If the switch-out target is a Dondozo with a Tatsugiri in its mouth + // (e.g. when it uses Flip Turn), make it spit out the Tatsugiri before switching out. + switchOutTarget.lapseTag(BattlerTagType.COMMANDED); + if (switchOutTarget instanceof PlayerPokemon) { /** * Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch @@ -5955,6 +6000,12 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return false; } + // Dondozo with an allied Tatsugiri in its mouth cannot be forced out + const commandedTag = switchOutTarget.getTag(BattlerTagType.COMMANDED); + if (commandedTag?.getSourcePokemon(switchOutTarget.scene)?.isActive(true)) { + return false; + } + if (!player && user.scene.currentBattle.isBattleMysteryEncounter() && !user.scene.currentBattle.mysteryEncounter?.fleeAllowed) { // Don't allow wild opponents to be force switched during MEs with flee disabled return false; @@ -10300,8 +10351,8 @@ export function initMoves() { new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), new AttackMove(Moves.ORDER_UP, Type.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9) - .makesContact(false) - .partial(), // No effect implemented (requires Commander) + .attr(OrderUpStatBoostAttr) + .makesContact(false), new AttackMove(Moves.JET_PUNCH, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 15, -1, 1, 9) .punchingMove(), new StatusMove(Moves.SPICY_EXTRACT, Type.GRASS, -1, 15, -1, 0, 9) diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index 7d9b531c9ab..5c1af43c426 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -23,6 +23,7 @@ import { ReturnPhase } from "#app/phases/return-phase"; import i18next from "i18next"; import { ModifierTier } from "#app/modifier/modifier-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { BattlerTagType } from "#enums/battler-tag-type"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theWinstrateChallenge"; @@ -187,6 +188,7 @@ function endTrainerBattleAndShowDialogue(scene: BattleScene): Promise { } else { scene.arena.resetArenaEffects(); const playerField = scene.getPlayerField(); + playerField.forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED)); playerField.forEach((_, p) => scene.unshiftPhase(new ReturnPhase(scene, p))); for (const pokemon of scene.getPlayerParty()) { diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 0eace9a1607..c53157c68d4 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -88,5 +88,6 @@ export enum BattlerTagType { IMPRISON = "IMPRISON", SYRUP_BOMB = "SYRUP_BOMB", ELECTRIFIED = "ELECTRIFIED", - TELEKINESIS = "TELEKINESIS" + TELEKINESIS = "TELEKINESIS", + COMMANDED = "COMMANDED" } diff --git a/src/enums/pokemon-anim-type.ts b/src/enums/pokemon-anim-type.ts index 5a0a0c2f622..b153fb2e652 100644 --- a/src/enums/pokemon-anim-type.ts +++ b/src/enums/pokemon-anim-type.ts @@ -12,5 +12,15 @@ export enum PokemonAnimType { * Removes a Pokemon's Substitute doll from the field. * The Pokemon then moves back to its original position. */ - SUBSTITUTE_REMOVE + SUBSTITUTE_REMOVE, + /** + * Brings Tatsugiri and Dondozo to the center of the field, with + * Tatsugiri jumping into the Dondozo's mouth + */ + COMMANDER_APPLY, + /** + * Dondozo "spits out" Tatsugiri, moving Tatsugiri back to its original + * field position. + */ + COMMANDER_REMOVE } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 78f82e929de..77881d36c56 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1563,6 +1563,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @returns `true` if the pokemon is trapped */ public isTrapped(trappedAbMessages: string[] = [], simulated: boolean = true): boolean { + const commandedTag = this.getTag(BattlerTagType.COMMANDED); + if (commandedTag?.getSourcePokemon(this.scene)?.isActive(true)) { + return true; + } + if (this.isOfType(Type.GHOST)) { return false; } @@ -2914,6 +2919,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo)); } this.destroySubstitute(); + this.lapseTag(BattlerTagType.COMMANDED); this.resetSummonData(); } @@ -2962,6 +2968,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.setPhaseQueueSplice(); this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure)); this.destroySubstitute(); + this.lapseTag(BattlerTagType.COMMANDED); this.resetSummonData(); } return damage; diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 0891262649c..c3b1f15fab4 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -31,6 +31,7 @@ import i18next from "i18next"; import { type DoubleBattleChanceBoosterModifierType, type EvolutionItemModifierType, type FormChangeItemModifierType, type ModifierOverride, type ModifierType, type PokemonBaseStatTotalModifierType, type PokemonExpBoosterModifierType, type PokemonFriendshipBoosterModifierType, type PokemonMoveAccuracyBoosterModifierType, type PokemonMultiHitModifierType, type TerastallizeModifierType, type TmModifierType, getModifierType, ModifierPoolType, ModifierTypeGenerator, modifierTypes, PokemonHeldItemModifierType } from "./modifier-type"; import { Color, ShadowColor } from "#enums/color"; import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters"; +import { applyAbAttrs, CommanderAbAttr } from "#app/data/ability"; export type ModifierPredicate = (modifier: Modifier) => boolean; @@ -1937,10 +1938,16 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { * @returns always `true` */ override apply(pokemon: Pokemon): boolean { + // Restore the Pokemon to half HP pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), toDmgValue(pokemon.getMaxHp() / 2), i18next.t("modifier:pokemonInstantReviveApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), false, false, true)); + // Remove the Pokemon's FAINT status pokemon.resetStatus(true, false, true); + + // Reapply Commander on the Pokemon's side of the field, if applicable + const field = pokemon.isPlayer() ? pokemon.scene.getPlayerField() : pokemon.scene.getEnemyField(); + field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false)); return true; } diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index c1d38815c2f..b99eb8d1b2e 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -54,6 +54,11 @@ export class CommandPhase extends FieldPhase { } } + // If the Pokemon has applied Commander's effects to its ally, skip this command + if (this.scene.currentBattle?.double && this.getPokemon().getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon(this.scene) === this.getPokemon()) { + this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.FIGHT, move: { move: Moves.NONE, targets: []}, skip: true }; + } + if (this.scene.currentBattle.turnCommands[this.fieldIndex]?.skip) { return this.end(); } @@ -92,7 +97,7 @@ export class CommandPhase extends FieldPhase { handleCommand(command: Command, cursor: integer, ...args: any[]): boolean { const playerPokemon = this.scene.getPlayerField()[this.fieldIndex]; - let success: boolean; + let success: boolean = false; switch (command) { case Command.FIGHT: @@ -232,11 +237,8 @@ export class CommandPhase extends FieldPhase { const trapTag = playerPokemon.getTag(TrappedTag); const fairyLockTag = playerPokemon.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER); - // trapTag should be defined at this point, but just in case... if (!trapTag && !fairyLockTag) { - currentBattle.turnCommands[this.fieldIndex] = isSwitch - ? { command: Command.POKEMON, cursor: cursor, args: args } - : { command: Command.RUN }; + i18next.t(`battle:noEscape${isSwitch ? "Switch" : "Flee"}`); break; } if (!isSwitch) { @@ -272,11 +274,11 @@ export class CommandPhase extends FieldPhase { break; } - if (success!) { // TODO: is the bang correct? + if (success) { this.end(); } - return success!; // TODO: is the bang correct? + return success; } cancel() { diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index c4d919c0325..123f9ded9fc 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -36,6 +36,7 @@ import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import i18next from "i18next"; import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; +import { BattlerTagType } from "#enums/battler-tag-type"; export class EncounterPhase extends BattlePhase { private loaded: boolean; @@ -482,6 +483,7 @@ export class EncounterPhase extends BattlePhase { } } else { if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) { + this.scene.getPlayerField().forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED)); this.scene.pushPhase(new ReturnPhase(this.scene, 1)); } this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, false)); diff --git a/src/phases/enemy-command-phase.ts b/src/phases/enemy-command-phase.ts index 3647a237ef1..83a85009ae0 100644 --- a/src/phases/enemy-command-phase.ts +++ b/src/phases/enemy-command-phase.ts @@ -2,6 +2,8 @@ import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; import { Command } from "#app/ui/command-ui-handler"; import { FieldPhase } from "./field-phase"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; /** * Phase for determining an enemy AI's action for the next turn. @@ -34,6 +36,11 @@ export class EnemyCommandPhase extends FieldPhase { const trainer = battle.trainer; + if (battle.double && enemyPokemon.hasAbility(Abilities.COMMANDER) + && enemyPokemon.getAlly().getTag(BattlerTagType.COMMANDED)) { + this.skipTurn = true; + } + /** * If the enemy has a trainer, decide whether or not the enemy should switch * to another member in its party. diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 62ece3769b5..48150ffaf05 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -211,11 +211,14 @@ export class MoveEffectPhase extends PokemonPhase { && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) && !target.getTag(SemiInvulnerableTag); + /** Is the target hidden by the effects of its Commander ability? */ + const isCommanding = this.scene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon(this.scene) === target; + /** * If the move missed a target, stop all future hits against that target * and move on to the next target (if there is one). */ - if (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()]) { + if (isCommanding || (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()])) { this.stopMultiHit(target); this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); if (moveHistoryEntry.result === MoveResult.PENDING) { diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index e7d1f7e9074..2d1c3c4ae31 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -417,6 +417,7 @@ export class MysteryEncounterBattlePhase extends Phase { } } else { if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) { + scene.getPlayerField().forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED)); scene.pushPhase(new ReturnPhase(scene, 1)); } scene.pushPhase(new ToggleDoublePositionPhase(scene, false)); diff --git a/src/phases/pokemon-anim-phase.ts b/src/phases/pokemon-anim-phase.ts index 26ae11d1026..e9e1129f158 100644 --- a/src/phases/pokemon-anim-phase.ts +++ b/src/phases/pokemon-anim-phase.ts @@ -3,6 +3,7 @@ import { SubstituteTag } from "#app/data/battler-tags"; import { PokemonAnimType } from "#enums/pokemon-anim-type"; import Pokemon from "#app/field/pokemon"; import { BattlePhase } from "#app/phases/battle-phase"; +import { Species } from "#enums/species"; export class PokemonAnimPhase extends BattlePhase { @@ -37,12 +38,18 @@ export class PokemonAnimPhase extends BattlePhase { case PokemonAnimType.SUBSTITUTE_REMOVE: this.doSubstituteRemoveAnim(); break; + case PokemonAnimType.COMMANDER_APPLY: + this.doCommanderApplyAnim(); + break; + case PokemonAnimType.COMMANDER_REMOVE: + this.doCommanderRemoveAnim(); + break; default: this.end(); } } - doSubstituteAddAnim(): void { + private doSubstituteAddAnim(): void { const substitute = this.pokemon.getTag(SubstituteTag); if (substitute === null) { return this.end(); @@ -106,7 +113,7 @@ export class PokemonAnimPhase extends BattlePhase { }); } - doSubstitutePreMoveAnim(): void { + private doSubstitutePreMoveAnim(): void { if (this.fieldAssets.length !== 1) { return this.end(); } @@ -135,7 +142,7 @@ export class PokemonAnimPhase extends BattlePhase { }); } - doSubstitutePostMoveAnim(): void { + private doSubstitutePostMoveAnim(): void { if (this.fieldAssets.length !== 1) { return this.end(); } @@ -164,7 +171,7 @@ export class PokemonAnimPhase extends BattlePhase { }); } - doSubstituteRemoveAnim(): void { + private doSubstituteRemoveAnim(): void { if (this.fieldAssets.length !== 1) { return this.end(); } @@ -233,4 +240,121 @@ export class PokemonAnimPhase extends BattlePhase { } }); } + + private doCommanderApplyAnim(): void { + if (!this.scene.currentBattle?.double) { + return this.end(); + } + const dondozo = this.pokemon.getAlly(); + + if (dondozo?.species?.speciesId !== Species.DONDOZO) { + return this.end(); + } + + const tatsugiriX = this.pokemon.x + this.pokemon.getSprite().x; + const tatsugiriY = this.pokemon.y + this.pokemon.getSprite().y; + + const getSourceSprite = () => { + const sprite = this.scene.addPokemonSprite(this.pokemon, tatsugiriX, tatsugiriY, this.pokemon.getSprite().texture, this.pokemon.getSprite()!.frame.name, true); + [ "spriteColors", "fusionSpriteColors" ].map(k => sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k]); + sprite.setPipelineData("spriteKey", this.pokemon.getBattleSpriteKey()); + sprite.setPipelineData("shiny", this.pokemon.shiny); + sprite.setPipelineData("variant", this.pokemon.variant); + sprite.setPipelineData("ignoreFieldPos", true); + sprite.setOrigin(0.5, 1); + this.pokemon.getSprite().on("animationupdate", (_anim, frame) => sprite.setFrame(frame.textureFrame)); + this.scene.field.add(sprite); + return sprite; + }; + + const sourceSprite = getSourceSprite(); + + this.pokemon.setVisible(false); + + const sourceFpOffset = this.pokemon.getFieldPositionOffset(); + const dondozoFpOffset = dondozo.getFieldPositionOffset(); + + this.scene.playSound("se/pb_throw"); + + this.scene.tweens.add({ + targets: sourceSprite, + duration: 375, + scale: 0.5, + x: { value: tatsugiriX + (dondozoFpOffset[0] - sourceFpOffset[0]) / 2, ease: "Linear" }, + y: { value: (this.pokemon.isPlayer() ? 100 : 65) + sourceFpOffset[1], ease: "Sine.easeOut" }, + onComplete: () => { + this.scene.field.bringToTop(dondozo); + this.scene.tweens.add({ + targets: sourceSprite, + duration: 375, + scale: 0.01, + x: { value: dondozo.x, ease: "Linear" }, + y: { value: dondozo.y + dondozo.height / 2, ease: "Sine.easeIn" }, + onComplete: () => { + sourceSprite.destroy(); + this.scene.playSound("battle_anims/PRSFX- Liquidation1.wav"); + this.scene.tweens.add({ + targets: dondozo, + duration: 250, + ease: "Sine.easeInOut", + scale: 0.85, + yoyo: true, + onComplete: () => this.end() + }); + } + }); + } + }); + } + + private doCommanderRemoveAnim(): void { + // Note: unlike the other Commander animation, this is played through the + // Dondozo instead of the Tatsugiri. + const tatsugiri = this.pokemon.getAlly(); + + const tatsuSprite = this.scene.addPokemonSprite( + tatsugiri, + this.pokemon.x + this.pokemon.getSprite().x, + this.pokemon.y + this.pokemon.getSprite().y + this.pokemon.height / 2, + tatsugiri.getSprite().texture, + tatsugiri.getSprite()!.frame.name, + true + ); + [ "spriteColors", "fusionSpriteColors" ].map(k => tatsuSprite.pipelineData[k] = tatsugiri.getSprite().pipelineData[k]); + tatsuSprite.setPipelineData("spriteKey", tatsugiri.getBattleSpriteKey()); + tatsuSprite.setPipelineData("shiny", tatsugiri.shiny); + tatsuSprite.setPipelineData("variant", tatsugiri.variant); + tatsuSprite.setPipelineData("ignoreFieldPos", true); + this.pokemon.getSprite().on("animationupdate", (_anim, frame) => tatsuSprite.setFrame(frame.textureFrame)); + + tatsuSprite.setOrigin(0.5, 1); + tatsuSprite.setScale(0.01); + + this.scene.field.add(tatsuSprite); + this.scene.field.bringToTop(this.pokemon); + tatsuSprite.setVisible(true); + + this.scene.tweens.add({ + targets: this.pokemon, + duration: 250, + ease: "Sine.easeInOut", + scale: 1.15, + yoyo: true, + onComplete: () => { + this.scene.playSound("battle_anims/PRSFX- Liquidation4.wav"); + this.scene.tweens.add({ + targets: tatsuSprite, + duration: 500, + scale: 1, + x: { value: tatsugiri.x + tatsugiri.getSprite().x, ease: "Linear" }, + y: { value: tatsugiri.y + tatsugiri.getSprite().y, ease: "Sine.easeIn" }, + onComplete: () => { + tatsugiri.setVisible(true); + tatsuSprite.destroy(); + this.end(); + } + }); + } + }); + } } diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 3db98d9926c..644a6235a42 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -1,6 +1,6 @@ import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { applyPostSummonAbAttrs, PostSummonAbAttr } from "#app/data/ability"; +import { applyAbAttrs, applyPostSummonAbAttrs, CommanderAbAttr, PostSummonAbAttr } from "#app/data/ability"; import { ArenaTrapTag } from "#app/data/arena-tag"; import { StatusEffect } from "#app/enums/status-effect"; import { PokemonPhase } from "./pokemon-phase"; @@ -28,5 +28,8 @@ export class PostSummonPhase extends PokemonPhase { } applyPostSummonAbAttrs(PostSummonAbAttr, pokemon).then(() => this.end()); + + const field = pokemon.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField(); + field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false)); } } diff --git a/src/test/abilities/commander.test.ts b/src/test/abilities/commander.test.ts new file mode 100644 index 00000000000..99b3d50797c --- /dev/null +++ b/src/test/abilities/commander.test.ts @@ -0,0 +1,224 @@ +import { BattlerIndex } from "#app/battle"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { PokemonAnimType } from "#enums/pokemon-anim-type"; +import { EffectiveStat, Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; +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 Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Commander", () => { + 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 + .startingLevel(100) + .enemyLevel(100) + .moveset([ Moves.LIQUIDATION, Moves.MEMENTO, Moves.SPLASH, Moves.FLIP_TURN ]) + .ability(Abilities.COMMANDER) + .battleType("double") + .disableCrits() + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + + vi.spyOn(game.scene, "triggerPokemonBattleAnim").mockReturnValue(true); + }); + + it("causes the source to jump into Dondozo's mouth, granting a stat boost and hiding the source", async () => { + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + const affectedStats: EffectiveStat[] = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ]; + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + affectedStats.forEach((stat) => expect(dondozo.getStatStage(stat)).toBe(2)); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + // Force both enemies to target the Tatsugiri + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + + await game.phaseInterceptor.to("BerryPhase", false); + game.scene.getEnemyField().forEach(enemy => expect(enemy.getLastXMoves(1)[0].result).toBe(MoveResult.MISS)); + expect(tatsugiri.isFullHp()).toBeTruthy(); + }); + + it("should activate when a Dondozo switches in and cancel the source's move", async () => { + game.override.enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.MAGIKARP, Species.DONDOZO ]); + + const tatsugiri = game.scene.getPlayerField()[0]; + + game.move.select(Moves.LIQUIDATION, 0, BattlerIndex.ENEMY); + game.doSwitchPokemon(2); + + await game.phaseInterceptor.to("MovePhase", false); + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + + const dondozo = game.scene.getPlayerField()[1]; + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(tatsugiri.getMoveHistory()).toHaveLength(0); + expect(game.scene.getEnemyField()[0].isFullHp()).toBeTruthy(); + }); + + it("source should reenter the field when Dondozo faints", async () => { + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + game.move.select(Moves.MEMENTO, 1, BattlerIndex.ENEMY); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + + await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER ]); + + await game.phaseInterceptor.to("FaintPhase", false); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeUndefined(); + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(dondozo, PokemonAnimType.COMMANDER_REMOVE); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(tatsugiri.isFullHp()).toBeFalsy(); + }); + + it("source should still take damage from Poison while hidden", async () => { + game.override + .statusEffect(StatusEffect.POISON) + .enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(tatsugiri.isFullHp()).toBeFalsy(); + }); + + it("source should still take damage from Salt Cure while hidden", async () => { + game.override.enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + tatsugiri.addTag(BattlerTagType.SALT_CURED, 0, Moves.NONE, game.scene.getField()[BattlerIndex.ENEMY].id); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(tatsugiri.isFullHp()).toBeFalsy(); + }); + + it("source should still take damage from Sandstorm while hidden", async () => { + game.override + .weather(WeatherType.SANDSTORM) + .enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(tatsugiri.isFullHp()).toBeFalsy(); + }); + + it("should make Dondozo immune to being forced out", async () => { + game.override.enemyMoveset([ Moves.SPLASH, Moves.WHIRLWIND ]); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.forceEnemyMove(Moves.WHIRLWIND, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + + // Test may time out here if Whirlwind forced out a Pokemon + await game.phaseInterceptor.to("TurnEndPhase"); + expect(dondozo.isActive(true)).toBeTruthy(); + }); + + it("should interrupt the source's semi-invulnerability", async () => { + game.override + .moveset([ Moves.SPLASH, Moves.DIVE ]) + .enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.MAGIKARP, Species.DONDOZO ]); + + const tatsugiri = game.scene.getPlayerField()[0]; + + game.move.select(Moves.DIVE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + + await game.phaseInterceptor.to("CommandPhase"); + await game.toNextTurn(); + + expect(tatsugiri.getTag(BattlerTagType.UNDERWATER)).toBeDefined(); + game.doSwitchPokemon(2); + + await game.phaseInterceptor.to("MovePhase", false); + const dondozo = game.scene.getPlayerField()[1]; + expect(tatsugiri.getTag(BattlerTagType.UNDERWATER)).toBeUndefined(); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + await game.toNextTurn(); + const enemy = game.scene.getEnemyField()[0]; + expect(enemy.isFullHp()).toBeTruthy(); + }); +}); diff --git a/src/test/moves/order_up.test.ts b/src/test/moves/order_up.test.ts new file mode 100644 index 00000000000..d0b52dc1a9d --- /dev/null +++ b/src/test/moves/order_up.test.ts @@ -0,0 +1,85 @@ +import { BattlerIndex } from "#app/battle"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { PokemonAnimType } from "#enums/pokemon-anim-type"; +import { EffectiveStat, 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, vi } from "vitest"; + +describe("Moves - Order Up", () => { + 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.ORDER_UP) + .ability(Abilities.COMMANDER) + .battleType("double") + .disableCrits() + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingLevel(100) + .enemyLevel(100); + + vi.spyOn(game.scene, "triggerPokemonBattleAnim").mockReturnValue(true); + }); + + it.each([ + { formIndex: 0, formName: "Curly", stat: Stat.ATK, statName: "Attack" }, + { formIndex: 1, formName: "Droopy", stat: Stat.DEF, statName: "Defense" }, + { formIndex: 2, formName: "Stretchy", stat: Stat.SPD, statName: "Speed" } + ])("should raise the user's $statName when the user is commanded by a $formName Tatsugiri", async ({ formIndex, stat }) => { + game.override.starterForms({ [Species.TATSUGIRI]: formIndex }); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + game.move.select(Moves.ORDER_UP, 1, BattlerIndex.ENEMY); + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("BerryPhase", false); + + const affectedStats: EffectiveStat[] = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ]; + affectedStats.forEach(st => expect(dondozo.getStatStage(st)).toBe(st === stat ? 3 : 2)); + }); + + it("should be boosted by Sheer Force while still applying a stat boost", async () => { + game.override + .passiveAbility(Abilities.SHEER_FORCE) + .starterForms({ [Species.TATSUGIRI]: 0 }); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + game.move.select(Moves.ORDER_UP, 1, BattlerIndex.ENEMY); + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(dondozo.battleData.abilitiesApplied.includes(Abilities.SHEER_FORCE)).toBeTruthy(); + expect(dondozo.getStatStage(Stat.ATK)).toBe(3); + }); +}); From 198d3ce2a6bce6d25748bd961b7cab1659b78d27 Mon Sep 17 00:00:00 2001 From: geeilhan <107366005+geeilhan@users.noreply.github.com> Date: Tue, 5 Nov 2024 18:37:47 +0100 Subject: [PATCH 30/98] [P2] Fixing Incorrect Freeze Dry Interaction With Soaked Wonder Guard Target (#4798) * [Bug] fixed interaction between freeze dry and soaked wonder guard target * added automated test for freeze dry interaction with soaked wonder guard target * using parameter instead of function to get typeMultiplier --- src/data/ability.ts | 6 +++++- src/test/moves/freeze_dry.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 7d54459d7d4..6a3e8da3cd4 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -512,7 +512,11 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr { } applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { - if (move instanceof AttackMove && pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker) < 2) { + const modifierValue = args.length > 0 + ? (args[0] as Utils.NumberHolder).value + : pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker); + + if (move instanceof AttackMove && modifierValue < 2) { cancelled.value = true; // Suppresses "No Effect" message (args[0] as Utils.NumberHolder).value = 0; return true; diff --git a/src/test/moves/freeze_dry.test.ts b/src/test/moves/freeze_dry.test.ts index c09d12fa597..f766ed41a82 100644 --- a/src/test/moves/freeze_dry.test.ts +++ b/src/test/moves/freeze_dry.test.ts @@ -72,6 +72,31 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1); }); + /** + * Freeze drys forced super effectiveness should overwrite wonder guard + */ + it("should deal 2x dmg against soaked wonder guard target", async () => { + game.override + .enemySpecies(Species.SHEDINJA) + .enemyMoveset(Moves.SPLASH) + .starterSpecies(Species.MAGIKARP) + .moveset([ Moves.SOAK, Moves.FREEZE_DRY ]); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.SOAK); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + + game.move.select(Moves.FREEZE_DRY); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + }); + // enable if this is ever fixed (lol) it.todo("should deal 2x damage to water types under Normalize", async () => { game.override.ability(Abilities.NORMALIZE); From b79b643b317e898b35b84c2e368b24d22f865346 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Tue, 5 Nov 2024 18:42:04 +0100 Subject: [PATCH 31/98] [P2][ME] Fix GTS not getting proper item tier when trading items (#4802) --- .../encounters/global-trade-system-encounter.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 90f6ee942be..720e904ecdb 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -367,10 +367,11 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter!; - const modifier = encounter.misc.chosenModifier; + const modifier = encounter.misc.chosenModifier as PokemonHeldItemModifier; + const party = scene.getPlayerParty(); // Check tier of the traded item, the received item will be one tier up - const type = modifier.type.withTierFromPool(); + const type = modifier.type.withTierFromPool(ModifierPoolType.PLAYER, party); let tier = type.tier ?? ModifierTier.GREAT; // Eggs and White Herb are not in the pool if (type.id === "WHITE_HERB") { @@ -385,11 +386,11 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = tier++; } - regenerateModifierPoolThresholds(scene.getPlayerParty(), ModifierPoolType.PLAYER, 0); + regenerateModifierPoolThresholds(party, ModifierPoolType.PLAYER, 0); let item: ModifierTypeOption | null = null; // TMs excluded from possible rewards while (!item || item.type.id.includes("TM_")) { - item = getPlayerModifierTypeOptions(1, scene.getPlayerParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; + item = getPlayerModifierTypeOptions(1, party, [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; } encounter.setDialogueToken("itemName", item.type.name); From eb32545772da2d6a0cf133989ff1424cf1527a4d Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:23:22 -0800 Subject: [PATCH 32/98] [Balance][Beta] Commander now increases double battle chance (#4803) --- src/data/ability.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/ability.ts b/src/data/ability.ts index 6a3e8da3cd4..4752217da19 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -6285,6 +6285,7 @@ export function initAbilities() { .bypassFaint(), new Ability(Abilities.COMMANDER, 9) .attr(CommanderAbAttr) + .attr(DoubleBattleChanceAbAttr) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .edgeCase(), // Encore, Frenzy, and other non-`TURN_END` tags don't lapse correctly on the commanding Pokemon. From 4f733796c5394c8060f8736b065d5929162384f6 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Tue, 5 Nov 2024 18:32:07 -0800 Subject: [PATCH 33/98] [Move] Implement Grudge (#4794) * some work * slay dnr * Fixed up move mechanics * bahhh * yawn * updated lapse type to correctness * Test + documentation * yattt * Remove some redundant code * Apply suggestions from code review Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Update battler-tags.ts * Fix `PokemonAnimPhase` --------- Co-authored-by: frutescens Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/data/battler-tags.ts | 41 +++++++++++++++ src/data/move.ts | 2 +- src/enums/battler-tag-type.ts | 3 +- src/field/pokemon.ts | 19 +++---- src/phases/faint-phase.ts | 18 +++++-- src/phases/pokemon-anim-phase.ts | 5 +- src/test/moves/grudge.test.ts | 90 ++++++++++++++++++++++++++++++++ 7 files changed, 159 insertions(+), 19 deletions(-) create mode 100644 src/test/moves/grudge.test.ts diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 2012c4330cc..473974048c9 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2827,6 +2827,45 @@ export class PowerTrickTag extends BattlerTag { } } +/** + * Tag associated with the move Grudge. + * If this tag is active when the bearer faints from an opponent's move, the tag reduces that move's PP to 0. + * Otherwise, it lapses when the bearer makes another move. + */ +export class GrudgeTag extends BattlerTag { + constructor() { + super(BattlerTagType.GRUDGE, [ BattlerTagLapseType.CUSTOM, BattlerTagLapseType.PRE_MOVE ], 1, Moves.GRUDGE); + } + + onAdd(pokemon: Pokemon) { + super.onAdd(pokemon); + pokemon.scene.queueMessage(i18next.t("battlerTags:grudgeOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); + } + + /** + * Activates Grudge's special effect on the attacking Pokemon and lapses the tag. + * @param pokemon + * @param lapseType + * @param sourcePokemon {@linkcode Pokemon} the source of the move that fainted the tag's bearer + * @returns `false` if Grudge activates its effect or lapses + */ + override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType, sourcePokemon?: Pokemon): boolean { + if (lapseType === BattlerTagLapseType.CUSTOM && sourcePokemon) { + if (sourcePokemon.isActive() && pokemon.isOpponent(sourcePokemon)) { + const lastMove = pokemon.turnData.attacksReceived[0]; + const lastMoveData = sourcePokemon.getMoveset().find(m => m?.moveId === lastMove.move); + if (lastMoveData && lastMove.move !== Moves.STRUGGLE) { + lastMoveData.ppUsed = lastMoveData.getMovePp(); + pokemon.scene.queueMessage(i18next.t("battlerTags:grudgeLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: lastMoveData.getName() })); + } + } + return false; + } else { + return super.lapse(pokemon, lapseType); + } + } +} + /** * 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 @@ -3008,6 +3047,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new TelekinesisTag(sourceMove); case BattlerTagType.POWER_TRICK: return new PowerTrickTag(sourceMove, sourceId); + case BattlerTagType.GRUDGE: + return new GrudgeTag(); 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 c97749e0357..fb09d822a1d 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8489,7 +8489,7 @@ export function initMoves() { .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN) .condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)), new SelfStatusMove(Moves.GRUDGE, Type.GHOST, -1, 5, -1, 0, 3) - .unimplemented(), + .attr(AddBattlerTagAttr, BattlerTagType.GRUDGE, true, undefined, 1), new SelfStatusMove(Moves.SNATCH, Type.DARK, -1, 10, -1, 4, 3) .unimplemented(), new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3) diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index c53157c68d4..660a47e0d68 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -89,5 +89,6 @@ export enum BattlerTagType { SYRUP_BOMB = "SYRUP_BOMB", ELECTRIFIED = "ELECTRIFIED", TELEKINESIS = "TELEKINESIS", - COMMANDED = "COMMANDED" + COMMANDED = "COMMANDED", + GRUDGE = "GRUDGE" } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 77881d36c56..25a771c9281 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -7,7 +7,7 @@ import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, Varia import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { starterPassiveAbilities } from "#app/data/balance/passives"; -import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; +import { Constructor, isNullOrUndefined, randSeedInt, type nil } from "#app/utils"; import * as Utils from "#app/utils"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type"; import { getLevelTotalExp } from "#app/data/exp"; @@ -2841,6 +2841,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // In case of fatal damage, this tag would have gotten cleared before we could lapse it. const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); + const grudgeTag = this.getTag(BattlerTagType.GRUDGE); const isOneHitKo = result === HitResult.ONE_HIT_KO; @@ -2912,12 +2913,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.isFainted()) { // set splice index here, so future scene queues happen before FaintedPhase this.scene.setPhaseQueueSplice(); - if (!isNullOrUndefined(destinyTag) && dmg) { - // Destiny Bond will activate during FaintPhase - this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo, destinyTag, source)); - } else { - this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo)); - } + this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo, destinyTag, grudgeTag, source)); + this.destroySubstitute(); this.lapseTag(BattlerTagType.COMMANDED); this.resetSummonData(); @@ -3051,19 +3048,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** @overload */ - getTag(tagType: BattlerTagType): BattlerTag | null; + getTag(tagType: BattlerTagType): BattlerTag | nil; /** @overload */ - getTag(tagType: Constructor): T | null; + getTag(tagType: Constructor): T | nil; - getTag(tagType: BattlerTagType | Constructor): BattlerTag | null { + getTag(tagType: BattlerTagType | Constructor): BattlerTag | nil { if (!this.summonData) { return null; } return (tagType instanceof Function ? this.summonData.tags.find(t => t instanceof tagType) : this.summonData.tags.find(t => t.tagType === tagType) - )!; // TODO: is this bang correct? + ); } findTag(tagFilter: ((tag: BattlerTag) => boolean)) { diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 3e90233a38c..d66c5b66144 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -1,7 +1,7 @@ import { BattlerIndex, BattleType } from "#app/battle"; import BattleScene from "#app/battle-scene"; import { applyPostFaintAbAttrs, applyPostKnockOutAbAttrs, applyPostVictoryAbAttrs, PostFaintAbAttr, PostKnockOutAbAttr, PostVictoryAbAttr } from "#app/data/ability"; -import { BattlerTagLapseType, DestinyBondTag } from "#app/data/battler-tags"; +import { BattlerTagLapseType, DestinyBondTag, GrudgeTag } from "#app/data/battler-tags"; import { battleSpecDialogue } from "#app/data/dialogue"; import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; @@ -31,18 +31,24 @@ export class FaintPhase extends PokemonPhase { /** * Destiny Bond tag belonging to the currently fainting Pokemon, if applicable */ - private destinyTag?: DestinyBondTag; + private destinyTag?: DestinyBondTag | null; /** - * The source Pokemon that dealt fatal damage and should get KO'd by Destiny Bond, if applicable + * Grudge tag belonging to the currently fainting Pokemon, if applicable + */ + private grudgeTag?: GrudgeTag | null; + + /** + * The source Pokemon that dealt fatal damage */ private source?: Pokemon; - constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure: boolean = false, destinyTag?: DestinyBondTag, source?: Pokemon) { + constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure: boolean = false, destinyTag?: DestinyBondTag | null, grudgeTag?: GrudgeTag | null, source?: Pokemon) { super(scene, battlerIndex); this.preventEndure = preventEndure; this.destinyTag = destinyTag; + this.grudgeTag = grudgeTag; this.source = source; } @@ -53,6 +59,10 @@ export class FaintPhase extends PokemonPhase { this.destinyTag.lapse(this.source, BattlerTagLapseType.CUSTOM); } + if (!isNullOrUndefined(this.grudgeTag) && !isNullOrUndefined(this.source)) { + this.grudgeTag.lapse(this.getPokemon(), BattlerTagLapseType.CUSTOM, this.source); + } + if (!this.preventEndure) { const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier; diff --git a/src/phases/pokemon-anim-phase.ts b/src/phases/pokemon-anim-phase.ts index e9e1129f158..ad0be34af7d 100644 --- a/src/phases/pokemon-anim-phase.ts +++ b/src/phases/pokemon-anim-phase.ts @@ -1,8 +1,9 @@ import BattleScene from "#app/battle-scene"; import { SubstituteTag } from "#app/data/battler-tags"; -import { PokemonAnimType } from "#enums/pokemon-anim-type"; import Pokemon from "#app/field/pokemon"; import { BattlePhase } from "#app/phases/battle-phase"; +import { isNullOrUndefined } from "#app/utils"; +import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { Species } from "#enums/species"; @@ -51,7 +52,7 @@ export class PokemonAnimPhase extends BattlePhase { private doSubstituteAddAnim(): void { const substitute = this.pokemon.getTag(SubstituteTag); - if (substitute === null) { + if (isNullOrUndefined(substitute)) { return this.end(); } diff --git a/src/test/moves/grudge.test.ts b/src/test/moves/grudge.test.ts new file mode 100644 index 00000000000..340808929ab --- /dev/null +++ b/src/test/moves/grudge.test.ts @@ -0,0 +1,90 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { BattlerIndex } from "#app/battle"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Grudge", () => { + 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.EMBER, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.SHEDINJA) + .enemyAbility(Abilities.WONDER_GUARD) + .enemyMoveset([ Moves.GRUDGE, Moves.SPLASH ]); + }); + + it("should reduce the PP of the Pokemon's move to 0 when the user has fainted", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + game.move.select(Moves.EMBER); + await game.forceEnemyMove(Moves.GRUDGE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + const playerMove = playerPokemon?.getMoveset().find(m => m?.moveId === Moves.EMBER); + + expect(playerMove?.getPpRatio()).toBe(0); + }); + + it("should remain in effect until the user's next move", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.GRUDGE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + + game.move.select(Moves.EMBER); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + + const playerMove = playerPokemon?.getMoveset().find(m => m?.moveId === Moves.EMBER); + + expect(playerMove?.getPpRatio()).toBe(0); + }); + + it("should not reduce the opponent's PP if the user dies to weather/indirect damage", async () => { + // Opponent will be reduced to 1 HP by False Swipe, then faint to Sandstorm + game.override + .moveset([ Moves.FALSE_SWIPE ]) + .startingLevel(100) + .ability(Abilities.SAND_STREAM) + .enemySpecies(Species.RATTATA); + await game.classicMode.startBattle([ Species.GEODUDE ]); + + const enemyPokemon = game.scene.getEnemyPokemon(); + const playerPokemon = game.scene.getPlayerPokemon(); + + game.move.select(Moves.FALSE_SWIPE); + await game.forceEnemyMove(Moves.GRUDGE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemyPokemon?.isFainted()).toBe(true); + + const playerMove = playerPokemon?.getMoveset().find(m => m?.moveId === Moves.FALSE_SWIPE); + expect(playerMove?.getPpRatio()).toBeGreaterThan(0); + }); +}); From 6b7efb444b4b774fdb0dc892caa3f9e4b22967ae Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Wed, 6 Nov 2024 08:29:24 -0800 Subject: [PATCH 34/98] [Ability] Fully implement Synchronize (#4785) Co-authored-by: frutescens Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/ability.ts | 5 ++- src/data/battler-tags.ts | 26 +++++++++++++- src/data/move.ts | 20 ++++++----- src/enums/battler-tag-type.ts | 3 +- src/test/abilities/corrosion.test.ts | 46 ++++++++++++++++++++++++ src/test/abilities/synchronize.test.ts | 12 ------- src/test/moves/psycho_shift.test.ts | 49 ++++++++++++++++++++++++++ 7 files changed, 135 insertions(+), 26 deletions(-) create mode 100644 src/test/abilities/corrosion.test.ts create mode 100644 src/test/moves/psycho_shift.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 4752217da19..2b65ff79d56 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -5433,8 +5433,7 @@ export function initAbilities() { .attr(EffectSporeAbAttr), new Ability(Abilities.SYNCHRONIZE, 3) .attr(SyncEncounterNatureAbAttr) - .attr(SynchronizeStatusAbAttr) - .partial(), // interaction with psycho shift needs work, keeping to old Gen interaction for now + .attr(SynchronizeStatusAbAttr), new Ability(Abilities.CLEAR_BODY, 3) .attr(ProtectStatAbAttr) .ignorable(), @@ -6036,7 +6035,7 @@ export function initAbilities() { .bypassFaint(), new Ability(Abilities.CORROSION, 7) .attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ Type.STEEL, Type.POISON ]) - .edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented), fling with toxic orb (not implemented yet), and synchronize (not fully implemented yet) + .edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented) + fling with toxic orb (not implemented yet) new Ability(Abilities.COMATOSE, 7) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 473974048c9..ebbab7bfa4a 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -18,7 +18,7 @@ import Move, { StatusCategoryOnAllyAttr } from "#app/data/move"; import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms"; -import { StatusEffect } from "#app/data/status-effect"; +import { getStatusEffectHealText, StatusEffect } from "#app/data/status-effect"; import { TerrainType } from "#app/data/terrain"; import { Type } from "#app/data/type"; import { WeatherType } from "#app/data/weather"; @@ -2866,6 +2866,28 @@ export class GrudgeTag extends BattlerTag { } } +/** + * Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon + */ +export class PsychoShiftTag extends BattlerTag { + constructor() { + super(BattlerTagType.PSYCHO_SHIFT, BattlerTagLapseType.AFTER_MOVE, 1, Moves.PSYCHO_SHIFT); + } + + /** + * Heals Psycho Shift's user of its status effect after it uses a move + * @returns `false` to expire the tag immediately + */ + override lapse(pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { + if (pokemon.status && pokemon.isActive(true)) { + pokemon.scene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon))); + pokemon.resetStatus(); + pokemon.updateInfo(); + } + return false; + } +} + /** * 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 @@ -3049,6 +3071,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new PowerTrickTag(sourceMove, sourceId); case BattlerTagType.GRUDGE: return new GrudgeTag(); + case BattlerTagType.PSYCHO_SHIFT: + return new PsychoShiftTag(); 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 fb09d822a1d..9964409df7a 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2270,24 +2270,26 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { super(false, { trigger: MoveEffectTrigger.HIT }); } - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + /** + * Applies the effect of Psycho Shift to its target + * Psycho Shift takes the user's status effect and passes it onto the target. The user is then healed after the move has been successfully executed. + * @returns `true` if Psycho Shift's effect is able to be applied to the target + */ + apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined); if (target.status) { return false; } else { const canSetStatus = target.canSetStatus(statusToApply, true, false, user); + const trySetStatus = canSetStatus ? target.trySetStatus(statusToApply, true, user) : false; - if (canSetStatus) { - if (user.status) { - user.scene.queueMessage(getStatusEffectHealText(user.status.effect, getPokemonNameWithAffix(user))); - } - user.resetStatus(); - user.updateInfo(); - target.trySetStatus(statusToApply, true, user); + if (trySetStatus && user.status) { + // PsychoShiftTag is added to the user if move succeeds so that the user is healed of its status effect after its move + user.addTag(BattlerTagType.PSYCHO_SHIFT); } - return canSetStatus; + return trySetStatus; } } diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 660a47e0d68..b2bbc1e6189 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -90,5 +90,6 @@ export enum BattlerTagType { ELECTRIFIED = "ELECTRIFIED", TELEKINESIS = "TELEKINESIS", COMMANDED = "COMMANDED", - GRUDGE = "GRUDGE" + GRUDGE = "GRUDGE", + PSYCHO_SHIFT = "PSYCHO_SHIFT", } diff --git a/src/test/abilities/corrosion.test.ts b/src/test/abilities/corrosion.test.ts new file mode 100644 index 00000000000..e607e85defb --- /dev/null +++ b/src/test/abilities/corrosion.test.ts @@ -0,0 +1,46 @@ +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"; + +describe("Abilities - Corrosion", () => { + 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 ]) + .battleType("single") + .disableCrits() + .enemySpecies(Species.GRIMER) + .enemyAbility(Abilities.CORROSION) + .enemyMoveset(Moves.TOXIC); + }); + + it("If a Poison- or Steel-type Pokémon with this Ability poisons a target with Synchronize, Synchronize does not gain the ability to poison Poison- or Steel-type Pokémon.", async () => { + game.override.ability(Abilities.SYNCHRONIZE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyPokemon(); + expect(playerPokemon!.status).toBeUndefined(); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + expect(playerPokemon!.status).toBeDefined(); + expect(enemyPokemon!.status).toBeUndefined(); + }); +}); diff --git a/src/test/abilities/synchronize.test.ts b/src/test/abilities/synchronize.test.ts index d34b5631271..f30ab9d3db4 100644 --- a/src/test/abilities/synchronize.test.ts +++ b/src/test/abilities/synchronize.test.ts @@ -94,16 +94,4 @@ describe("Abilities - Synchronize", () => { expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); }); - - it("should activate with Psycho Shift after the move clears the status", async () => { - game.override.statusEffect(StatusEffect.PARALYSIS); - await game.classicMode.startBattle(); - - game.move.select(Moves.PSYCHO_SHIFT); - await game.phaseInterceptor.to("BerryPhase"); - - expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise - expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); - expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); - }); }); diff --git a/src/test/moves/psycho_shift.test.ts b/src/test/moves/psycho_shift.test.ts new file mode 100644 index 00000000000..448a8c99ef0 --- /dev/null +++ b/src/test/moves/psycho_shift.test.ts @@ -0,0 +1,49 @@ +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 Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Psycho Shift", () => { + 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.PSYCHO_SHIFT ]) + .ability(Abilities.BALL_FETCH) + .statusEffect(StatusEffect.POISON) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyLevel(20) + .enemyAbility(Abilities.SYNCHRONIZE) + .enemyMoveset(Moves.SPLASH); + }); + + it("If Psycho Shift is used on a Pokémon with Synchronize, the user of Psycho Shift will already be afflicted with a status condition when Synchronize activates", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyPokemon(); + expect(enemyPokemon?.status).toBeUndefined(); + + game.move.select(Moves.PSYCHO_SHIFT); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon?.status).toBeNull(); + expect(enemyPokemon?.status).toBeDefined(); + }); +}); From 9dae28f26413bd2ffb409bec5132c48b2e5ab8ac Mon Sep 17 00:00:00 2001 From: ThePsychedelicSeal <116048497+ThePsychedelicSeal@users.noreply.github.com> Date: Wed, 6 Nov 2024 09:42:39 -0700 Subject: [PATCH 35/98] [Misc] Adding Type, Current HP, Max HP, & Status to updateGameInfo() (#4756) * Adding to window.gameInfo Added type -> not working currentHP -> not working maxHP status * Adding to updateGameInfo() * Update pokemon.ts * Modifying battle-scene.ts Added code to get Form and Tera Type Typo fixed in pokemon.ts * Output type names/etc instead of numbers --------- Co-authored-by: ThePsychedelicSeal Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> --- src/battle-scene.ts | 20 +++++++++++++++----- src/phases/command-phase.ts | 2 ++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index d17c62c6e0e..d1d520f5f9d 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -38,7 +38,7 @@ import PokemonData from "#app/system/pokemon-data"; import { Nature } from "#app/data/nature"; import { FormChangeItem, pokemonFormChanges, SpeciesFormChange, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "#app/data/pokemon-forms"; import { FormChangePhase } from "#app/phases/form-change-phase"; -import { getTypeRgb } from "#app/data/type"; +import { getTypeRgb, Type } from "#app/data/type"; import PokemonSpriteSparkleHandler from "#app/field/pokemon-sprite-sparkle-handler"; import CharSprite from "#app/ui/char-sprite"; import DamageNumberHandler from "#app/field/damage-number-handler"; @@ -98,6 +98,7 @@ import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { ExpGainsSpeed } from "#enums/exp-gains-speed"; import { BattlerTagType } from "#enums/battler-tag-type"; import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters"; +import { StatusEffect } from "#enums/status-effect"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; @@ -2982,12 +2983,21 @@ export default class BattleScene extends SceneBase { updateGameInfo(): void { const gameInfo = { - playTime: this.sessionPlayTime ? this.sessionPlayTime : 0, + playTime: this.sessionPlayTime ?? 0, gameMode: this.currentBattle ? this.gameMode.getName() : "Title", biome: this.currentBattle ? getBiomeName(this.arena.biomeType) : "", - wave: this.currentBattle?.waveIndex || 0, - party: this.party ? this.party.map(p => { - return { name: p.name, level: p.level }; + wave: this.currentBattle?.waveIndex ?? 0, + party: this.party ? this.party.map((p) => { + return { + name: p.name, + form: p.getFormKey(), + types: p.getTypes().map((type) => Type[type]), + teraType: p.getTeraType() !== Type.UNKNOWN ? Type[p.getTeraType()] : "", + level: p.level, + currentHP: p.hp, + maxHP: p.getMaxHp(), + status: p.status?.effect ? StatusEffect[p.status.effect] : "" + }; }) : [], modeChain: this.ui?.getModeChain() ?? [], }; diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index b99eb8d1b2e..547805ec1d1 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -32,6 +32,8 @@ export class CommandPhase extends FieldPhase { start() { super.start(); + this.scene.updateGameInfo(); + const commandUiHandler = this.scene.ui.handlers[Mode.COMMAND]; if (commandUiHandler) { if (this.scene.currentBattle.turn === 1 || commandUiHandler.getCursor() === Command.POKEMON) { From 1f6dab069d01809e5b333a4b9c0986cbd17d94aa Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Wed, 6 Nov 2024 21:25:27 -0500 Subject: [PATCH 36/98] [Feature][Balance] Add critical captures, update shake probability to match gen 6 (#4791) * Change shake probability to match Gen 6 * Add critical captures, update shake probability to gen 6 * Change IntegerHolder to NumberHolder * Adjust dex count thresholds for multiplier * Disable critical captures in fresh start runs * Skip first shake check for critical captures * Move shake check for crit captures to after first shake * Use less insane catch formula * Integer to number in bounceanim signature * Use max crit catch dex multiplier in daily runs * Adjust crit capture animation --------- Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/pokeball.ts | 67 ++++++++++++++++++++++++++++- src/phases/attempt-capture-phase.ts | 31 +++++++++---- 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/src/data/pokeball.ts b/src/data/pokeball.ts index 57a78e2cd61..447095b0468 100644 --- a/src/data/pokeball.ts +++ b/src/data/pokeball.ts @@ -1,3 +1,4 @@ +import { NumberHolder } from "#app/utils"; import { PokeballType } from "#enums/pokeball"; import BattleScene from "../battle-scene"; import i18next from "i18next"; @@ -82,11 +83,38 @@ export function getPokeballTintColor(type: PokeballType): number { } } -export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite, y1: number, y2: number, baseBounceDuration: integer, callback: Function) { +/** + * Gets the critical capture chance based on number of mons registered in Dex and modified {@link https://bulbapedia.bulbagarden.net/wiki/Catch_rate Catch rate} + * Formula from {@link https://www.dragonflycave.com/mechanics/gen-vi-vii-capturing Dragonfly Cave Gen 6 Capture Mechanics page} + * @param scene {@linkcode BattleScene} current BattleScene + * @param modifiedCatchRate the modified catch rate as calculated in {@linkcode AttemptCapturePhase} + * @returns the chance of getting a critical capture, out of 256 + */ +export function getCriticalCaptureChance(scene: BattleScene, modifiedCatchRate: number): number { + if (scene.gameMode.isFreshStartChallenge()) { + return 0; + } + const dexCount = scene.gameData.getSpeciesCount(d => !!d.caughtAttr); + const catchingCharmMultiplier = new NumberHolder(1); + //scene.findModifier(m => m instanceof CriticalCatchChanceBoosterModifier)?.apply(catchingCharmMultiplier); + const dexMultiplier = scene.gameMode.isDaily || dexCount > 800 ? 2.5 + : dexCount > 600 ? 2 + : dexCount > 400 ? 1.5 + : dexCount > 200 ? 1 + : dexCount > 100 ? 0.5 + : 0; + return Math.floor(catchingCharmMultiplier.value * dexMultiplier * Math.min(255, modifiedCatchRate) / 6); +} + +export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite, y1: number, y2: number, baseBounceDuration: number, callback: Function, isCritical: boolean = false) { let bouncePower = 1; let bounceYOffset = y1; let bounceY = y2; const yd = y2 - y1; + const x0 = pokeball.x; + const x1 = x0 + 3; + const x2 = x0 - 3; + let critShakes = 4; const doBounce = () => { scene.tweens.add({ @@ -117,5 +145,40 @@ export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameOb }); }; - doBounce(); + const doCritShake = () => { + scene.tweens.add({ + targets: pokeball, + x: x2, + duration: 125, + ease: "Linear", + onComplete: () => { + scene.tweens.add({ + targets: pokeball, + x: x1, + duration: 125, + ease: "Linear", + onComplete: () => { + critShakes--; + if (critShakes > 0) { + doCritShake(); + } else { + scene.tweens.add({ + targets: pokeball, + x: x0, + duration: 60, + ease: "Linear", + onComplete: () => scene.time.delayedCall(500, doBounce) + }); + } + } + }); + } + }); + }; + + if (isCritical) { + scene.time.delayedCall(500, doCritShake); + } else { + doBounce(); + } } diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index 483e6eac943..de10d1eca45 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -2,7 +2,7 @@ import { BattlerIndex } from "#app/battle"; import BattleScene from "#app/battle-scene"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { SubstituteTag } from "#app/data/battler-tags"; -import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor } from "#app/data/pokeball"; +import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, getCriticalCaptureChance } from "#app/data/pokeball"; import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; import { EnemyPokemon } from "#app/field/pokemon"; @@ -52,8 +52,10 @@ export class AttemptCapturePhase extends PokemonPhase { const catchRate = pokemon.species.catchRate; const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType); const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1; - const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier); - const y = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x))); + const modifiedCatchRate = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier); + const shakeProbability = Math.round(65536 / Math.pow((255 / modifiedCatchRate), 0.1875)); // Formula taken from gen 6 + const criticalCaptureChance = getCriticalCaptureChance(this.scene, modifiedCatchRate); + const isCritical = pokemon.randSeedInt(256) < criticalCaptureChance; const fpOffset = pokemon.getFieldPositionOffset(); const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType); @@ -61,17 +63,19 @@ export class AttemptCapturePhase extends PokemonPhase { this.pokeball.setOrigin(0.5, 0.625); this.scene.field.add(this.pokeball); - this.scene.playSound("se/pb_throw"); + this.scene.playSound("se/pb_throw", isCritical ? { rate: 0.2 } : undefined); // Crit catch throws are higher pitched this.scene.time.delayedCall(300, () => { this.scene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon); }); this.scene.tweens.add({ + // Throw animation targets: this.pokeball, x: { value: 236 + fpOffset[0], ease: "Linear" }, y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" }, duration: 500, onComplete: () => { + // Ball opens this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); this.scene.playSound("se/pb_rel"); @@ -80,30 +84,33 @@ export class AttemptCapturePhase extends PokemonPhase { addPokeballOpenParticles(this.scene, this.pokeball.x, this.pokeball.y, this.pokeballType); this.scene.tweens.add({ + // Mon enters ball targets: pokemon, duration: 500, ease: "Sine.easeIn", scale: 0.25, y: 20, onComplete: () => { + // Ball closes this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); pokemon.setVisible(false); this.scene.playSound("se/pb_catch"); this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`)); const doShake = () => { + // After the overall catch rate check, the game does 3 shake checks before confirming the catch. let shakeCount = 0; const pbX = this.pokeball.x; const shakeCounter = this.scene.tweens.addCounter({ from: 0, to: 1, - repeat: 4, + repeat: isCritical ? 2 : 4, // Critical captures only perform 1 shake check yoyo: true, ease: "Cubic.easeOut", duration: 250, repeatDelay: 500, onUpdate: t => { - if (shakeCount && shakeCount < 4) { + if (shakeCount && shakeCount < (isCritical ? 2 : 4)) { const value = t.getValue(); const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1; this.pokeball.setX(pbX + value * 4 * directionMultiplier); @@ -114,13 +121,18 @@ export class AttemptCapturePhase extends PokemonPhase { if (!pokemon.species.isObtainable()) { shakeCounter.stop(); this.failCatch(shakeCount); - } else if (shakeCount++ < 3) { - if (pokeballMultiplier === -1 || pokemon.randSeedInt(65536) < y) { + } else if (shakeCount++ < (isCritical ? 1 : 3)) { + // Shake check (skip check for critical or guaranteed captures, but still play the sound) + if (pokeballMultiplier === -1 || isCritical || modifiedCatchRate >= 255 || pokemon.randSeedInt(65536) < shakeProbability) { this.scene.playSound("se/pb_move"); } else { shakeCounter.stop(); this.failCatch(shakeCount); } + } else if (isCritical && pokemon.randSeedInt(65536) >= shakeProbability) { + // Above, perform the one shake check for critical captures after the ball shakes once + shakeCounter.stop(); + this.failCatch(shakeCount); } else { this.scene.playSound("se/pb_lock"); addPokeballCaptureStars(this.scene, this.pokeball); @@ -153,7 +165,8 @@ export class AttemptCapturePhase extends PokemonPhase { }); }; - this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake)); + // Ball bounces (handled in pokemon.ts) + this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake, isCritical)); } }); } From 4c5b83612b570df41decad27002173ca24b05918 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Thu, 7 Nov 2024 07:36:25 -0800 Subject: [PATCH 37/98] [P2] END biome transition now properly uses seeded RNG (#4809) Co-authored-by: frutescens --- src/data/balance/biomes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index 2ce693c360b..0edc8f6c3da 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -7666,7 +7666,7 @@ export function initBiomes() { if (biome === Biome.END) { const biomeList = Object.keys(Biome).filter(key => !isNaN(Number(key))); biomeList.pop(); // Removes Biome.END from the list - const randIndex = Utils.randInt(biomeList.length, 1); // Will never be Biome.TOWN + const randIndex = Utils.randSeedInt(biomeList.length, 1); // Will never be Biome.TOWN biome = Biome[biomeList[randIndex]]; } const linkedBiomes: (Biome | [ Biome, integer ])[] = Array.isArray(biomeLinks[biome]) From 5601bb14ecc25d8489ea4dd4a334ee70cd1984f1 Mon Sep 17 00:00:00 2001 From: Frederico Santos Date: Thu, 7 Nov 2024 21:21:30 +0000 Subject: [PATCH 38/98] Locales update --- public/locales | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales b/public/locales index fc4a1effd51..d600913dbf1 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef +Subproject commit d600913dbf1f8b47dae8dccbd8296df78f1c51b5 From b2fdb9fcd1ac5e50ebcd911f64bc6509c85431d3 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:33:25 -0500 Subject: [PATCH 39/98] [P2] Fix Cosmoem requirng an evolution level (#4812) --- src/data/balance/pokemon-evolutions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index 8f22b288f45..0511efb564d 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -1005,8 +1005,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.COSMOEM, 23, null, null) ], [Species.COSMOEM]: [ - new SpeciesEvolution(Species.SOLGALEO, 53, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG), - new SpeciesEvolution(Species.LUNALA, 53, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.SOLGALEO, 1, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG), + new SpeciesEvolution(Species.LUNALA, 1, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.MELTAN]: [ new SpeciesEvolution(Species.MELMETAL, 48, null, null) From 2b91d9d259516aa33a0a143888ce80c9e2bf30c9 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:30:49 +0100 Subject: [PATCH 40/98] [Dev] Remove logging for api requests outside of dev (#4804) --- src/plugins/api/api-base.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/api/api-base.ts b/src/plugins/api/api-base.ts index 0740de4e675..5c1a30ff3ab 100644 --- a/src/plugins/api/api-base.ts +++ b/src/plugins/api/api-base.ts @@ -69,7 +69,9 @@ export abstract class ApiBase { "Content-Type": config.headers?.["Content-Type"] ?? "application/json", }; - console.log(`Sending ${config.method ?? "GET"} request to: `, this.base + path, config); + if (import.meta.env.DEV) { + console.log(`Sending ${config.method ?? "GET"} request to: `, this.base + path, config); + } return await fetch(this.base + path, config); } From aa2c794910aaef68f95001eacc3735091098e279 Mon Sep 17 00:00:00 2001 From: Daniel Pochert Date: Fri, 8 Nov 2024 05:09:25 +0100 Subject: [PATCH 41/98] [Balance/Bug] Boss segments properly heal (#4819) --- src/field/pokemon.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 25a771c9281..45c9caf7477 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5079,26 +5079,6 @@ export class EnemyPokemon extends Pokemon { } } - heal(amount: integer): integer { - if (this.isBoss()) { - const amountRatio = amount / this.getMaxHp(); - const segmentBypassCount = Math.floor(amountRatio / (1 / this.bossSegments)); - const segmentSize = this.getMaxHp() / this.bossSegments; - for (let s = 1; s < this.bossSegments; s++) { - const hpThreshold = segmentSize * s; - if (this.hp <= Math.round(hpThreshold)) { - const healAmount = Math.min(amount, this.getMaxHp() - this.hp, Math.round(hpThreshold + (segmentSize * segmentBypassCount) - this.hp)); - this.hp += healAmount; - return healAmount; - } else if (s >= this.bossSegmentIndex) { - return super.heal(amount); - } - } - } - - return super.heal(amount); - } - getFieldIndex(): integer { return this.scene.getEnemyField().indexOf(this); } From 4821df68f2829c0ed98ddc382c3ddf1a0ba11c03 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:10:46 -0800 Subject: [PATCH 42/98] [P1] Prevents crash from using Sketch against a lost turn (#4806) * Added check to make sure that Sketch does not copy a failed move. * Added check for Struggle. * Added a revised check. * Added test + change to valid move finding conditional. * Made revision to .find target * Reverting previous commit, whoops. * Add moveset checks to Sketch tests --------- Co-authored-by: frutescens Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/move.ts | 3 ++- src/test/moves/sketch.test.ts | 46 +++++++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 9964409df7a..2eb2f792ef9 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6748,7 +6748,8 @@ export class SketchAttr extends MoveEffectAttr { return false; } - const targetMove = target.getMoveHistory().filter(m => !m.virtual).at(-1); + const targetMove = target.getLastXMoves(target.battleSummonData.turnCount) + .find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual); if (!targetMove) { return false; } diff --git a/src/test/moves/sketch.test.ts b/src/test/moves/sketch.test.ts index 2e3eb97a76c..4386ce5868e 100644 --- a/src/test/moves/sketch.test.ts +++ b/src/test/moves/sketch.test.ts @@ -1,10 +1,12 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult, PokemonMove } from "#app/field/pokemon"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { StatusEffect } from "#app/enums/status-effect"; +import { BattlerIndex } from "#app/battle"; describe("Moves - Sketch", () => { let phaserGame: Phaser.Game; @@ -32,22 +34,46 @@ describe("Moves - Sketch", () => { }); it("Sketch should not fail even if a previous Sketch failed to retrieve a valid move and ran out of PP", async () => { - game.override.moveset([ Moves.SKETCH, Moves.SKETCH ]); - await game.classicMode.startBattle([ Species.REGIELEKI ]); - const playerPokemon = game.scene.getPlayerPokemon(); + const playerPokemon = game.scene.getPlayerPokemon()!; + // can't use normal moveset override because we need to check moveset changes + playerPokemon.moveset = [ new PokemonMove(Moves.SKETCH), new PokemonMove(Moves.SKETCH) ]; game.move.select(Moves.SKETCH); await game.phaseInterceptor.to("TurnEndPhase"); - expect(playerPokemon?.getLastXMoves()[0].result).toBe(MoveResult.FAIL); - const moveSlot0 = playerPokemon?.getMoveset()[0]; - expect(moveSlot0?.moveId).toBe(Moves.SKETCH); - expect(moveSlot0?.getPpRatio()).toBe(0); + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + const moveSlot0 = playerPokemon.getMoveset()[0]!; + expect(moveSlot0.moveId).toBe(Moves.SKETCH); + expect(moveSlot0.getPpRatio()).toBe(0); await game.toNextTurn(); game.move.select(Moves.SKETCH); await game.phaseInterceptor.to("TurnEndPhase"); - expect(playerPokemon?.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); - // Can't verify if the player Pokemon's moveset was successfully changed because of overrides. + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(playerPokemon.moveset[0]?.moveId).toBe(Moves.SPLASH); + expect(playerPokemon.moveset[1]?.moveId).toBe(Moves.SKETCH); + }); + + it("Sketch should retrieve the most recent valid move from its target history", async () => { + game.override.enemyStatusEffect(StatusEffect.PARALYSIS); + await game.classicMode.startBattle([ Species.REGIELEKI ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + playerPokemon.moveset = [ new PokemonMove(Moves.SKETCH), new PokemonMove(Moves.GROWL) ]; + + game.move.select(Moves.GROWL); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.move.forceStatusActivation(false); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + + await game.toNextTurn(); + game.move.select(Moves.SKETCH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.move.forceStatusActivation(true); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(playerPokemon.moveset[0]?.moveId).toBe(Moves.SPLASH); + expect(playerPokemon.moveset[1]?.moveId).toBe(Moves.GROWL); }); }); From 625b98a6fee1636e26f86aff0638fa22dd9df842 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Fri, 8 Nov 2024 07:50:23 -0800 Subject: [PATCH 43/98] [Move][Refactor] Create `Move.getPriority` + Fully Implement Upper Hand (#4789) * Fully Implement Upper Hand * doc for UpperHandCondition * New Upper Hand tests + some nit fixes * Use Fake Out over mocked Tackle in test --- src/data/ability.ts | 8 +-- src/data/arena-tag.ts | 10 ++- src/data/move.ts | 35 +++++++++- src/data/terrain.ts | 6 +- src/phases/turn-start-phase.ts | 22 +++---- src/test/moves/upper_hand.test.ts | 103 ++++++++++++++++++++++++++++++ 6 files changed, 151 insertions(+), 33 deletions(-) create mode 100644 src/test/moves/upper_hand.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 2b65ff79d56..c24369b07eb 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -7,7 +7,7 @@ import { Weather, WeatherType } from "./weather"; import { BattlerTag, BattlerTagLapseType, GroundedTag } from "./battler-tags"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { Gender } from "./gender"; -import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; +import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { TerrainType } from "./terrain"; @@ -583,15 +583,11 @@ export class PostDefendAbAttr extends AbAttr { export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr { applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { - const attackPriority = new Utils.IntegerHolder(move.priority); - applyMoveAttrs(IncrementMovePriorityAttr, attacker, null, move, attackPriority); - applyAbAttrs(ChangeMovePriorityAbAttr, attacker, null, simulated, move, attackPriority); - if (move.moveTarget === MoveTarget.USER || move.moveTarget === MoveTarget.NEAR_ALLY) { return false; } - if (attackPriority.value > 0 && !move.isMultiTarget()) { + if (move.getPriority(attacker) > 0 && !move.isMultiTarget()) { cancelled.value = true; return true; } diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 73f8e16d573..d70a865f4a4 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -2,12 +2,12 @@ import { Arena } from "#app/field/arena"; import BattleScene from "#app/battle-scene"; import { Type } from "#app/data/type"; import { BooleanHolder, NumberHolder, toDmgValue } from "#app/utils"; -import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "#app/data/move"; +import { MoveCategory, allMoves, MoveTarget } from "#app/data/move"; import { getPokemonNameWithAffix } from "#app/messages"; import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon"; import { StatusEffect } from "#app/data/status-effect"; import { BattlerIndex } from "#app/battle"; -import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, InfiltratorAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; +import { BlockNonDirectDamageAbAttr, InfiltratorAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; import { Stat } from "#enums/stat"; import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims"; import i18next from "i18next"; @@ -318,17 +318,15 @@ export class ConditionalProtectTag extends ArenaTag { */ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { const move = allMoves[moveId]; - const priority = new NumberHolder(move.priority); const effectPhase = arena.scene.getCurrentPhase(); if (effectPhase instanceof MoveEffectPhase) { const attacker = effectPhase.getUserPokemon(); - applyMoveAttrs(IncrementMovePriorityAttr, attacker, null, move, priority); if (attacker) { - applyAbAttrs(ChangeMovePriorityAbAttr, attacker, null, false, move, priority); + return move.getPriority(attacker) > 0; } } - return priority.value > 0; + return move.priority > 0; }; /** diff --git a/src/data/move.ts b/src/data/move.ts index 2eb2f792ef9..07dcf013206 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPostItemLostAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, PostItemLostAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; +import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPostItemLostAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ChangeMovePriorityAbAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, PostItemLostAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; import { TerrainType } from "./terrain"; @@ -829,6 +829,15 @@ export default class Move implements Localizable { return power.value; } + + getPriority(user: Pokemon, simulated: boolean = true) { + const priority = new Utils.NumberHolder(this.priority); + + applyMoveAttrs(IncrementMovePriorityAttr, user, null, this, priority); + applyAbAttrs(ChangeMovePriorityAbAttr, user, null, simulated, this, priority); + + return priority.value; + } } export class AttackMove extends Move { @@ -7452,6 +7461,27 @@ export class FirstMoveCondition extends MoveCondition { } } +/** + * Condition used by the move {@link https://bulbapedia.bulbagarden.net/wiki/Upper_Hand_(move) | Upper Hand}. + * Moves with this condition are only successful when the target has selected + * a high-priority attack (after factoring in priority-boosting effects) and + * hasn't moved yet this turn. + */ +export class UpperHandCondition extends MoveCondition { + constructor() { + super((user, target, move) => { + const targetCommand = user.scene.currentBattle.turnCommands[target.getBattlerIndex()]; + + return !!targetCommand + && targetCommand.command === Command.FIGHT + && !target.turnData.acted + && !!targetCommand.move?.move + && allMoves[targetCommand.move.move].category !== MoveCategory.STATUS + && allMoves[targetCommand.move.move].getPriority(target) > 0; + }); + } +} + export class hitsSameTypeAttr extends VariableMoveTypeMultiplierAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const multiplier = args[0] as Utils.NumberHolder; @@ -10569,8 +10599,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, false, 2), new AttackMove(Moves.UPPER_HAND, Type.FIGHTING, MoveCategory.PHYSICAL, 65, 100, 15, 100, 3, 9) .attr(FlinchAttr) - .condition((user, target, move) => user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].priority > 0 ) // TODO: is this bang correct? - .partial(), // Should also apply when target move priority increased by ability ex. gale wings + .condition(new UpperHandCondition()), new AttackMove(Moves.MALIGNANT_CHAIN, Type.POISON, MoveCategory.SPECIAL, 100, 100, 5, 50, 0, 9) .attr(StatusEffectAttr, StatusEffect.TOXIC) ); diff --git a/src/data/terrain.ts b/src/data/terrain.ts index d8ee8d67925..0bee1ddc7a9 100644 --- a/src/data/terrain.ts +++ b/src/data/terrain.ts @@ -1,8 +1,6 @@ import Pokemon from "../field/pokemon"; import Move from "./move"; import { Type } from "./type"; -import * as Utils from "../utils"; -import { ChangeMovePriorityAbAttr, applyAbAttrs } from "./ability"; import { ProtectAttr } from "./move"; import { BattlerIndex } from "#app/battle"; import i18next from "i18next"; @@ -58,10 +56,8 @@ export class Terrain { switch (this.terrainType) { case TerrainType.PSYCHIC: if (!move.hasAttr(ProtectAttr)) { - const priority = new Utils.IntegerHolder(move.priority); - applyAbAttrs(ChangeMovePriorityAbAttr, user, null, false, move, priority); // Cancels move if the move has positive priority and targets a Pokemon grounded on the Psychic Terrain - return priority.value > 0 && user.getOpponents().some(o => targets.includes(o.getBattlerIndex()) && o.isGrounded()); + return move.getPriority(user) > 0 && user.getOpponents().some(o => targets.includes(o.getBattlerIndex()) && o.isGrounded()); } } diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index dc3ee3f660a..b48b018a046 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -1,6 +1,6 @@ import BattleScene from "#app/battle-scene"; -import { applyAbAttrs, BypassSpeedChanceAbAttr, PreventBypassSpeedChanceAbAttr, ChangeMovePriorityAbAttr } from "#app/data/ability"; -import { allMoves, applyMoveAttrs, IncrementMovePriorityAttr, MoveHeaderAttr } from "#app/data/move"; +import { applyAbAttrs, BypassSpeedChanceAbAttr, PreventBypassSpeedChanceAbAttr } from "#app/data/ability"; +import { allMoves, MoveHeaderAttr } from "#app/data/move"; import { Abilities } from "#app/enums/abilities"; import { Stat } from "#app/enums/stat"; import Pokemon, { PokemonMove } from "#app/field/pokemon"; @@ -98,26 +98,22 @@ export class TurnStartPhase extends FieldPhase { const aMove = allMoves[aCommand.move!.move]; const bMove = allMoves[bCommand!.move!.move]; - // The game now considers priority and applies the relevant move and ability attributes - const aPriority = new Utils.IntegerHolder(aMove.priority); - const bPriority = new Utils.IntegerHolder(bMove.priority); + const aUser = this.scene.getField(true).find(p => p.getBattlerIndex() === a)!; + const bUser = this.scene.getField(true).find(p => p.getBattlerIndex() === b)!; - applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, aMove, aPriority); - applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, bMove, bPriority); - - applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, false, aMove, aPriority); - applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, false, bMove, bPriority); + const aPriority = aMove.getPriority(aUser, false); + const bPriority = bMove.getPriority(bUser, false); // The game now checks for differences in priority levels. // If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result. // This conditional is used to ensure that Quick Claw can still activate with abilities like Stall and Mycelium Might (attack moves only) // Otherwise, the game returns the user of the move with the highest priority. - const isSameBracket = Math.ceil(aPriority.value) - Math.ceil(bPriority.value) === 0; - if (aPriority.value !== bPriority.value) { + const isSameBracket = Math.ceil(aPriority) - Math.ceil(bPriority) === 0; + if (aPriority !== bPriority) { if (isSameBracket && battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { return battlerBypassSpeed[a].value ? -1 : 1; } - return aPriority.value < bPriority.value ? 1 : -1; + return (aPriority < bPriority) ? 1 : -1; } } diff --git a/src/test/moves/upper_hand.test.ts b/src/test/moves/upper_hand.test.ts new file mode 100644 index 00000000000..f94197d3fbd --- /dev/null +++ b/src/test/moves/upper_hand.test.ts @@ -0,0 +1,103 @@ +import { BattlerIndex } from "#app/battle"; +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 Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Upper Hand", () => { + 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.UPPER_HAND) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.QUICK_ATTACK) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should flinch the opponent before they use a priority attack", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const feebas = game.scene.getPlayerPokemon()!; + const magikarp = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.UPPER_HAND); + await game.phaseInterceptor.to("BerryPhase"); + + expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(magikarp.isFullHp()).toBeFalsy(); + expect(feebas.isFullHp()).toBeTruthy(); + }); + + it.each([ + { descriptor: "non-priority attack", move: Moves.TACKLE }, + { descriptor: "status move", move: Moves.BABY_DOLL_EYES } + ])("should fail when the opponent selects a $descriptor", async ({ move }) => { + game.override.enemyMoveset(move); + + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const feebas = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.UPPER_HAND); + await game.phaseInterceptor.to("BerryPhase"); + + expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should flinch the opponent before they use an attack boosted by Gale Wings", async () => { + game.override + .enemyAbility(Abilities.GALE_WINGS) + .enemyMoveset(Moves.GUST); + + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const feebas = game.scene.getPlayerPokemon()!; + const magikarp = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.UPPER_HAND); + await game.phaseInterceptor.to("BerryPhase"); + + expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(magikarp.isFullHp()).toBeFalsy(); + expect(feebas.isFullHp()).toBeTruthy(); + }); + + it("should fail if the target has already moved", async () => { + game.override + .enemyMoveset(Moves.FAKE_OUT) + .enemyAbility(Abilities.SHEER_FORCE); + + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const feebas = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.UPPER_HAND); + + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(feebas.isFullHp()).toBeFalsy(); + }); +}); From 58d40b905aa48fd10a1681172eb2ace5ec5b9d02 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:35:33 -0800 Subject: [PATCH 44/98] [Bug] Fixing Encore's interactions with other Move Restriction moves (#4805) * Converted EncoreTag into a MoveRestrictionBattlerTag * Wrote test and added documentation * Added documentation describing EncoreTag as a whole * Added PRE_MOVE lapse code to handle early tag expiration from PP-less encored move * Replaced PRE_MOVE with CUSTOM for lapsing Encore in situations where the encored move has 0 PP * Add encore tests * fix overrides * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update command-phase.ts * Addressed failing eslint test --------- Co-authored-by: frutescens Co-authored-by: innerthunder Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/battler-tags.ts | 43 ++++++++++++- src/phases/command-phase.ts | 26 ++------ src/test/moves/encore.test.ts | 116 ++++++++++++++++++++++++++++++++++ src/ui/command-ui-handler.ts | 3 - 4 files changed, 162 insertions(+), 26 deletions(-) create mode 100644 src/test/moves/encore.test.ts diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index ebbab7bfa4a..5726a5d53f3 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -909,11 +909,15 @@ export class FrenzyTag extends BattlerTag { } } -export class EncoreTag extends BattlerTag { +/** + * Applies the effects of the move Encore onto the target Pokemon + * Encore forces the target Pokemon to use its most-recent move for 3 turns + */ +export class EncoreTag extends MoveRestrictionBattlerTag { public moveId: Moves; constructor(sourceId: number) { - super(BattlerTagType.ENCORE, BattlerTagLapseType.AFTER_MOVE, 3, Moves.ENCORE, sourceId); + super(BattlerTagType.ENCORE, [ BattlerTagLapseType.CUSTOM, BattlerTagLapseType.AFTER_MOVE ], 3, Moves.ENCORE, sourceId); } /** @@ -969,6 +973,39 @@ export class EncoreTag extends BattlerTag { } } + /** + * If the encored move has run out of PP, Encore ends early. Otherwise, Encore lapses based on the AFTER_MOVE battler tag lapse type. + * @returns `true` to persist | `false` to end and be removed + */ + override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (lapseType === BattlerTagLapseType.CUSTOM) { + const encoredMove = pokemon.getMoveset().find(m => m?.moveId === this.moveId); + if (encoredMove && encoredMove?.getPpRatio() > 0) { + return true; + } + return false; + } else { + return super.lapse(pokemon, lapseType); + } + } + + /** + * Checks if the move matches the moveId stored within the tag and returns a boolean value + * @param move {@linkcode Moves} the move selected + * @param user N/A + * @returns `true` if the move does not match with the moveId stored and as a result, restricted + */ + override isMoveRestricted(move: Moves, _user?: Pokemon): boolean { + if (move !== this.moveId) { + return true; + } + return false; + } + + override selectionDeniedText(_pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:moveDisabled", { moveName: allMoves[move].name }); + } + onRemove(pokemon: Pokemon): void { super.onRemove(pokemon); @@ -2360,7 +2397,7 @@ export class HealBlockTag extends MoveRestrictionBattlerTag { } /** - * Uses DisabledTag's selectionDeniedText() message + * Uses its own unique selectionDeniedText() message */ override selectionDeniedText(pokemon: Pokemon, move: Moves): string { return i18next.t("battle:moveDisabledHealBlock", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name, healBlockName: allMoves[Moves.HEAL_BLOCK].name }); diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 547805ec1d1..6163b428603 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -61,6 +61,12 @@ export class CommandPhase extends FieldPhase { this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.FIGHT, move: { move: Moves.NONE, targets: []}, skip: true }; } + // Checks if the Pokemon is under the effects of Encore. If so, Encore can end early if the encored move has no more PP. + const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag; + if (encoreTag) { + this.getPokemon().lapseTag(BattlerTagType.ENCORE); + } + if (this.scene.currentBattle.turnCommands[this.fieldIndex]?.skip) { return this.end(); } @@ -291,26 +297,6 @@ export class CommandPhase extends FieldPhase { } } - checkFightOverride(): boolean { - const pokemon = this.getPokemon(); - - const encoreTag = pokemon.getTag(EncoreTag) as EncoreTag; - - if (!encoreTag) { - return false; - } - - const moveIndex = pokemon.getMoveset().findIndex(m => m?.moveId === encoreTag.moveId); - - if (moveIndex === -1 || !pokemon.getMoveset()[moveIndex]!.isUsable(pokemon)) { // TODO: is this bang correct? - return false; - } - - this.handleCommand(Command.FIGHT, moveIndex, false); - - return true; - } - getFieldIndex(): integer { return this.fieldIndex; } diff --git a/src/test/moves/encore.test.ts b/src/test/moves/encore.test.ts new file mode 100644 index 00000000000..7d8dc9658bf --- /dev/null +++ b/src/test/moves/encore.test.ts @@ -0,0 +1,116 @@ +import { BattlerTagType } from "#enums/battler-tag-type"; +import { BattlerIndex } from "#app/battle"; +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 Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Encore", () => { + 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.ENCORE ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([ Moves.SPLASH, Moves.TACKLE ]) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should prevent the target from using any move except the last used move", async () => { + await game.classicMode.startBattle([ Species.SNORLAX ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.ENCORE); + await game.forceEnemyMove(Moves.SPLASH); + + await game.toNextTurn(); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeDefined(); + + game.move.select(Moves.SPLASH); + // The enemy AI would normally be inclined to use Tackle, but should be + // forced into using Splash. + await game.phaseInterceptor.to("BerryPhase", false); + + expect(enemyPokemon.getLastXMoves().every(turnMove => turnMove.move === Moves.SPLASH)).toBeTruthy(); + }); + + describe("should fail against the following moves:", () => { + it.each([ + { moveId: Moves.TRANSFORM, name: "Transform", delay: false }, + { moveId: Moves.MIMIC, name: "Mimic", delay: true }, + { moveId: Moves.SKETCH, name: "Sketch", delay: true }, + { moveId: Moves.ENCORE, name: "Encore", delay: false }, + { moveId: Moves.STRUGGLE, name: "Struggle", delay: false } + ])("$name", async ({ moveId, delay }) => { + game.override.enemyMoveset(moveId); + + await game.classicMode.startBattle([ Species.SNORLAX ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + if (delay) { + game.move.select(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + } + + game.move.select(Moves.ENCORE); + + const turnOrder = delay + ? [ BattlerIndex.PLAYER, BattlerIndex.ENEMY ] + : [ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]; + await game.setTurnOrder(turnOrder); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeUndefined(); + }); + }); + + it("Pokemon under both Encore and Torment should alternate between Struggle and restricted move", async () => { + const turnOrder = [ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]; + game.override.moveset([ Moves.ENCORE, Moves.TORMENT, Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const enemyPokemon = game.scene.getEnemyPokemon(); + game.move.select(Moves.ENCORE); + await game.setTurnOrder(turnOrder); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon?.getTag(BattlerTagType.ENCORE)).toBeDefined(); + + await game.toNextTurn(); + game.move.select(Moves.TORMENT); + await game.setTurnOrder(turnOrder); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon?.getTag(BattlerTagType.TORMENT)).toBeDefined(); + + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + await game.setTurnOrder(turnOrder); + await game.phaseInterceptor.to("BerryPhase"); + const lastMove = enemyPokemon?.getLastXMoves()[0]; + expect(lastMove?.move).toBe(Moves.STRUGGLE); + }); +}); diff --git a/src/ui/command-ui-handler.ts b/src/ui/command-ui-handler.ts index 0f5edc28675..0dacacc7b70 100644 --- a/src/ui/command-ui-handler.ts +++ b/src/ui/command-ui-handler.ts @@ -90,9 +90,6 @@ export default class CommandUiHandler extends UiHandler { switch (cursor) { // Fight case Command.FIGHT: - if ((this.scene.getCurrentPhase() as CommandPhase).checkFightOverride()) { - return true; - } ui.setMode(Mode.FIGHT, (this.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); success = true; break; From 00f7fd47dfeb47778c071a0f5d2da5adf11c12d5 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:44:34 -0800 Subject: [PATCH 45/98] [Refactor] Remove unnecessary re-exports (#4818) * Remove unnecessary re-exports * Move `Type` enum to `src/enums/type.ts` * Remove import style change from `modifier-type.ts` --- src/battle-scene.ts | 7 +++--- src/battle.ts | 2 +- src/data/ability.ts | 8 ++++--- src/data/arena-tag.ts | 4 ++-- src/data/balance/biomes.ts | 2 +- src/data/balance/pokemon-evolutions.ts | 8 +++---- src/data/battler-tags.ts | 7 +++--- src/data/challenge.ts | 4 ++-- src/data/custom-pokemon-data.ts | 2 +- src/data/move.ts | 8 ++++--- .../encounters/absolute-avarice-encounter.ts | 2 +- .../encounters/bug-type-superfan-encounter.ts | 2 +- .../encounters/clowning-around-encounter.ts | 2 +- .../encounters/dark-deal-encounter.ts | 2 +- .../encounters/fiery-fallout-encounter.ts | 6 ++--- .../global-trade-system-encounter.ts | 3 ++- .../encounters/safari-zone-encounter.ts | 2 +- .../slumbering-snorlax-encounter.ts | 2 +- .../teleporting-hijinks-encounter.ts | 2 +- .../the-expert-pokemon-breeder-encounter.ts | 2 +- .../the-pokemon-salesman-encounter.ts | 2 +- .../encounters/the-strong-stuff-encounter.ts | 2 +- .../the-winstrate-challenge-encounter.ts | 2 +- .../encounters/training-session-encounter.ts | 3 ++- .../encounters/weird-dream-encounter.ts | 2 +- .../mystery-encounter-option.ts | 2 +- .../mystery-encounter-requirements.ts | 8 +++---- .../mystery-encounters/mystery-encounter.ts | 2 +- .../utils/encounter-phase-utils.ts | 5 ++-- .../utils/encounter-pokemon-utils.ts | 8 ++++--- src/data/nature.ts | 4 +--- src/data/pokeball.ts | 2 -- src/data/pokemon-forms.ts | 6 ++--- src/data/pokemon-species.ts | 2 +- src/data/status-effect.ts | 2 -- src/data/terrain.ts | 2 +- src/data/trainer-config.ts | 4 ++-- src/data/type.ts | 23 +------------------ src/data/weather.ts | 3 +-- src/enums/type.ts | 22 ++++++++++++++++++ src/events/arena.ts | 2 +- src/field/anims.ts | 2 +- src/field/arena.ts | 5 ++-- src/field/pokemon.ts | 13 +++++++---- src/modifier/modifier-type.ts | 19 +++++++-------- src/modifier/modifier.ts | 2 +- src/phases/command-phase.ts | 2 +- src/phases/move-effect-phase.ts | 2 +- src/phases/move-phase.ts | 2 +- src/system/game-data.ts | 4 ++-- src/system/pokemon-data.ts | 4 ++-- src/test/abilities/battle_bond.test.ts | 3 ++- src/test/abilities/disguise.test.ts | 2 +- src/test/abilities/flash_fire.test.ts | 2 +- src/test/abilities/galvanize.test.ts | 2 +- src/test/abilities/libero.test.ts | 5 ++-- src/test/abilities/magic_guard.test.ts | 5 ++-- src/test/abilities/mimicry.test.ts | 2 +- src/test/abilities/parental_bond.test.ts | 6 ++--- src/test/abilities/pastel_veil.test.ts | 2 +- src/test/abilities/power_construct.test.ts | 3 ++- src/test/abilities/protean.test.ts | 5 ++-- src/test/abilities/sand_veil.test.ts | 4 ++-- src/test/abilities/schooling.test.ts | 3 ++- src/test/abilities/shields_down.test.ts | 3 ++- src/test/abilities/synchronize.test.ts | 2 +- src/test/abilities/zen_mode.test.ts | 3 ++- src/test/abilities/zero_to_hero.test.ts | 3 ++- src/test/arena/weather_fog.test.ts | 2 +- src/test/arena/weather_hail.test.ts | 4 ++-- src/test/arena/weather_sandstorm.test.ts | 2 +- src/test/battle/double_battle.test.ts | 3 ++- src/test/battle/inverse_battle.test.ts | 2 +- src/test/data/status-effect.test.ts | 2 +- src/test/field/pokemon.test.ts | 2 +- src/test/final_boss.test.ts | 12 +++++----- src/test/items/toxic_orb.test.ts | 2 +- src/test/moves/aurora_veil.test.ts | 4 ++-- src/test/moves/camouflage.test.ts | 2 +- src/test/moves/dragon_cheer.test.ts | 2 +- src/test/moves/dragon_rage.test.ts | 2 +- src/test/moves/effectiveness.test.ts | 2 +- src/test/moves/electrify.test.ts | 2 +- src/test/moves/flower_shield.test.ts | 2 +- src/test/moves/forests_curse.test.ts | 2 +- src/test/moves/fusion_flare.test.ts | 2 +- src/test/moves/heal_block.test.ts | 2 +- src/test/moves/nightmare.test.ts | 2 +- src/test/moves/plasma_fists.test.ts | 2 +- src/test/moves/pledge_moves.test.ts | 2 +- src/test/moves/purify.test.ts | 3 ++- src/test/moves/reflect_type.test.ts | 2 +- src/test/moves/relic_song.test.ts | 2 +- src/test/moves/roost.test.ts | 2 +- src/test/moves/substitute.test.ts | 12 +++++----- src/test/moves/tar_shot.test.ts | 2 +- src/test/moves/tera_blast.test.ts | 2 +- src/test/moves/tera_starstorm.test.ts | 2 +- src/test/moves/thunder_wave.test.ts | 4 ++-- src/test/moves/toxic_spikes.test.ts | 2 +- src/test/moves/trick_or_treat.test.ts | 2 +- .../mystery-encounter/encounter-test-utils.ts | 23 ++++++++++--------- .../clowning-around-encounter.test.ts | 2 +- .../fiery-fallout-encounter.test.ts | 13 ++++++----- .../the-strong-stuff-encounter.test.ts | 12 +++++----- .../the-winstrate-challenge-encounter.test.ts | 11 +++++---- .../mystery-encounter-utils.test.ts | 20 ++++++++-------- src/test/phases/form-change-phase.test.ts | 2 +- src/test/ui/starter-select.test.ts | 2 +- src/test/utils/helpers/overridesHelper.ts | 19 +++++++-------- src/ui/arena-flyout.ts | 2 +- src/ui/battle-info.ts | 5 ++-- src/ui/fight-ui-handler.ts | 3 ++- src/ui/modifier-select-ui-handler.ts | 3 ++- src/ui/move-info-overlay.ts | 2 +- src/ui/party-ui-handler.ts | 2 +- src/ui/pokemon-hatch-info-container.ts | 2 +- src/ui/pokemon-info-container.ts | 2 +- src/ui/run-info-ui-handler.ts | 3 ++- src/ui/starter-select-ui-handler.ts | 5 ++-- src/ui/summary-ui-handler.ts | 8 ++++--- 121 files changed, 281 insertions(+), 249 deletions(-) create mode 100644 src/enums/type.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index d1d520f5f9d..ed8a79125bc 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -5,7 +5,7 @@ import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } f import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import * as Utils from "#app/utils"; import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, RememberMoveModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "#app/data/battle-anims"; import { Phase } from "#app/phase"; import { initGameSpeed } from "#app/system/game-speed"; @@ -35,10 +35,11 @@ import { Gender } from "#app/data/gender"; import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin"; import { addUiThemeOverrides } from "#app/ui/ui-theme"; import PokemonData from "#app/system/pokemon-data"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { FormChangeItem, pokemonFormChanges, SpeciesFormChange, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "#app/data/pokemon-forms"; import { FormChangePhase } from "#app/phases/form-change-phase"; -import { getTypeRgb, Type } from "#app/data/type"; +import { getTypeRgb } from "#app/data/type"; +import { Type } from "#enums/type"; import PokemonSpriteSparkleHandler from "#app/field/pokemon-sprite-sparkle-handler"; import CharSprite from "#app/ui/char-sprite"; import DamageNumberHandler from "#app/field/damage-number-handler"; diff --git a/src/battle.ts b/src/battle.ts index 0356772bb07..75f0dff2534 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -4,7 +4,7 @@ import * as Utils from "./utils"; import Trainer, { TrainerVariant } from "./field/trainer"; import { GameMode } from "./game-mode"; import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier"; -import { PokeballType } from "./data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { trainerConfigs } from "#app/data/trainer-config"; import { SpeciesFormKey } from "#enums/species-form-key"; import Pokemon, { EnemyPokemon, PlayerPokemon, QueuedMove } from "#app/field/pokemon"; diff --git a/src/data/ability.ts b/src/data/ability.ts index c24369b07eb..08dc1ed27a4 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1,11 +1,11 @@ import Pokemon, { EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; -import { Type } from "./type"; +import { Type } from "#enums/type"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import { getPokemonNameWithAffix } from "../messages"; -import { Weather, WeatherType } from "./weather"; +import { Weather } from "#app/data/weather"; import { BattlerTag, BattlerTagLapseType, GroundedTag } from "./battler-tags"; -import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; +import { getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "#app/data/status-effect"; import { Gender } from "./gender"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; @@ -36,6 +36,8 @@ import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { NewBattlePhase } from "#app/phases/new-battle-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { PokemonAnimType } from "#enums/pokemon-anim-type"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; export class Ability implements Localizable { public id: Abilities; diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index d70a865f4a4..2f57650c65d 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1,11 +1,11 @@ import { Arena } from "#app/field/arena"; import BattleScene from "#app/battle-scene"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { BooleanHolder, NumberHolder, toDmgValue } from "#app/utils"; import { MoveCategory, allMoves, MoveTarget } from "#app/data/move"; import { getPokemonNameWithAffix } from "#app/messages"; import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon"; -import { StatusEffect } from "#app/data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import { BattlerIndex } from "#app/battle"; import { BlockNonDirectDamageAbAttr, InfiltratorAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; import { Stat } from "#enums/stat"; diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index 0edc8f6c3da..0f4926cf7c7 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -1,4 +1,4 @@ -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import * as Utils from "#app/utils"; import { pokemonEvolutions, SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import i18next from "i18next"; diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index 0511efb564d..9e86ea7397b 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -1,10 +1,10 @@ import { Gender } from "#app/data/gender"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import Pokemon from "#app/field/pokemon"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import * as Utils from "#app/utils"; -import { WeatherType } from "#app/data/weather"; -import { Nature } from "#app/data/nature"; +import { WeatherType } from "#enums/weather-type"; +import { Nature } from "#enums/nature"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 5726a5d53f3..25d65fbc372 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -18,10 +18,9 @@ import Move, { StatusCategoryOnAllyAttr } from "#app/data/move"; import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms"; -import { getStatusEffectHealText, StatusEffect } from "#app/data/status-effect"; +import { getStatusEffectHealText } from "#app/data/status-effect"; import { TerrainType } from "#app/data/terrain"; -import { Type } from "#app/data/type"; -import { WeatherType } from "#app/data/weather"; +import { Type } from "#enums/type"; import Pokemon, { HitResult, MoveResult } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { CommonAnimPhase } from "#app/phases/common-anim-phase"; @@ -38,6 +37,8 @@ import { Moves } from "#enums/moves"; import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { Species } from "#enums/species"; import { EFFECTIVE_STATS, getStatKey, Stat, type BattleStat, type EffectiveStat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; export enum BattlerTagLapseType { FAINT, diff --git a/src/data/challenge.ts b/src/data/challenge.ts index a64a90e5d14..af6bbf5b00f 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -7,11 +7,11 @@ import Pokemon, { PokemonMove } from "#app/field/pokemon"; import { BattleType, FixedBattleConfig } from "#app/battle"; import Trainer, { TrainerVariant } from "#app/field/trainer"; import { GameMode } from "#app/game-mode"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Challenges } from "#enums/challenges"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { Moves } from "#enums/moves"; import { TypeColor, TypeShadow } from "#enums/color"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; diff --git a/src/data/custom-pokemon-data.ts b/src/data/custom-pokemon-data.ts index 2e94123fc84..7bc884cff50 100644 --- a/src/data/custom-pokemon-data.ts +++ b/src/data/custom-pokemon-data.ts @@ -1,5 +1,5 @@ import { Abilities } from "#enums/abilities"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { isNullOrUndefined } from "#app/utils"; import { Nature } from "#enums/nature"; diff --git a/src/data/move.ts b/src/data/move.ts index 07dcf013206..071d7fa1e65 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2,11 +2,12 @@ import { ChargeAnim, initMoveAnim, loadMoveAnimAssets, MoveChargeAnim } from "./ import { CommandedTag, EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; -import { getNonVolatileStatusEffects, getStatusEffectHealText, isNonVolatileStatusEffect, StatusEffect } from "./status-effect"; -import { getTypeDamageMultiplier, Type } from "./type"; +import { getNonVolatileStatusEffects, getStatusEffectHealText, isNonVolatileStatusEffect } from "./status-effect"; +import { getTypeDamageMultiplier } from "./type"; +import { Type } from "#enums/type"; import { Constructor, NumberHolder } from "#app/utils"; import * as Utils from "../utils"; -import { WeatherType } from "./weather"; +import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPostItemLostAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ChangeMovePriorityAbAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, PostItemLostAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; @@ -38,6 +39,7 @@ import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms"; import { GameMode } from "#app/game-mode"; import { applyChallenges, ChallengeType } from "./challenge"; import { SwitchType } from "#enums/switch-type"; +import { StatusEffect } from "enums/status-effect"; export enum MoveCategory { PHYSICAL, diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index fa8e1aed1c7..9c00148fbac 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -18,7 +18,7 @@ import { randInt } from "#app/utils"; import { BattlerIndex } from "#app/battle"; import { applyModifierTypeToPlayerPokemon, catchPokemon, getHighestLevelPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { TrainerSlot } from "#app/data/trainer-config"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import HeldModifierConfig from "#app/interfaces/held-modifier-config"; import { BerryType } from "#enums/berry-type"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index f2605795955..7a03e6efdd2 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -36,7 +36,7 @@ import { HeldItemRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { AttackTypeBoosterModifierType, ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; import { AttackTypeBoosterModifier, diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 5502cc7b53a..ae6cabd4dae 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -12,7 +12,7 @@ import { TrainerType } from "#enums/trainer-type"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Abilities } from "#enums/abilities"; import { applyAbilityOverrideToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { randSeedInt, randSeedShuffle } from "#app/utils"; diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 8a814b58248..2b801d70f61 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -1,4 +1,4 @@ -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 5794277ffe1..bbc979e844e 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -8,14 +8,14 @@ import { AbilityRequirement, CombinationPokemonRequirement, TypeRequirement } fr import { Species } from "#enums/species"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Gender } from "#app/data/gender"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { BattlerIndex } from "#app/battle"; import Pokemon, { PokemonMove } from "#app/field/pokemon"; import { Moves } from "#enums/moves"; import { EncounterBattleAnim } from "#app/data/battle-anims"; -import { WeatherType } from "#app/data/weather"; +import { WeatherType } from "#enums/weather-type"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; -import { StatusEffect } from "#app/data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { applyAbilityOverrideToPokemon, applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 720e904ecdb..b0d547e36cf 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -21,11 +21,12 @@ import PokemonData from "#app/system/pokemon-data"; import i18next from "i18next"; import { Gender, getGenderSymbol } from "#app/data/gender"; import { getNatureName } from "#app/data/nature"; -import { getPokeballAtlasKey, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; +import { getPokeballAtlasKey, getPokeballTintColor } from "#app/data/pokeball"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { trainerNamePools } from "#app/data/trainer-names"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import type { PokeballType } from "#enums/pokeball"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/globalTradeSystem"; diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index 0ee3c57b0a2..0353d52a592 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -6,7 +6,7 @@ import MysteryEncounterOption, { MysteryEncounterOptionBuilder } from "#app/data import { TrainerSlot } from "#app/data/trainer-config"; import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier"; import { EnemyPokemon } from "#app/field/pokemon"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { PlayerGender } from "#enums/player-gender"; import { IntegerHolder, randSeedInt } from "#app/utils"; import { getPokemonSpecies } from "#app/data/pokemon-species"; diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index 8ea19e1225b..3fb502be545 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -3,7 +3,7 @@ import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifi import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import { StatusEffect } from "#app/data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index faeba95f358..042e9278673 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -12,7 +12,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { Biome } from "#enums/biome"; import { getBiomeKey } from "#app/field/arena"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { getPartyLuckValue, modifierTypes } from "#app/modifier/modifier-type"; import { TrainerSlot } from "#app/data/trainer-config"; import { BattlerTagType } from "#enums/battler-tag-type"; diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index 7c904bd047a..6a4c6592fda 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -23,7 +23,7 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { achvs } from "#app/system/achv"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { getPokeballTintColor } from "#app/data/pokeball"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts index 95f359547e4..77d92e3749e 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -8,7 +8,7 @@ import { catchPokemon, getRandomSpeciesByStarterTier, getSpriteKeysFromPokemon } import { getPokemonSpecies } from "#app/data/pokemon-species"; import { speciesStarterCosts } from "#app/data/balance/starters"; import { Species } from "#enums/species"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index b1ef6cedb21..754632aedea 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -5,7 +5,7 @@ import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Species } from "#enums/species"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import Pokemon, { PokemonMove } from "#app/field/pokemon"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { modifyPlayerPokemonBST } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index 5c1af43c426..f4446241873 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -10,7 +10,7 @@ import { Abilities } from "#enums/abilities"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Moves } from "#enums/moves"; import { Nature } from "#enums/nature"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { BerryType } from "#enums/berry-type"; import { Stat } from "#enums/stat"; import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms"; diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 8814eb0d8cf..4a0a91bf751 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -1,6 +1,6 @@ import { Ability, allAbilities } from "#app/data/ability"; import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { getNatureName, Nature } from "#app/data/nature"; +import { getNatureName } from "#app/data/nature"; import { speciesStarterCosts } from "#app/data/balance/starters"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; @@ -21,6 +21,7 @@ import i18next from "i18next"; import { getStatKey } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import type { Nature } from "#enums/nature"; /** The i18n namespace for the encounter */ const namespace = "mysteryEncounters/trainingSession"; diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index c05e707ed4d..3c541e20bf4 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -1,4 +1,4 @@ -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index c674ebdc46e..4ff8fd95f85 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -2,7 +2,7 @@ import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounte import { Moves } from "#app/enums/moves"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import BattleScene from "#app/battle-scene"; -import { Type } from "../type"; +import { Type } from "#enums/type"; import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 1358c465d17..811b622de76 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -1,11 +1,11 @@ import BattleScene from "#app/battle-scene"; import { allAbilities } from "#app/data/ability"; import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; -import { StatusEffect } from "#app/data/status-effect"; -import { Type } from "#app/data/type"; -import { WeatherType } from "#app/data/weather"; +import { StatusEffect } from "#enums/status-effect"; +import { Type } from "#enums/type"; +import { WeatherType } from "#enums/weather-type"; import { PlayerPokemon } from "#app/field/pokemon"; import { AttackTypeBoosterModifier } from "#app/modifier/modifier"; import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index badbfcf7a56..e341da4e435 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -5,7 +5,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; import MysteryEncounterIntroVisuals, { MysteryEncounterSpriteConfig } from "#app/field/mystery-encounter-intro"; import * as Utils from "#app/utils"; -import { StatusEffect } from "../status-effect"; +import { StatusEffect } from "#enums/status-effect"; import MysteryEncounterDialogue, { OptionTextDisplay } from "./mystery-encounter-dialogue"; import MysteryEncounterOption, { MysteryEncounterOptionBuilder, OptionPhaseCallback } from "./mystery-encounter-option"; import { EncounterPokemonRequirement, EncounterSceneRequirement, HealthRatioRequirement, PartySizeRequirement, StatusEffectRequirement, WaveRangeRequirement } from "./mystery-encounter-requirements"; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 429b6bfa292..c6dda134346 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -19,11 +19,11 @@ import i18next from "i18next"; import BattleScene from "#app/battle-scene"; import Trainer, { TrainerVariant } from "#app/field/trainer"; import { Gender } from "#app/data/gender"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { Moves } from "#enums/moves"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-config"; import PokemonSpecies from "#app/data/pokemon-species"; import { Egg, IEggOptions } from "#app/data/egg"; @@ -37,6 +37,7 @@ import { GameOverPhase } from "#app/phases/game-over-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { PartyExpPhase } from "#app/phases/party-exp-phase"; import { Variant } from "#app/data/variant"; +import { StatusEffect } from "#enums/status-effect"; /** * Animates exclamation sprite over trainer's head at start of encounter diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index dd42cc9ce55..fc85754bdde 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -3,15 +3,15 @@ import i18next from "i18next"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; -import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; +import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor } from "#app/data/pokeball"; import { PlayerGender } from "#enums/player-gender"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; -import { getStatusEffectCatchRateMultiplier, StatusEffect } from "#app/data/status-effect"; +import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { achvs } from "#app/system/achv"; import { Mode } from "#app/ui/ui"; import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import { Species } from "#enums/species"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species"; import { speciesStarterCosts } from "#app/data/balance/starters"; import { getEncounterText, queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; @@ -23,6 +23,8 @@ import { VictoryPhase } from "#app/phases/victory-phase"; import { SummaryUiMode } from "#app/ui/summary-ui-handler"; import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { Abilities } from "#enums/abilities"; +import type { PokeballType } from "#enums/pokeball"; +import { StatusEffect } from "#enums/status-effect"; /** Will give +1 level every 10 waves */ export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; diff --git a/src/data/nature.ts b/src/data/nature.ts index edac06f1a4f..b90047ce6f0 100644 --- a/src/data/nature.ts +++ b/src/data/nature.ts @@ -3,9 +3,7 @@ import { TextStyle, getBBCodeFrag } from "../ui/text"; import { Nature } from "#enums/nature"; import { UiTheme } from "#enums/ui-theme"; import i18next from "i18next"; -import { Stat, EFFECTIVE_STATS, getShortenedStatKey } from "#app/enums/stat"; - -export { Nature }; +import { Stat, EFFECTIVE_STATS, getShortenedStatKey } from "#enums/stat"; export function getNatureName(nature: Nature, includeStatEffects: boolean = false, forStarterSelect: boolean = false, ignoreBBCode: boolean = false, uiTheme: UiTheme = UiTheme.DEFAULT): string { let ret = Utils.toReadableString(Nature[nature]); diff --git a/src/data/pokeball.ts b/src/data/pokeball.ts index 447095b0468..49cb0d1bf72 100644 --- a/src/data/pokeball.ts +++ b/src/data/pokeball.ts @@ -3,8 +3,6 @@ import { PokeballType } from "#enums/pokeball"; import BattleScene from "../battle-scene"; import i18next from "i18next"; -export { PokeballType }; - export const MAX_PER_TYPE_POKEBALLS: integer = 99; export function getPokeballAtlasKey(type: PokeballType): string { diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 7cc20d50fb9..2db0ed54294 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -1,8 +1,8 @@ import { PokemonFormChangeItemModifier, TerastallizeModifier } from "../modifier/modifier"; import Pokemon from "../field/pokemon"; -import { StatusEffect } from "./status-effect"; +import { StatusEffect } from "#enums/status-effect"; import { MoveCategory, allMoves } from "./move"; -import { Type } from "./type"; +import { Type } from "#enums/type"; import { Constructor, nil } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; @@ -10,7 +10,7 @@ import { Species } from "#enums/species"; import { TimeOfDay } from "#enums/time-of-day"; import { getPokemonNameWithAffix } from "#app/messages"; import i18next from "i18next"; -import { WeatherType } from "./weather"; +import { WeatherType } from "#enums/weather-type"; import { Challenges } from "#app/enums/challenges"; import { SpeciesFormKey } from "#enums/species-form-key"; diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 0cdeef10d9e..e7fe902956c 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -12,7 +12,7 @@ import { uncatchableSpecies } from "#app/data/balance/biomes"; import { speciesEggMoves } from "#app/data/balance/egg-moves"; import { GrowthRate } from "#app/data/exp"; import { EvolutionLevel, SpeciesWildEvolutionDelay, pokemonEvolutions, pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { LevelMoves, pokemonFormLevelMoves, pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; import { Stat } from "#enums/stat"; import { Variant, VariantSet, variantColorCache, variantData } from "#app/data/variant"; diff --git a/src/data/status-effect.ts b/src/data/status-effect.ts index 56e754ac407..6b4e1d546df 100644 --- a/src/data/status-effect.ts +++ b/src/data/status-effect.ts @@ -2,8 +2,6 @@ import { randIntRange } from "#app/utils"; import { StatusEffect } from "#enums/status-effect"; import i18next, { ParseKeys } from "i18next"; -export { StatusEffect }; - export class Status { public effect: StatusEffect; /** Toxic damage is `1/16 max HP * toxicTurnCount` */ diff --git a/src/data/terrain.ts b/src/data/terrain.ts index 0bee1ddc7a9..6ba9acfd166 100644 --- a/src/data/terrain.ts +++ b/src/data/terrain.ts @@ -1,6 +1,6 @@ import Pokemon from "../field/pokemon"; import Move from "./move"; -import { Type } from "./type"; +import { Type } from "#enums/type"; import { ProtectAttr } from "./move"; import { BattlerIndex } from "#app/battle"; import i18next from "i18next"; diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index 18bcec1f928..d82d568ecc6 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -2,11 +2,11 @@ import BattleScene, { startingWave } from "#app/battle-scene"; import { ModifierTypeFunc, modifierTypes } from "#app/modifier/modifier-type"; import { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; import * as Utils from "#app/utils"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { pokemonEvolutions, pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; import PokemonSpecies, { getPokemonSpecies, PokemonSpeciesFilter } from "#app/data/pokemon-species"; import { tmSpecies } from "#app/data/balance/tms"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { doubleBattleDialogue } from "#app/data/dialogue"; import { PersistentModifier } from "#app/modifier/modifier"; import { TrainerVariant } from "#app/field/trainer"; diff --git a/src/data/type.ts b/src/data/type.ts index 483ec068d3c..6170eadc91e 100644 --- a/src/data/type.ts +++ b/src/data/type.ts @@ -1,25 +1,4 @@ -export enum Type { - UNKNOWN = -1, - NORMAL = 0, - FIGHTING, - FLYING, - POISON, - GROUND, - ROCK, - BUG, - GHOST, - STEEL, - FIRE, - WATER, - GRASS, - ELECTRIC, - PSYCHIC, - ICE, - DRAGON, - DARK, - FAIRY, - STELLAR -} +import { Type } from "#enums/type"; export type TypeDamageMultiplier = 0 | 0.125 | 0.25 | 0.5 | 1 | 2 | 4 | 8; diff --git a/src/data/weather.ts b/src/data/weather.ts index 20c03af77c8..0a76a015402 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -2,7 +2,7 @@ import { Biome } from "#enums/biome"; import { WeatherType } from "#enums/weather-type"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon from "../field/pokemon"; -import { Type } from "./type"; +import { Type } from "#enums/type"; import Move, { AttackMove } from "./move"; import * as Utils from "../utils"; import BattleScene from "../battle-scene"; @@ -10,7 +10,6 @@ import { SuppressWeatherEffectAbAttr } from "./ability"; import { TerrainType, getTerrainName } from "./terrain"; import i18next from "i18next"; -export { WeatherType }; export class Weather { public weatherType: WeatherType; public turnsLeft: integer; diff --git a/src/enums/type.ts b/src/enums/type.ts new file mode 100644 index 00000000000..a04849c2ca3 --- /dev/null +++ b/src/enums/type.ts @@ -0,0 +1,22 @@ +export enum Type { + UNKNOWN = -1, + NORMAL = 0, + FIGHTING, + FLYING, + POISON, + GROUND, + ROCK, + BUG, + GHOST, + STEEL, + FIRE, + WATER, + GRASS, + ELECTRIC, + PSYCHIC, + ICE, + DRAGON, + DARK, + FAIRY, + STELLAR +} diff --git a/src/events/arena.ts b/src/events/arena.ts index c05e67d353c..b1126e5c03d 100644 --- a/src/events/arena.ts +++ b/src/events/arena.ts @@ -1,7 +1,7 @@ import { ArenaTagSide } from "#app/data/arena-tag"; import { ArenaTagType } from "#enums/arena-tag-type"; import { TerrainType } from "#app/data/terrain"; -import { WeatherType } from "#app/data/weather"; +import { WeatherType } from "#enums/weather-type"; /** Alias for all {@linkcode ArenaEvent} type strings */ export enum ArenaEventType { diff --git a/src/field/anims.ts b/src/field/anims.ts index c73c52027c5..dddf38e4a7e 100644 --- a/src/field/anims.ts +++ b/src/field/anims.ts @@ -1,5 +1,5 @@ import BattleScene from "../battle-scene"; -import { PokeballType } from "../data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import * as Utils from "../utils"; export function addPokeballOpenParticles(scene: BattleScene, x: number, y: number, pokeballType: PokeballType): void { diff --git a/src/field/arena.ts b/src/field/arena.ts index abc2b89569c..da74063ac06 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -3,9 +3,9 @@ import { biomePokemonPools, BiomePoolTier, BiomeTierTrainerPools, biomeTrainerPo import { Constructor } from "#app/utils"; import * as Utils from "#app/utils"; import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species"; -import { getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage, Weather, WeatherType } from "#app/data/weather"; +import { getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage, Weather } from "#app/data/weather"; import { CommonAnim } from "#app/data/battle-anims"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import Move from "#app/data/move"; import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "#app/data/arena-tag"; import { BattlerIndex } from "#app/battle"; @@ -31,6 +31,7 @@ import { Abilities } from "#enums/abilities"; import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#app/data/pokemon-forms"; import { CommonAnimPhase } from "#app/phases/common-anim-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; +import { WeatherType } from "#enums/weather-type"; export class Arena { public scene: BattleScene; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 45c9caf7477..e0b7bf1094f 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -9,18 +9,19 @@ import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, spec import { starterPassiveAbilities } from "#app/data/balance/passives"; import { Constructor, isNullOrUndefined, randSeedInt, type nil } from "#app/utils"; import * as Utils from "#app/utils"; -import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type"; +import { TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type"; +import { Type } from "#enums/type"; import { getLevelTotalExp } from "#app/data/exp"; import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier } from "#app/modifier/modifier"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { Gender } from "#app/data/gender"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; -import { Status, StatusEffect, getRandomStatus } from "#app/data/status-effect"; +import { Status, getRandomStatus } from "#app/data/status-effect"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/tms"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags"; -import { WeatherType } from "#app/data/weather"; +import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; @@ -32,7 +33,7 @@ import { LevelMoves } from "#app/data/balance/pokemon-level-moves"; import { DamageAchv, achvs } from "#app/system/achv"; import { DexAttr, StarterDataEntry, StarterMoveset } from "#app/system/game-data"; import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from "@material/material-color-utilities"; -import { Nature, getNatureStatMultiplier } from "#app/data/nature"; +import { getNatureStatMultiplier } from "#app/data/nature"; import { SpeciesFormChange, SpeciesFormChangeActiveTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms"; import { TerrainType } from "#app/data/terrain"; import { TrainerSlot } from "#app/data/trainer-config"; @@ -66,6 +67,8 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { SwitchType } from "#enums/switch-type"; import { SpeciesFormKey } from "#enums/species-form-key"; import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#app/data/balance/rates"; +import { Nature } from "#enums/nature"; +import { StatusEffect } from "#enums/status-effect"; export enum FieldPosition { CENTER, diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index dfa46ce3667..571c54d76e9 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -3,30 +3,31 @@ import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evol import { tmPoolTiers, tmSpecies } from "#app/data/balance/tms"; import { getBerryEffectDescription, getBerryName } from "#app/data/berry"; import { allMoves, AttackMove, selfStatLowerMoves } from "#app/data/move"; -import { getNatureName, getNatureStatMultiplier, Nature } from "#app/data/nature"; -import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS, PokeballType } from "#app/data/pokeball"; +import { getNatureName, getNatureStatMultiplier } from "#app/data/nature"; +import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeCondition, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; -import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect"; -import { Type } from "#app/data/type"; +import { getStatusEffectDescriptor } from "#app/data/status-effect"; +import { Type } from "#enums/type"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { - AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier -} from "#app/modifier/modifier"; +import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; import { ModifierTier } from "#app/modifier/modifier-tier"; import Overrides from "#app/overrides"; import { Unlockables } from "#app/system/unlockables"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import { getModifierTierTextTint } from "#app/ui/text"; -import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; +import { formatMoney, getEnumKeys, getEnumValues, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { Moves } from "#enums/moves"; +import { Nature } from "#enums/nature"; +import { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; import { SpeciesFormKey } from "#enums/species-form-key"; import { getStatKey, PermanentStat, Stat, TEMP_BATTLE_STATS, TempBattleStat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; const outputModifierData = false; @@ -730,7 +731,7 @@ export class MoneyRewardModifierType extends ModifierType { } getDescription(scene: BattleScene): string { - const moneyAmount = new IntegerHolder(scene.getWaveMoneyAmount(this.moneyMultiplier)); + const moneyAmount = new NumberHolder(scene.getWaveMoneyAmount(this.moneyMultiplier)); scene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); const formattedMoney = formatMoney(scene.moneyFormat, moneyAmount.value); diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index c3b1f15fab4..d2965247826 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -6,7 +6,7 @@ import { allMoves } from "#app/data/move"; import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; import { type FormChangeItem, SpeciesFormChangeItemTrigger, SpeciesFormChangeLapseTeraTrigger, SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectHealText } from "#app/data/status-effect"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import Pokemon, { type PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 6163b428603..eab76282908 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -7,7 +7,7 @@ import { Abilities } from "#app/enums/abilities"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Biome } from "#app/enums/biome"; import { Moves } from "#app/enums/moves"; -import { PokeballType } from "#app/enums/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { Command } from "#app/ui/command-ui-handler"; diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 48150ffaf05..ef863d64c50 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -43,7 +43,7 @@ import { VariableTargetAttr, } from "#app/data/move"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import Pokemon, { HitResult, MoveResult, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index b11a7d9b711..6bdef281d70 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -28,7 +28,7 @@ import { } from "#app/data/move"; import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { getTerrainBlockMessage } from "#app/data/weather"; import { MoveUsedEvent } from "#app/events/battle-scene"; import Pokemon, { MoveResult, PokemonMove } from "#app/field/pokemon"; diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 4b7eeedfe77..8f179ddb677 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -22,7 +22,7 @@ import { vouchers, VoucherType } from "#app/system/voucher"; import { AES, enc } from "crypto-js"; import { Mode } from "#app/ui/ui"; import { clientSessionId, loggedInUser, updateUserInfo } from "#app/account"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { GameStats } from "#app/system/game-stats"; import { Tutorial } from "#app/tutorial"; import { speciesEggMoves } from "#app/data/balance/egg-moves"; @@ -33,7 +33,7 @@ import { setSettingGamepad, SettingGamepad, settingGamepadDefaults } from "#app/ import { setSettingKeyboard, SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { TagAddedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena"; import * as Modifier from "#app/modifier/modifier"; -import { StatusEffect } from "#app/data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import ChallengeData from "#app/system/challenge-data"; import { Device } from "#enums/devices"; import { GameDataType } from "#enums/game-data-type"; diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 485c1d90c1d..443382186c7 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -1,8 +1,8 @@ import { BattleType } from "../battle"; import BattleScene from "../battle-scene"; import { Gender } from "../data/gender"; -import { Nature } from "../data/nature"; -import { PokeballType } from "../data/pokeball"; +import { Nature } from "#enums/nature"; +import { PokeballType } from "#enums/pokeball"; import { getPokemonSpecies } from "../data/pokemon-species"; import { Status } from "../data/status-effect"; import Pokemon, { EnemyPokemon, PokemonMove, PokemonSummonData } from "../field/pokemon"; diff --git a/src/test/abilities/battle_bond.test.ts b/src/test/abilities/battle_bond.test.ts index e9970e1c049..db7ed01e7d9 100644 --- a/src/test/abilities/battle_bond.test.ts +++ b/src/test/abilities/battle_bond.test.ts @@ -1,8 +1,9 @@ import { allMoves, MultiHitAttr, MultiHitType } from "#app/data/move"; -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/src/test/abilities/disguise.test.ts b/src/test/abilities/disguise.test.ts index f44782207cb..07a84bd7a5a 100644 --- a/src/test/abilities/disguise.test.ts +++ b/src/test/abilities/disguise.test.ts @@ -1,10 +1,10 @@ import { BattlerIndex } from "#app/battle"; -import { StatusEffect } from "#app/data/status-effect"; import { toDmgValue } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/flash_fire.test.ts b/src/test/abilities/flash_fire.test.ts index f03e1689649..0ca55227ea4 100644 --- a/src/test/abilities/flash_fire.test.ts +++ b/src/test/abilities/flash_fire.test.ts @@ -1,11 +1,11 @@ import { BattlerIndex } from "#app/battle"; -import { StatusEffect } from "#app/data/status-effect"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Species } from "#app/enums/species"; import { MovePhase } from "#app/phases/move-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/galvanize.test.ts b/src/test/abilities/galvanize.test.ts index 438f166174d..80e767866ea 100644 --- a/src/test/abilities/galvanize.test.ts +++ b/src/test/abilities/galvanize.test.ts @@ -1,6 +1,6 @@ import { BattlerIndex } from "#app/battle"; import { allMoves } from "#app/data/move"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; diff --git a/src/test/abilities/libero.test.ts b/src/test/abilities/libero.test.ts index aac1cb16d97..e7bc9eeeb63 100644 --- a/src/test/abilities/libero.test.ts +++ b/src/test/abilities/libero.test.ts @@ -1,6 +1,6 @@ import { allMoves } from "#app/data/move"; -import { Type } from "#app/data/type"; -import { Weather, WeatherType } from "#app/data/weather"; +import { Type } from "#enums/type"; +import { Weather } from "#app/data/weather"; import { PlayerPokemon } from "#app/field/pokemon"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; @@ -8,6 +8,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; diff --git a/src/test/abilities/magic_guard.test.ts b/src/test/abilities/magic_guard.test.ts index 8075eac66f2..7c038354748 100644 --- a/src/test/abilities/magic_guard.test.ts +++ b/src/test/abilities/magic_guard.test.ts @@ -1,12 +1,13 @@ import { ArenaTagSide, getArenaTag } from "#app/data/arena-tag"; -import { StatusEffect, getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; -import { WeatherType } from "#app/data/weather"; +import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/mimicry.test.ts b/src/test/abilities/mimicry.test.ts index 8f22de33061..29aa1d649d3 100644 --- a/src/test/abilities/mimicry.test.ts +++ b/src/test/abilities/mimicry.test.ts @@ -1,7 +1,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/parental_bond.test.ts b/src/test/abilities/parental_bond.test.ts index 993a5eb8344..d8f952ae6ad 100644 --- a/src/test/abilities/parental_bond.test.ts +++ b/src/test/abilities/parental_bond.test.ts @@ -1,11 +1,11 @@ -import { Stat } from "#enums/stat"; -import { StatusEffect } from "#app/data/status-effect"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { toDmgValue } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/pastel_veil.test.ts b/src/test/abilities/pastel_veil.test.ts index 6f09fd8ff06..dd8360493a1 100644 --- a/src/test/abilities/pastel_veil.test.ts +++ b/src/test/abilities/pastel_veil.test.ts @@ -1,10 +1,10 @@ import { BattlerIndex } from "#app/battle"; -import { StatusEffect } from "#app/data/status-effect"; import { Abilities } from "#app/enums/abilities"; import { CommandPhase } from "#app/phases/command-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/power_construct.test.ts b/src/test/abilities/power_construct.test.ts index bb80e9f3ac8..aaab5ddb5c4 100644 --- a/src/test/abilities/power_construct.test.ts +++ b/src/test/abilities/power_construct.test.ts @@ -1,9 +1,10 @@ -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; diff --git a/src/test/abilities/protean.test.ts b/src/test/abilities/protean.test.ts index 5f86ec4c6e6..0d7202e3f6d 100644 --- a/src/test/abilities/protean.test.ts +++ b/src/test/abilities/protean.test.ts @@ -1,6 +1,6 @@ import { allMoves } from "#app/data/move"; -import { Type } from "#app/data/type"; -import { Weather, WeatherType } from "#app/data/weather"; +import { Type } from "#enums/type"; +import { Weather } from "#app/data/weather"; import { PlayerPokemon } from "#app/field/pokemon"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; @@ -8,6 +8,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; diff --git a/src/test/abilities/sand_veil.test.ts b/src/test/abilities/sand_veil.test.ts index c62357f17af..ee8ca450df9 100644 --- a/src/test/abilities/sand_veil.test.ts +++ b/src/test/abilities/sand_veil.test.ts @@ -1,12 +1,12 @@ import { StatMultiplierAbAttr, allAbilities } from "#app/data/ability"; -import { Stat } from "#enums/stat"; -import { WeatherType } from "#app/data/weather"; import { CommandPhase } from "#app/phases/command-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; diff --git a/src/test/abilities/schooling.test.ts b/src/test/abilities/schooling.test.ts index 5f953dbf9ab..e1ec58f517e 100644 --- a/src/test/abilities/schooling.test.ts +++ b/src/test/abilities/schooling.test.ts @@ -1,9 +1,10 @@ -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; diff --git a/src/test/abilities/shields_down.test.ts b/src/test/abilities/shields_down.test.ts index fbb2e96e463..6100d3e04d9 100644 --- a/src/test/abilities/shields_down.test.ts +++ b/src/test/abilities/shields_down.test.ts @@ -1,9 +1,10 @@ -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; diff --git a/src/test/abilities/synchronize.test.ts b/src/test/abilities/synchronize.test.ts index f30ab9d3db4..2ae80ae9c7a 100644 --- a/src/test/abilities/synchronize.test.ts +++ b/src/test/abilities/synchronize.test.ts @@ -1,7 +1,7 @@ -import { StatusEffect } from "#app/data/status-effect"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/zen_mode.test.ts b/src/test/abilities/zen_mode.test.ts index 74575415788..4ba5e3d5929 100644 --- a/src/test/abilities/zen_mode.test.ts +++ b/src/test/abilities/zen_mode.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { DamagePhase } from "#app/phases/damage-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { Mode } from "#app/ui/ui"; @@ -7,6 +7,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; import { SwitchType } from "#enums/switch-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; diff --git a/src/test/abilities/zero_to_hero.test.ts b/src/test/abilities/zero_to_hero.test.ts index 160b43abf1e..5702f73e6c4 100644 --- a/src/test/abilities/zero_to_hero.test.ts +++ b/src/test/abilities/zero_to_hero.test.ts @@ -1,9 +1,10 @@ -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/arena/weather_fog.test.ts b/src/test/arena/weather_fog.test.ts index db591dda2b8..8c1fcb1e3a4 100644 --- a/src/test/arena/weather_fog.test.ts +++ b/src/test/arena/weather_fog.test.ts @@ -1,9 +1,9 @@ import { allMoves } from "#app/data/move"; -import { WeatherType } from "#app/data/weather"; import { Abilities } from "#app/enums/abilities"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/src/test/arena/weather_hail.test.ts b/src/test/arena/weather_hail.test.ts index b8f4276a3d9..0b267550d75 100644 --- a/src/test/arena/weather_hail.test.ts +++ b/src/test/arena/weather_hail.test.ts @@ -1,10 +1,10 @@ -import { WeatherType } from "#app/data/weather"; +import { BattlerIndex } from "#app/battle"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { BattlerIndex } from "#app/battle"; describe("Weather - Hail", () => { let phaserGame: Phaser.Game; diff --git a/src/test/arena/weather_sandstorm.test.ts b/src/test/arena/weather_sandstorm.test.ts index 09d0c1d2b77..17ccfdee94b 100644 --- a/src/test/arena/weather_sandstorm.test.ts +++ b/src/test/arena/weather_sandstorm.test.ts @@ -1,8 +1,8 @@ -import { WeatherType } from "#app/data/weather"; import { Abilities } from "#app/enums/abilities"; import { Stat } from "#app/enums/stat"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/battle/double_battle.test.ts b/src/test/battle/double_battle.test.ts index a7585d55bab..1fc24bfc027 100644 --- a/src/test/battle/double_battle.test.ts +++ b/src/test/battle/double_battle.test.ts @@ -1,8 +1,9 @@ -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/battle/inverse_battle.test.ts b/src/test/battle/inverse_battle.test.ts index 110f92d15b7..0bda678bbd3 100644 --- a/src/test/battle/inverse_battle.test.ts +++ b/src/test/battle/inverse_battle.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { Challenges } from "#enums/challenges"; diff --git a/src/test/data/status-effect.test.ts b/src/test/data/status-effect.test.ts index 1b1a97fc51f..4831e8de5de 100644 --- a/src/test/data/status-effect.test.ts +++ b/src/test/data/status-effect.test.ts @@ -1,6 +1,5 @@ import { Status, - StatusEffect, getStatusEffectActivationText, getStatusEffectDescriptor, getStatusEffectHealText, @@ -11,6 +10,7 @@ import { MoveResult } from "#app/field/pokemon"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { mockI18next } from "#test/utils/testUtils"; import i18next from "i18next"; diff --git a/src/test/field/pokemon.test.ts b/src/test/field/pokemon.test.ts index ce9862e22e4..0bfbd03e9d9 100644 --- a/src/test/field/pokemon.test.ts +++ b/src/test/field/pokemon.test.ts @@ -1,7 +1,7 @@ import { Species } from "#app/enums/species"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import GameManager from "../utils/gameManager"; -import { PokeballType } from "#app/enums/pokeball"; +import { PokeballType } from "#enums/pokeball"; import BattleScene from "#app/battle-scene"; import { Moves } from "#app/enums/moves"; diff --git a/src/test/final_boss.test.ts b/src/test/final_boss.test.ts index de2cddff944..5540d9511e4 100644 --- a/src/test/final_boss.test.ts +++ b/src/test/final_boss.test.ts @@ -1,12 +1,12 @@ -import { StatusEffect } from "#app/data/status-effect"; -import { Abilities } from "#app/enums/abilities"; -import { Biome } from "#app/enums/biome"; -import { Moves } from "#app/enums/moves"; -import { Species } from "#app/enums/species"; import { GameModes } from "#app/game-mode"; import { TurnHeldItemTransferModifier } from "#app/modifier/modifier"; +import { Abilities } from "#enums/abilities"; +import { Biome } from "#enums/biome"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import GameManager from "./utils/gameManager"; const FinalWave = { Classic: 200, diff --git a/src/test/items/toxic_orb.test.ts b/src/test/items/toxic_orb.test.ts index 583e302126c..6918d7f34f0 100644 --- a/src/test/items/toxic_orb.test.ts +++ b/src/test/items/toxic_orb.test.ts @@ -1,8 +1,8 @@ -import { StatusEffect } from "#app/data/status-effect"; import i18next from "#app/plugins/i18n"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/src/test/moves/aurora_veil.test.ts b/src/test/moves/aurora_veil.test.ts index e68117a2f59..2d7484b4eb5 100644 --- a/src/test/moves/aurora_veil.test.ts +++ b/src/test/moves/aurora_veil.test.ts @@ -1,13 +1,13 @@ import { ArenaTagSide } from "#app/data/arena-tag"; import Move, { allMoves } from "#app/data/move"; -import { WeatherType } from "#app/data/weather"; -import { Abilities } from "#app/enums/abilities"; import { ArenaTagType } from "#app/enums/arena-tag-type"; import Pokemon from "#app/field/pokemon"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { NumberHolder } from "#app/utils"; +import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/camouflage.test.ts b/src/test/moves/camouflage.test.ts index acf37635c47..5773afffcc3 100644 --- a/src/test/moves/camouflage.test.ts +++ b/src/test/moves/camouflage.test.ts @@ -2,7 +2,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { TerrainType } from "#app/data/terrain"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { BattlerIndex } from "#app/battle"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; diff --git a/src/test/moves/dragon_cheer.test.ts b/src/test/moves/dragon_cheer.test.ts index 37b74d44360..750f09214ca 100644 --- a/src/test/moves/dragon_cheer.test.ts +++ b/src/test/moves/dragon_cheer.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; import { Abilities } from "#enums/abilities"; diff --git a/src/test/moves/dragon_rage.test.ts b/src/test/moves/dragon_rage.test.ts index 595f347a6b5..e8185f013e5 100644 --- a/src/test/moves/dragon_rage.test.ts +++ b/src/test/moves/dragon_rage.test.ts @@ -1,5 +1,5 @@ import { Stat } from "#enums/stat"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Species } from "#app/enums/species"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { modifierTypes } from "#app/modifier/modifier-type"; diff --git a/src/test/moves/effectiveness.test.ts b/src/test/moves/effectiveness.test.ts index d1903c79844..829d4533f9b 100644 --- a/src/test/moves/effectiveness.test.ts +++ b/src/test/moves/effectiveness.test.ts @@ -1,7 +1,7 @@ import { allMoves } from "#app/data/move"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { TrainerSlot } from "#app/data/trainer-config"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; diff --git a/src/test/moves/electrify.test.ts b/src/test/moves/electrify.test.ts index 5d15a825688..8015dd0a74d 100644 --- a/src/test/moves/electrify.test.ts +++ b/src/test/moves/electrify.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; diff --git a/src/test/moves/flower_shield.test.ts b/src/test/moves/flower_shield.test.ts index 1a5cd326fd8..4c03df5212b 100644 --- a/src/test/moves/flower_shield.test.ts +++ b/src/test/moves/flower_shield.test.ts @@ -1,6 +1,6 @@ import { Stat } from "#enums/stat"; import { SemiInvulnerableTag } from "#app/data/battler-tags"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Biome } from "#app/enums/biome"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; diff --git a/src/test/moves/forests_curse.test.ts b/src/test/moves/forests_curse.test.ts index c49bdab5255..010b00599a5 100644 --- a/src/test/moves/forests_curse.test.ts +++ b/src/test/moves/forests_curse.test.ts @@ -1,7 +1,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/fusion_flare.test.ts b/src/test/moves/fusion_flare.test.ts index 162cefcfb1e..75641c04d02 100644 --- a/src/test/moves/fusion_flare.test.ts +++ b/src/test/moves/fusion_flare.test.ts @@ -1,7 +1,7 @@ -import { StatusEffect } from "#app/data/status-effect"; import { TurnStartPhase } from "#app/phases/turn-start-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/heal_block.test.ts b/src/test/moves/heal_block.test.ts index 252533215a8..25f2076ff3e 100644 --- a/src/test/moves/heal_block.test.ts +++ b/src/test/moves/heal_block.test.ts @@ -1,12 +1,12 @@ import { BattlerIndex } from "#app/battle"; import { ArenaTagSide } from "#app/data/arena-tag"; -import { WeatherType } from "#app/data/weather"; import GameManager from "#app/test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/nightmare.test.ts b/src/test/moves/nightmare.test.ts index f4c485ff1b4..850b0793b1e 100644 --- a/src/test/moves/nightmare.test.ts +++ b/src/test/moves/nightmare.test.ts @@ -1,7 +1,7 @@ -import { StatusEffect } from "#app/data/status-effect"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/plasma_fists.test.ts b/src/test/moves/plasma_fists.test.ts index 24210cd47fa..4075c1ab988 100644 --- a/src/test/moves/plasma_fists.test.ts +++ b/src/test/moves/plasma_fists.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; diff --git a/src/test/moves/pledge_moves.test.ts b/src/test/moves/pledge_moves.test.ts index 93fcf57cc60..64d586e7ba4 100644 --- a/src/test/moves/pledge_moves.test.ts +++ b/src/test/moves/pledge_moves.test.ts @@ -2,7 +2,7 @@ import { BattlerIndex } from "#app/battle"; import { allAbilities } from "#app/data/ability"; import { ArenaTagSide } from "#app/data/arena-tag"; import { allMoves, FlinchAttr } from "#app/data/move"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { ArenaTagType } from "#enums/arena-tag-type"; import { Stat } from "#enums/stat"; import { toDmgValue } from "#app/utils"; diff --git a/src/test/moves/purify.test.ts b/src/test/moves/purify.test.ts index 3b07eafd75a..171f94a611a 100644 --- a/src/test/moves/purify.test.ts +++ b/src/test/moves/purify.test.ts @@ -1,9 +1,10 @@ import { BattlerIndex } from "#app/battle"; -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; diff --git a/src/test/moves/reflect_type.test.ts b/src/test/moves/reflect_type.test.ts index 0e47d4b00fc..50e0fc2fbe6 100644 --- a/src/test/moves/reflect_type.test.ts +++ b/src/test/moves/reflect_type.test.ts @@ -1,7 +1,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/relic_song.test.ts b/src/test/moves/relic_song.test.ts index eb877b6054d..c09514850eb 100644 --- a/src/test/moves/relic_song.test.ts +++ b/src/test/moves/relic_song.test.ts @@ -1,4 +1,4 @@ -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Challenges } from "#app/enums/challenges"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; diff --git a/src/test/moves/roost.test.ts b/src/test/moves/roost.test.ts index e595f879844..69301dc86cf 100644 --- a/src/test/moves/roost.test.ts +++ b/src/test/moves/roost.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; diff --git a/src/test/moves/substitute.test.ts b/src/test/moves/substitute.test.ts index 92f66c967c4..14ab4ab5054 100644 --- a/src/test/moves/substitute.test.ts +++ b/src/test/moves/substitute.test.ts @@ -2,7 +2,11 @@ import { BattlerIndex } from "#app/battle"; import { ArenaTagSide } from "#app/data/arena-tag"; import { SubstituteTag, TrappedTag } from "#app/data/battler-tags"; import { allMoves, StealHeldItemChanceAttr } from "#app/data/move"; -import { StatusEffect } from "#app/data/status-effect"; +import { MoveResult } from "#app/field/pokemon"; +import { CommandPhase } from "#app/phases/command-phase"; +import GameManager from "#app/test/utils/gameManager"; +import { Command } from "#app/ui/command-ui-handler"; +import { Mode } from "#app/ui/ui"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -10,11 +14,7 @@ import { BerryType } from "#enums/berry-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; -import { MoveResult } from "#app/field/pokemon"; -import { CommandPhase } from "#app/phases/command-phase"; -import GameManager from "#app/test/utils/gameManager"; -import { Command } from "#app/ui/command-ui-handler"; -import { Mode } from "#app/ui/ui"; +import { StatusEffect } from "#enums/status-effect"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/src/test/moves/tar_shot.test.ts b/src/test/moves/tar_shot.test.ts index 4734da366e4..5fb70abc19c 100644 --- a/src/test/moves/tar_shot.test.ts +++ b/src/test/moves/tar_shot.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; import { Stat } from "#app/enums/stat"; diff --git a/src/test/moves/tera_blast.test.ts b/src/test/moves/tera_blast.test.ts index 0ce8a552105..311ac0f0d0e 100644 --- a/src/test/moves/tera_blast.test.ts +++ b/src/test/moves/tera_blast.test.ts @@ -1,7 +1,7 @@ import { BattlerIndex } from "#app/battle"; import { Stat } from "#enums/stat"; import { allMoves } from "#app/data/move"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#app/enums/abilities"; import { HitResult } from "#app/field/pokemon"; import { Moves } from "#enums/moves"; diff --git a/src/test/moves/tera_starstorm.test.ts b/src/test/moves/tera_starstorm.test.ts index c7b6b66c1ce..22dd5b3c4d1 100644 --- a/src/test/moves/tera_starstorm.test.ts +++ b/src/test/moves/tera_starstorm.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; diff --git a/src/test/moves/thunder_wave.test.ts b/src/test/moves/thunder_wave.test.ts index 03e9ebb94f3..5551451e59b 100644 --- a/src/test/moves/thunder_wave.test.ts +++ b/src/test/moves/thunder_wave.test.ts @@ -1,8 +1,8 @@ -import { StatusEffect } from "#app/data/status-effect"; -import { Abilities } from "#app/enums/abilities"; import { EnemyPokemon } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/toxic_spikes.test.ts b/src/test/moves/toxic_spikes.test.ts index a5c63a2652f..bdd59ed0ac8 100644 --- a/src/test/moves/toxic_spikes.test.ts +++ b/src/test/moves/toxic_spikes.test.ts @@ -1,10 +1,10 @@ import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; -import { StatusEffect } from "#app/data/status-effect"; import { decrypt, encrypt, GameData, SessionSaveData } from "#app/system/game-data"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/trick_or_treat.test.ts b/src/test/moves/trick_or_treat.test.ts index 7ecd00ed076..5c85cac05e2 100644 --- a/src/test/moves/trick_or_treat.test.ts +++ b/src/test/moves/trick_or_treat.test.ts @@ -1,7 +1,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/mystery-encounter/encounter-test-utils.ts b/src/test/mystery-encounter/encounter-test-utils.ts index f95a442d4c2..cd2fd2db042 100644 --- a/src/test/mystery-encounter/encounter-test-utils.ts +++ b/src/test/mystery-encounter/encounter-test-utils.ts @@ -1,18 +1,19 @@ -import { Button } from "#app/enums/buttons"; -import { MysteryEncounterBattlePhase, MysteryEncounterOptionSelectedPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; -import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler"; -import { Mode } from "#app/ui/ui"; -import GameManager from "../utils/gameManager"; -import MessageUiHandler from "#app/ui/message-ui-handler"; -import { Status, StatusEffect } from "#app/data/status-effect"; -import { expect, vi } from "vitest"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { Status } from "#app/data/status-effect"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MessagePhase } from "#app/phases/message-phase"; +import { MysteryEncounterBattlePhase, MysteryEncounterOptionSelectedPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; +import { VictoryPhase } from "#app/phases/victory-phase"; +import MessageUiHandler from "#app/ui/message-ui-handler"; +import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler"; import PartyUiHandler from "#app/ui/party-ui-handler"; import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; +import { Mode } from "#app/ui/ui"; import { isNullOrUndefined } from "#app/utils"; -import { CommandPhase } from "#app/phases/command-phase"; -import { VictoryPhase } from "#app/phases/victory-phase"; -import { MessagePhase } from "#app/phases/message-phase"; +import { Button } from "#enums/buttons"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/utils/gameManager"; +import { expect, vi } from "vitest"; /** * Runs a {@linkcode MysteryEncounter} to either the start of a battle, or to the {@linkcode MysteryEncounterRewardsPhase}, depending on the option selected diff --git a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index 7ea1f883bd1..a403a306b3d 100644 --- a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -27,7 +27,7 @@ import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { BerryType } from "#enums/berry-type"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { CommandPhase } from "#app/phases/command-phase"; import { MovePhase } from "#app/phases/move-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; diff --git a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index 92555e98679..215cab5c65a 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -1,8 +1,8 @@ import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; -import { Biome } from "#app/enums/biome"; -import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; -import { Species } from "#app/enums/species"; -import GameManager from "#app/test/utils/gameManager"; +import { Biome } from "#enums/biome"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { FieryFalloutEncounter } from "#app/data/mystery-encounters/encounters/fiery-fallout-encounter"; import { Gender } from "#app/data/gender"; @@ -13,8 +13,8 @@ import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRu import { Moves } from "#enums/moves"; import BattleScene from "#app/battle-scene"; import { AttackTypeBoosterModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { Type } from "#app/data/type"; -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Type } from "#enums/type"; +import { Status } from "#app/data/status-effect"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -25,6 +25,7 @@ import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Abilities } from "#enums/abilities"; import i18next from "i18next"; +import { StatusEffect } from "#enums/status-effect"; const namespace = "mysteryEncounters/fieryFallout"; /** Arcanine and Ninetails for 2 Fire types. Lapras, Gengar, Abra for burnable mon. */ diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index be0e6e68b5e..ae725f3480a 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -1,8 +1,8 @@ import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; -import { Biome } from "#app/enums/biome"; -import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; -import { Species } from "#app/enums/species"; -import GameManager from "#app/test/utils/gameManager"; +import { Biome } from "#enums/biome"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import * as BattleAnims from "#app/data/battle-anims"; @@ -11,7 +11,7 @@ import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } f import { Moves } from "#enums/moves"; import BattleScene from "#app/battle-scene"; import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-encounter"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { BerryType } from "#enums/berry-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { PokemonMove } from "#app/field/pokemon"; @@ -25,7 +25,7 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CommandPhase } from "#app/phases/command-phase"; import { MovePhase } from "#app/phases/move-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; -import { Abilities } from "#app/enums/abilities"; +import { Abilities } from "#enums/abilities"; const namespace = "mysteryEncounters/theStrongStuff"; const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ]; diff --git a/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts index 2653b76ab7c..701a3c94add 100644 --- a/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -1,9 +1,9 @@ import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; -import { Biome } from "#app/enums/biome"; -import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; -import { Species } from "#app/enums/species"; -import GameManager from "#app/test/utils/gameManager"; +import { Biome } from "#enums/biome"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import BattleScene from "#app/battle-scene"; @@ -19,12 +19,13 @@ import { Nature } from "#enums/nature"; import { Moves } from "#enums/moves"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { TheWinstrateChallengeEncounter } from "#app/data/mystery-encounters/encounters/the-winstrate-challenge-encounter"; -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; import { CommandPhase } from "#app/phases/command-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { PartyHealPhase } from "#app/phases/party-heal-phase"; import { VictoryPhase } from "#app/phases/victory-phase"; +import { StatusEffect } from "#enums/status-effect"; const namespace = "mysteryEncounters/theWinstrateChallenge"; const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ]; diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index b57a88d5cad..ab2d5f052c0 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -1,17 +1,17 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import GameManager from "#app/test/utils/gameManager"; -import Phaser from "phaser"; -import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; -import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import { StatusEffect } from "#app/data/status-effect"; -import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; import { speciesStarterCosts } from "#app/data/balance/starters"; -import { Type } from "#app/data/type"; -import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, getRandomPlayerPokemon, getRandomSpeciesByStarterTier, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { getEncounterText, queueEncounterMessage, showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, getRandomPlayerPokemon, getRandomSpeciesByStarterTier, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Type } from "#enums/type"; import { MessagePhase } from "#app/phases/message-phase"; +import GameManager from "#app/test/utils/gameManager"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Mystery Encounter Utils", () => { let phaserGame: Phaser.Game; diff --git a/src/test/phases/form-change-phase.test.ts b/src/test/phases/form-change-phase.test.ts index 70ca429672f..3c0016260a3 100644 --- a/src/test/phases/form-change-phase.test.ts +++ b/src/test/phases/form-change-phase.test.ts @@ -4,7 +4,7 @@ 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 { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { modifierTypes } from "#app/modifier/modifier-type"; diff --git a/src/test/ui/starter-select.test.ts b/src/test/ui/starter-select.test.ts index 8f86d62c546..15af3619ed3 100644 --- a/src/test/ui/starter-select.test.ts +++ b/src/test/ui/starter-select.test.ts @@ -1,5 +1,5 @@ import { Gender } from "#app/data/gender"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { allSpecies } from "#app/data/pokemon-species"; import { GameModes } from "#app/game-mode"; import { EncounterPhase } from "#app/phases/encounter-phase"; diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index 404f5c34a26..02950d497ee 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -1,19 +1,20 @@ -import { StatusEffect } from "#app/data/status-effect"; -import { Weather, WeatherType } from "#app/data/weather"; +import { Variant } from "#app/data/variant"; +import { Weather } from "#app/data/weather"; import { Abilities } from "#app/enums/abilities"; -import { Biome } from "#app/enums/biome"; -import { Moves } from "#app/enums/moves"; -import { Species } from "#app/enums/species"; import * as GameMode from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode"; import { ModifierOverride } from "#app/modifier/modifier-type"; import Overrides from "#app/overrides"; +import { Unlockables } from "#app/system/unlockables"; +import { Biome } from "#enums/biome"; +import { Moves } from "#enums/moves"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import type { WeatherType } from "#enums/weather-type"; import { vi } from "vitest"; import { GameManagerHelper } from "./gameManagerHelper"; -import { Unlockables } from "#app/system/unlockables"; -import { Variant } from "#app/data/variant"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; /** * Helper to handle overrides in tests diff --git a/src/ui/arena-flyout.ts b/src/ui/arena-flyout.ts index a82f97244cd..573cb85db70 100644 --- a/src/ui/arena-flyout.ts +++ b/src/ui/arena-flyout.ts @@ -1,7 +1,7 @@ import { addTextObject, TextStyle } from "./text"; import BattleScene from "#app/battle-scene"; import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; -import { WeatherType } from "#app/data/weather"; +import { WeatherType } from "#enums/weather-type"; import { TerrainType } from "#app/data/terrain"; import { addWindow, WindowVariant } from "./ui-theme"; import { ArenaEvent, ArenaEventType, TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena"; diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 1d97998f491..72447988bdd 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -3,9 +3,10 @@ import { getLevelTotalExp, getLevelRelExp } from "../data/exp"; import * as Utils from "../utils"; import { addTextObject, TextStyle } from "./text"; import { getGenderSymbol, getGenderColor, Gender } from "../data/gender"; -import { StatusEffect } from "../data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import BattleScene from "../battle-scene"; -import { Type, getTypeRgb } from "../data/type"; +import { getTypeRgb } from "#app/data/type"; +import { Type } from "#enums/type"; import { getVariantTint } from "#app/data/variant"; import { Stat } from "#enums/stat"; import BattleFlyout from "./battle-flyout"; diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index ee6641a1a27..eaf504495d5 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -1,6 +1,7 @@ import BattleScene, { InfoToggle } from "../battle-scene"; import { addTextObject, TextStyle } from "./text"; -import { getTypeDamageMultiplierColor, Type } from "../data/type"; +import { getTypeDamageMultiplierColor } from "#app/data/type"; +import { Type } from "#enums/type"; import { Command } from "./command-ui-handler"; import { Mode } from "./ui"; import UiHandler from "./ui-handler"; diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 3f89ebe415f..a0358b5ca8c 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -1,6 +1,6 @@ import BattleScene from "../battle-scene"; import { getPlayerShopModifierTypeOptionsForWave, ModifierTypeOption, TmModifierType } from "../modifier/modifier-type"; -import { getPokeballAtlasKey, PokeballType } from "../data/pokeball"; +import { getPokeballAtlasKey } from "#app/data/pokeball"; import { addTextObject, getTextStyleOptions, getModifierTierTextTint, getTextColor, TextStyle } from "./text"; import AwaitableUiHandler from "./awaitable-ui-handler"; import { Mode } from "./ui"; @@ -15,6 +15,7 @@ import i18next from "i18next"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import { IntegerHolder } from "./../utils"; import Phaser from "phaser"; +import type { PokeballType } from "#enums/pokeball"; export const SHOP_OPTIONS_ROW_LIMIT = 7; const SINGLE_SHOP_ROW_YOFFSET = 12; diff --git a/src/ui/move-info-overlay.ts b/src/ui/move-info-overlay.ts index 6c58d32c515..d9c4200ea9b 100644 --- a/src/ui/move-info-overlay.ts +++ b/src/ui/move-info-overlay.ts @@ -3,7 +3,7 @@ import { TextStyle, addTextObject } from "./text"; import { addWindow } from "./ui-theme"; import * as Utils from "../utils"; import Move, { MoveCategory } from "../data/move"; -import { Type } from "../data/type"; +import { Type } from "#enums/type"; import i18next from "i18next"; export interface MoveInfoOverlaySettings { diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index a26aa572ef3..bd3561dd0b4 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -8,7 +8,7 @@ import * as Utils from "#app/utils"; import { PokemonFormChangeItemModifier, PokemonHeldItemModifier, SwitchEffectTransferModifier } from "#app/modifier/modifier"; import { allMoves, ForceSwitchOutAttr } from "#app/data/move"; import { getGenderColor, getGenderSymbol } from "#app/data/gender"; -import { StatusEffect } from "#app/data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { addWindow } from "#app/ui/ui-theme"; diff --git a/src/ui/pokemon-hatch-info-container.ts b/src/ui/pokemon-hatch-info-container.ts index 146d70522fd..494855d20fa 100644 --- a/src/ui/pokemon-hatch-info-container.ts +++ b/src/ui/pokemon-hatch-info-container.ts @@ -1,7 +1,7 @@ import PokemonInfoContainer from "#app/ui/pokemon-info-container"; import BattleScene from "#app/battle-scene"; import { Gender } from "#app/data/gender"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import * as Utils from "#app/utils"; import { TextStyle, addTextObject } from "#app/ui/text"; import { speciesEggMoves } from "#app/data/balance/egg-moves"; diff --git a/src/ui/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index 5b11aff43b1..ead24e6d92f 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -3,7 +3,7 @@ import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import BattleScene from "../battle-scene"; import { Gender, getGenderColor, getGenderSymbol } from "../data/gender"; import { getNatureName } from "../data/nature"; -import { Type } from "../data/type"; +import { Type } from "#enums/type"; import Pokemon from "../field/pokemon"; import i18next from "i18next"; import { DexAttr, DexEntry, StarterDataEntry } from "../system/game-data"; diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 4975f05b8a3..82d390016f7 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -15,7 +15,8 @@ import { TrainerVariant } from "../field/trainer"; import { Challenges } from "#enums/challenges"; import { getLuckString, getLuckTextTint } from "../modifier/modifier-type"; import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle"; -import { Type, getTypeRgb } from "../data/type"; +import { getTypeRgb } from "#app/data/type"; +import { Type } from "#enums/type"; import { TypeColor, TypeShadow } from "#app/enums/color"; import { getNatureStatMultiplier, getNatureName } from "../data/nature"; import { getVariantTint } from "#app/data/variant"; diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index bb4dc74d44e..a08b7ceaec6 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -10,13 +10,13 @@ import { speciesEggMoves } from "#app/data/balance/egg-moves"; import { GrowthRate, getGrowthRateColor } from "#app/data/exp"; import { Gender, getGenderColor, getGenderSymbol } from "#app/data/gender"; import { allMoves } from "#app/data/move"; -import { Nature, getNatureName } from "#app/data/nature"; +import { getNatureName } from "#app/data/nature"; import { pokemonFormChanges } from "#app/data/pokemon-forms"; import { LevelMoves, pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; import PokemonSpecies, { allSpecies, getPokemonSpeciesForm, getPokerusStarters } from "#app/data/pokemon-species"; import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters"; import { starterPassiveAbilities } from "#app/data/balance/passives"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { GameModes } from "#app/game-mode"; import { AbilityAttr, DexAttr, DexAttrProps, DexEntry, StarterMoveset, StarterAttributes, StarterPreferences, StarterPrefs } from "#app/system/game-data"; import { Tutorial, handleTutorial } from "#app/tutorial"; @@ -50,6 +50,7 @@ import { TitlePhase } from "#app/phases/title-phase"; import { Abilities } from "#enums/abilities"; import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters"; import { BooleanHolder, capitalizeString, fixedInt, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, randIntRange, rgbHexToRgba, toReadableString } from "#app/utils"; +import type { Nature } from "#enums/nature"; export type StarterSelectCallback = (starters: Starter[]) => void; diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 4b36867571e..2e05f3de4c1 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -5,16 +5,17 @@ import * as Utils from "#app/utils"; import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { argbFromRgba } from "@material/material-color-utilities"; -import { Type, getTypeRgb } from "#app/data/type"; +import { getTypeRgb } from "#app/data/type"; +import { Type } from "#enums/type"; import { TextStyle, addBBCodeTextObject, addTextObject, getBBCodeFrag } from "#app/ui/text"; import Move, { MoveCategory } from "#app/data/move"; import { getPokeballAtlasKey } from "#app/data/pokeball"; import { getGenderColor, getGenderSymbol } from "#app/data/gender"; import { getLevelRelExp, getLevelTotalExp } from "#app/data/exp"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { StatusEffect } from "#app/data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import { getBiomeName } from "#app/data/balance/biomes"; -import { Nature, getNatureName, getNatureStatMultiplier } from "#app/data/nature"; +import { getNatureName, getNatureStatMultiplier } from "#app/data/nature"; import { loggedInUser } from "#app/account"; import { Variant, getVariantTint } from "#app/data/variant"; import { Button } from "#enums/buttons"; @@ -23,6 +24,7 @@ import i18next from "i18next"; import { modifierSortFunc } from "#app/modifier/modifier"; import { PlayerGender } from "#enums/player-gender"; import { Stat, PERMANENT_STATS, getStatKey } from "#enums/stat"; +import { Nature } from "#enums/nature"; enum Page { PROFILE, From de4ec5e1cb2d550b9ee9c8803faab4c6fbe552d5 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Sat, 9 Nov 2024 01:38:45 -0500 Subject: [PATCH 46/98] [Feature] [Item] Add Catching Charm item (#4811) * Add catching charm item * Add Catching Charm item * Disable catching charm in item pool when dex isn't full enough * Replace catching charm icon --------- Co-authored-by: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> --- public/images/items.png | Bin 58629 -> 59627 bytes public/images/items/catching_charm.png | Bin 454 -> 322 bytes src/data/pokeball.ts | 3 ++- src/modifier/modifier-type.ts | 4 +++- src/modifier/modifier.ts | 32 +++++++++++++++++++++++++ 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/public/images/items.png b/public/images/items.png index cb4f8fa7d065f53b4861bc32eb0867b9eda94b5d..191766f520e852149c71b728781f7feb401f4514 100644 GIT binary patch literal 59627 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 X1m@~7!j6GBz~zy~6ZIN3n^*q_+l|+J delta 58282 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
RY4!*yekQ&gfCG%r@kb`(v^8OaANm<<#%b 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
  • _n(YR4Gokl~ClQ$4;Z#bnwLvU#*=}H89kgbNdPS zu^E1Jci%?JBgm}~i#pe#C>j8`{U?e433~%2mSb$rDfiW?&+(vaV*1SZ_}jV@URdqB zw)f+GFiO3 zz9hr=f-J-Z#l$qkDWfi$MFbtO>C!aV>}PL73~I@iwv9Qv$|4NgK^<}IUE6KZ2r9Lc$+;L`6j(VwfA69p(12Kyx_v6J^^>x4PWE&WlvaEC zMVo=k6<_o3VmU5c>)ISD$7vU8$UQGSoZ}Gb8+>auZgpG-EmA2=MAA{+!YNGWZn@qa z+6+9AgH@E0GRdEw)&LfgeAfLt!G;aE@(&hSo%>@0hX9XkloTHfS7LK-A@L_-yTRI<rH1_C#Sko>A?yXPBxeX8|#y1ERg=HhN1@0O%7u2u^=SV2i9cl$`91iw_K zArGH}PB5iHI6Cd^nve(GU0`q(hON1Sc3oZ6U|Yzlc^$PI-R3e=m2^>5yRD$R*uX05-f7Y??vuTZ`-j0&F1AqO1XVcT;e~x zPM6TleF1NBjL}c)I>#JXw-bX6^x`nF6X?|IPCDh}6c8Yf3cI}d?Dofz^rGd^czB~O zBzcNa&EU+{K9J=DP7m$euH{)L?Xxwc@hr?!NIfXGHQ{>0m(kHN$G;!seNuyZj-xWa zf3zcL>vJ+Cg}2+8UPlwyMfS0uvpVc*q|0k#i?CQNK+mN|Mf`w=gl*{#ejz)bzbmYFIBpL3< zzo?$bg?EFOt)mX}X61_8iXjutvBrOi_XO7v&E5H%YW5#U86UvZ0+W31nz(u?M0)Qr z$soCWQh$Ypak9s7BewD{?OYD%8gDAs~5}P51&2!L_*rokZudu`YQs7!eriC zvW})IDN>Z+>NCnJOp92g*?gLswZB|b%|%Hu(F9oJ6r~K-`6BFWi?bf{yyp=89Y~mF zKBlQtzkCB13hKNK+D2^t z8L8CJSW8MuT1(Qf0;(Wq?|R)rSHQ43Dp((>?D$?BhlDnMz6xGqKTV%Mg7qpfQm}xD zu_g{PfqO0vZV86`#Qi3LKTL1RYrH6lBASrKdidHnEP5&Gp%pL5#jk;=JN{UygTgU(8+{FX; z1<>M4A-fe;6SzPg9QT6QI*r@L!?xzaV-KqKe%VDEZSR%q6^h~wD_i6yI$V16%$ZBT z18itj_{Bx|#U{P2J8^s@!r;k9hz0zi*)8u(Iw#vc>!*Q9(~Wk7^tZCJnWiY76h_Kq zgSbM8l+Pz~r0}e4;1xG)V1bv4dIvCrEp{B21vE4$Ktgc`JHKVgseD}Ga6@q{%v;~5 z9!xflD%x(kRQ0T#oupQ&QmKS0bonQ9{N0pQY>pjEzJqHON|G5X-SpZ%DN!R~gCu5O z#9C9gdoEL5x6Z#$wK94&$?!&_>%@65Ij(q_hXikZL5yta`xn@|kKwU!aS1i}rMnS2n66mRQX zWy=!bCp)18FO|NwRotRr-qTcYsm5B=*95ruMJyW);?|c4Iw?}31hM1oWuxO9{G|1!X8sz!?k8Q=0Nl=IbCl>T*XyCbz~4V-0&{W+@~dv?6n!)Bd~%%k;wy0|&Lmx+?aST8 z5jkP+ku9W3@j4@nfmy)W-JQBlF!HefWw9Rlf|t%N9RY1p_605K)(exn?@mVE>3C&( zhD8NaF|XM0we8Mx5xF0KO4n&+=9tA}n{wg$oqOBjkj`lFM}Zz6UQL)>EOyBUwuWZo z-6y`{I^f4ZW$#VGIohorSJWNn{a)}dtnoXRZzND5!!c5ic!S8 zZ*_F?Y7tqgXWP>!CvS*8#Rr_ejy}%UN%`^I;c}C)SyQVaVyvv2pBz=a|N94LFVJNF z{k;Y-Hu|{mkC@zVN)DbdY;A77{F@fXn^lFw+{QH5qjkiUA<-uH?_=on>Od8b(8)TB zGu@?ZDP&IJ;fx=*h~rFRv5yWdZ+<4Ic+5+~Q2tp#vR}P#WDRyi-XzS;Vaeqx50OWf z?CJlBHF;IoxsOw({13M*pALu+zI2kb8T@UlBD6<~!VUb_ zrf;aNh7UDVES_+*R$sg&O0;M2O917_(q4a)r>1!JIDhx1KufnDkxarKDP?Dk>$RfmmTDG zWBd^{L|jOcXSYvTt2>gzs_-V}cjy##uH1BiIU;XiXR-u@Jxa)rUK6CG4TODSK?=wD zb}9r!Ce}Eqc$J6zqs(b!N>k&^@h`VCcYJkHsjvFGj59HKz4D>?J@4Z@$<)OWs$?8ko zgD-y2qP6(h=!fYDnL3KBzl9PbfO06I(%x0X+o0p1W&&5hXDGx2v|X&+e_Qnf zGnTjNdQm%Y5%zv}o!}LcRztXE!m+eSDdxywpoY-H<7Qs$DaH7PcSmPu zSEcS3HI`$!ByvB3*Z#eZ=%))I82gb&-{b zU-CrBdalme8pxdu$rAf_do;ijSnAaUd_y*8TZFJk2}fckVYnl4a!7OcE%&Xl{g99? zS~#u--|=t&X>8(Dq`O?tQPz5LV%S)e$oTO?l}0Iz(9;l-{ny#dtM#7YDIxPtPYT(= zCxD3-zUbv`j4NDNSSWf1Tyul(zXHvsK|wxKFRTp^QlzO@g1p3?9*LQwzQ=T0Bu}zE zLdGRhQ$2Xjb^})&N6*SG#KPkv<@cE9OA*DGQHv1igR>7h&J7hUkENuYh}F@>Wua4o z)3g*t=6MibPP%$Pd4Ap>+Q|m{$8szZB_$UBU;^I;Umkz9V2vqnT_3ghk&GLJq@?1m zcXR>#yoNv5vFmT%933ec5Cuoma+UsErBSlA=%o2I6@a`BdMBy%IOY3vyDavCl?uX` z$pIOG(C7)L@2tog7;r@r)cd2-fyBZXMQsfY2vnW|#aem?jdKc(U*~=E#J(+q^(|Uz`c~_&srUn?|)w}-joD0x-j!+@Y=v3Q*1n% zlfN zt0~#oDE9njH@*}DEi!!Bqxs&&-tyQbYxD+Yg0JPcDepFOO9HHN5%!rJ4Yib25kA|(IV1e6zYZben}W7hwkbH=p~_^?I%~R$odS$s15f<(V+L4_gn44 zE24i@HdR*B%OCntfp^o`O1$}>`hN*A$th9!H;$rrxjo=BtO?Lwpf5DXyXDdJqr|ro zwDi|e7LuQnaM6eVJUO?@Yxlff5L416l1UHE-@_6}DKK+))#+ok4~7EcI=_#dO0awy z!!LyjXrgbht_;XDozcV-Eu8qIBo64QP7XLErsA`sFv3U}|l3M3pF~fz@rf8yttui;)ml67u4>9tT{{72xra`Ws zKoAz*FJ!cHbMC1`^!5Qw!2@hZqzh;|hdF*>O=UcX~J=)@^vT168>WP@lci^$dYus(&-_R_JEm);l@Wj&pWPm zZ=c+uBGdh#=lOPeC>9#Z%JJ=Tw>yaFd7N7;A)~*H5{f|j<_|W}+3`jGm+v?~-xV@% zIKd1k-O4B-A(5!AO}Hwo$L3ij{-Y-VNbu9y^m>l6v;+a$xiH$iEL^dA=I2`PDYg)$}7_)bV(8^H$U8deN(b)6c5~z{k#0 zD^b`qZBhQ67uGy6_CBWQqw}gzN}qs-D-e&p048$zfvW1g(sL>; zliNPZj*Sdk*{if5B85pBzpwQ>T(ar<7nqSE_EF^flT(Z!3i#i-dnT8UD=PM?yh6L3 zC}f8PNizAHeU=2ly~QDr2Z^{3RcO98=-?3~wvIHTNJ+AdnY{Q9 zNuoRplQARgjWtj%sm=t6j6V_Vac5B9F=W`ONZ;R>!M-(`&zxo4+B0Kj=|pUND_9Dt z6Ye7jtpkMnNJ%$0tEHr>J>RP+{k&^_aW;Yga*fQb-wEw1Qi{Qcd^PRRpNZl@3jTDy zgUWfwdWq&VGNSWJzc4e#&ZH|f$0kCT*%1Bxp|Aot19V6)cPoMXleu&j@8W%|BZsvn zslPJHz3lP3TkvZ<6klRZ!YiF4f}Ys`-tn%$jJtlete0%za8hsCU~g`qdmY zWD+I&wT@!>`z@5lnTb*HOGv5@=0l#*uw#N&vcGlKm|C+lvI|w5@BRKsyY<}-7!bH~ zk*l-yGp)c{e(2!9m_&Q`VRn!xR_sb@u**%0P1Id%K0DKf-|;{{Q(q^Yqmw{`47rJ& zT~j~i*wl)IWcur3LQIQtV8ce&zWwYy*W}Hp- zm}9T&Q~_T=A47-LZ&ov~!&~JzWO(Gt3YgJF^XiDPDv>`rROqU>oR7?sD}Qk`+8?fW z5K|3Nx}S5+2?i#o%gJ=5YPRF#48psG9)r@&10i%TcSY;XAHThH#{h9*I16VM$o;ou zAPxBUKP0bZQ}uxwesQn^{p z2nYxjG?Wz#jXGB8BK0jh9)&Ro%f+AE+2)Y`_!zu35Pz?oaDd3sT7#4#ieYqUh|fC% zJY9Tz-#(8z1Yr6ltMex9UTIV0F^cQeTT2al^~X~SzMl)QQBe|U5m3UEu(uJB@p?}^ zkuqxXO??kP*eF`nzVh1-%=l6^oFka=Ldm*XnpXt16D8X9skK8kxOYJMU$C#{`M}Fh z$rD=*-OF}0`;$ANZE25D?L~Et(=ty?jVdI)?VDsl`3QJ#CT}q3mKP7l%kPQPNT7GnhjH(Q%ujh)tc2&E38Lee?8^-0+*6}eT<)DmpZA5jg1AU&_lmJo^6vuE n>_abDblLA*_FdRrZX8_pGulWwDrXN7fG-V|$I7LO=-2-Tdgeal diff --git a/public/images/items/catching_charm.png b/public/images/items/catching_charm.png index 9d72fe465e3f1322e38b724e6ec333797e79f5fb..c220ff70c03408c81c400f715fa3d94f0da43788 100644 GIT binary patch delta 306 zcmX@ce28g+WIZzj1B1(wu46!ou{g-xiDBJ2nU_G0L4Z$)E09)DP>?F93dzea&lPcwu{-CPI3BF6xtBu@a z%3Bzf_z&qe%u()^7iN-kO;Y%Kk&*Am+o3K{4mBP#dL*U&?A(Ft`Vz;ZO3GEg+&jN{&7wEltcE*VLSL({Q8KJMeW#*{SzFcr lfYx20yZBNgJkxxA8MJ_G4j=}BOTm*tl&7no%Q~loCIAiueS!c0 delta 439 zcmV;o0Z9JB0>%T78Gi-<0047(dh`GQ0gXvSK~z}7?Uubu#4s4f`zLf4hrlTYI@tXd zR~MmUM^_h5C{Aupy83}|c)N&;gNq0v=W^ia=%{1I4i2w*18+%ly*Af0J%<(^C{4-p z%lkz<0Pt3+m74be{~Mt0q5t@RgD!oa)&b}rf13llJuMhUCw~ce!w_)ISp>*DP7Yzw zNpZ~t!Wuw@nA<}{!WjTTkT4Zx>CkJU^&-@ZQB4g3IRmJIQ6tg`KpBXMd8;YVQ{fDt z5b8h}Z54I!kV%=CFAboc63ver_aR`W+O#Ivv5 zm{Nk#K|pJOsvd{mRGK4%3V?-4=GFjZ7mf8Z5#E47G)rOf_ykO8M=VV24z>O40p7;B z&>XdMbtfU1GHB#L8t}q5YXCn8AwEiA?fgb6hcOwihA?BhwgwPl^K~Mn-}jbz8Us{a hG&=$4LOnna@CPb(llSSAz !!d.caughtAttr); const catchingCharmMultiplier = new NumberHolder(1); - //scene.findModifier(m => m instanceof CriticalCatchChanceBoosterModifier)?.apply(catchingCharmMultiplier); + scene.findModifier(m => m instanceof CriticalCatchChanceBoosterModifier)?.apply(catchingCharmMultiplier); const dexMultiplier = scene.gameMode.isDaily || dexCount > 800 ? 2.5 : dexCount > 600 ? 2 : dexCount > 400 ? 1.5 diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 571c54d76e9..3e459f2dc74 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -10,7 +10,7 @@ import { getStatusEffectDescriptor } from "#app/data/status-effect"; import { Type } from "#enums/type"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; +import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier, CriticalCatchChanceBoosterModifier } from "#app/modifier/modifier"; import { ModifierTier } from "#app/modifier/modifier-tier"; import Overrides from "#app/overrides"; import { Unlockables } from "#app/system/unlockables"; @@ -1554,6 +1554,7 @@ export const modifierTypes = { SHINY_CHARM: () => new ModifierType("modifierType:ModifierType.SHINY_CHARM", "shiny_charm", (type, _args) => new ShinyRateBoosterModifier(type)), ABILITY_CHARM: () => new ModifierType("modifierType:ModifierType.ABILITY_CHARM", "ability_charm", (type, _args) => new HiddenAbilityRateBoosterModifier(type)), + CATCHING_CHARM: () => new ModifierType("modifierType:ModifierType.CATCHING_CHARM", "catching_charm", (type, _args) => new CriticalCatchChanceBoosterModifier(type)), IV_SCANNER: () => new ModifierType("modifierType:ModifierType.IV_SCANNER", "scanner", (type, _args) => new IvScannerModifier(type)), @@ -1791,6 +1792,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.BATON, 2), new WeightedModifierType(modifierTypes.SOUL_DEW, 7), //new WeightedModifierType(modifierTypes.OVAL_CHARM, 6), + new WeightedModifierType(modifierTypes.CATCHING_CHARM, (party: Pokemon[]) => !party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.getSpeciesCount(d => !!d.caughtAttr) > 100 ? 4 : 0, 4), new WeightedModifierType(modifierTypes.SOOTHE_BELL, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 0 : 4), new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index d2965247826..9e97c866718 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -2957,6 +2957,38 @@ export class ShinyRateBoosterModifier extends PersistentModifier { } } +export class CriticalCatchChanceBoosterModifier extends PersistentModifier { + constructor(type: ModifierType, stackCount?: number) { + super(type, stackCount); + } + + match(modifier: Modifier): boolean { + return modifier instanceof CriticalCatchChanceBoosterModifier; + } + + clone(): CriticalCatchChanceBoosterModifier { + return new CriticalCatchChanceBoosterModifier(this.type, this.stackCount); + } + + /** + * Applies {@linkcode CriticalCatchChanceBoosterModifier} + * @param boost {@linkcode NumberHolder} holding the boost value + * @returns always `true` + */ + override apply(boost: NumberHolder): boolean { + // 1 stack: 2x + // 2 stack: 2.5x + // 3 stack: 3x + boost.value *= 1.5 + this.getStackCount() / 2; + + return true; + } + + getMaxStackCount(scene: BattleScene): number { + return 3; + } +} + export class LockModifierTiersModifier extends PersistentModifier { constructor(type: ModifierType, stackCount?: number) { super(type, stackCount); From c6cc187c96e0543a3d7c2b446890e854529f5b57 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Sat, 9 Nov 2024 04:10:49 -0500 Subject: [PATCH 47/98] [Balance] Modify potion and ether weight funcs (#4829) * Adjust for low HP mons --- src/modifier/modifier-type.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 3e459f2dc74..ae1aef3ff88 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1621,19 +1621,21 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.POKEBALL, (party: Pokemon[]) => (hasMaximumBalls(party, PokeballType.POKEBALL)) ? 0 : 6, 6), new WeightedModifierType(modifierTypes.RARE_CANDY, 2), new WeightedModifierType(modifierTypes.POTION, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 10 || p.getHpRatio() <= 0.875) && !p.isFainted()).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 10 && p.getHpRatio() <= 0.875) && !p.isFainted()).length, 3); return thresholdPartyMemberCount * 3; }, 9), new WeightedModifierType(modifierTypes.SUPER_POTION, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 25 || p.getHpRatio() <= 0.75) && !p.isFainted()).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 25 && p.getHpRatio() <= 0.75) && !p.isFainted()).length, 3); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.ETHER, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) + && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3); return thresholdPartyMemberCount * 3; }, 9), new WeightedModifierType(modifierTypes.MAX_ETHER, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) + && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.LURE, lureWeightFunc(10, 2)), @@ -1667,11 +1669,11 @@ const modifierPool: ModifierPool = { return party.filter(p => p.isFainted()).length >= Math.ceil(party.length / 2) ? 1 : 0; }, 1), new WeightedModifierType(modifierTypes.HYPER_POTION, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 100 || p.getHpRatio() <= 0.625) && !p.isFainted()).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 100 && p.getHpRatio() <= 0.625) && !p.isFainted()).length, 3); return thresholdPartyMemberCount * 3; }, 9), new WeightedModifierType(modifierTypes.MAX_POTION, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 150 || p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.FULL_RESTORE, (party: Pokemon[]) => { @@ -1681,15 +1683,17 @@ const modifierPool: ModifierPool = { } return false; })).length, 3); - const thresholdPartyMemberCount = Math.floor((Math.min(party.filter(p => (p.getInverseHp() >= 150 || p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3) + statusEffectPartyMemberCount) / 2); + const thresholdPartyMemberCount = Math.floor((Math.min(party.filter(p => (p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3) + statusEffectPartyMemberCount) / 2); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.ELIXIR, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) + && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3); return thresholdPartyMemberCount * 3; }, 9), new WeightedModifierType(modifierTypes.MAX_ELIXIR, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) + && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.DIRE_HIT, 4), From c54d21c313a6f3aafff9849b12ca772672689f09 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 9 Nov 2024 10:12:22 -0800 Subject: [PATCH 48/98] [Test] Fix flaky Wimp Out test (#4830) --- src/test/abilities/wimp_out.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/abilities/wimp_out.test.ts b/src/test/abilities/wimp_out.test.ts index 6f56a2f4e7e..df965fc340d 100644 --- a/src/test/abilities/wimp_out.test.ts +++ b/src/test/abilities/wimp_out.test.ts @@ -296,7 +296,9 @@ describe("Abilities - Wimp Out", () => { Species.TYRUNT ]); - game.move.select(Moves.SPLASH); + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); await game.phaseInterceptor.to("TurnEndPhase"); confirmNoSwitch(); From 329e43ad48519486125a54e534b7b6e01d4cac05 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sat, 9 Nov 2024 10:13:12 -0800 Subject: [PATCH 49/98] [P2] Removed incorrect calls to `resetBattleData` on switchout (#4828) --- src/field/pokemon.ts | 3 +-- src/phases/switch-summon-phase.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e0b7bf1094f..5478a6e5aaa 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4029,8 +4029,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.resetTurnData(); if (clearEffects) { this.destroySubstitute(); - this.resetSummonData(); - this.resetBattleData(); + this.resetSummonData(); // this also calls `resetBattleSummonData` } if (hideInfo) { this.hideInfo(); diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 36db8b7a7e7..51d54315165 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -138,7 +138,6 @@ export class SwitchSummonPhase extends SummonPhase { switchedInPokemon.setAlpha(0.5); } } else { - switchedInPokemon.resetBattleData(); switchedInPokemon.resetSummonData(); } this.summon(); From a763cd173dbdf193d26c3ace9c569c8a62713b7f Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sat, 9 Nov 2024 10:14:11 -0800 Subject: [PATCH 50/98] [Beta][P1-3] Fix Commander implementation bugs (#4826) --- src/battle-scene.ts | 18 +++++++++++------- src/field/pokemon.ts | 14 ++++++++++++-- src/phases/check-switch-phase.ts | 22 +++++++++++++--------- src/phases/encounter-phase.ts | 2 -- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index ed8a79125bc..c5acadc8eb6 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -774,7 +774,7 @@ export default class BattleScene extends SceneBase { /** * @returns An array of {@linkcode PlayerPokemon} filtered from the player's party - * that are {@linkcode PlayerPokemon.isAllowedInBattle | allowed in battle}. + * that are {@linkcode Pokemon.isAllowedInBattle | allowed in battle}. */ public getPokemonAllowedInBattle(): PlayerPokemon[] { return this.getPlayerParty().filter(p => p.isAllowedInBattle()); @@ -1243,23 +1243,27 @@ export default class BattleScene extends SceneBase { const lastBattle = this.currentBattle; - if (lastBattle?.double && !newDouble) { - this.tryRemovePhase(p => p instanceof SwitchPhase); - } - const maxExpLevel = this.getMaxExpLevel(); this.lastEnemyTrainer = lastBattle?.trainer ?? null; this.lastMysteryEncounter = lastBattle?.mysteryEncounter; + if (newBattleType === BattleType.MYSTERY_ENCOUNTER) { + // Disable double battle on mystery encounters (it may be re-enabled as part of encounter) + newDouble = false; + } + + if (lastBattle?.double && !newDouble) { + this.tryRemovePhase(p => p instanceof SwitchPhase); + this.getPlayerField().forEach(p => p.lapseTag(BattlerTagType.COMMANDED)); + } + this.executeWithSeedOffset(() => { this.currentBattle = new Battle(this.gameMode, newWaveIndex, newBattleType, newTrainer, newDouble); }, newWaveIndex << 3, this.waveSeed); this.currentBattle.incrementTurn(this); if (newBattleType === BattleType.MYSTERY_ENCOUNTER) { - // Disable double battle on mystery encounters (it may be re-enabled as part of encounter) - this.currentBattle.double = false; // Will generate the actual Mystery Encounter during NextEncounterPhase, to ensure it uses proper biome this.currentBattle.mysteryEncounterType = mysteryEncounterType; } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5478a6e5aaa..5d77aea248d 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -23,7 +23,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags"; import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; -import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr } from "#app/data/ability"; +import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr, CommanderAbAttr } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -3081,7 +3081,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } lapseTag(tagType: BattlerTagType): boolean { - const tags = this.summonData.tags; + const tags = this.summonData?.tags; + if (isNullOrUndefined(tags)) { + return false; + } const tag = tags.find(t => t.tagType === tagType); if (tag && !(tag.lapse(this, BattlerTagLapseType.CUSTOM))) { tag.onRemove(this); @@ -3646,6 +3649,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.triggerPokemonBattleAnim(this, PokemonAnimType.SUBSTITUTE_ADD); this.getTag(SubstituteTag)!.sourceInFocus = false; } + + // If this Pokemon has Commander and Dondozo as an active ally, hide this Pokemon's sprite. + if (this.hasAbilityWithAttr(CommanderAbAttr) + && this.scene.currentBattle.double + && this.getAlly()?.species.speciesId === Species.DONDOZO) { + this.setVisible(false); + } this.summonDataPrimer = null; } this.updateInfo(); diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index b87dff32f60..acf17c75668 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -26,25 +26,29 @@ export class CheckSwitchPhase extends BattlePhase { const pokemon = this.scene.getPlayerField()[this.fieldIndex]; + // End this phase early... + + // ...if the user is playing in Set Mode if (this.scene.battleStyle === BattleStyle.SET) { - super.end(); - return; + return super.end(); } + // ...if the checked Pokemon is somehow not on the field if (this.scene.field.getAll().indexOf(pokemon) === -1) { this.scene.unshiftPhase(new SummonMissingPhase(this.scene, this.fieldIndex)); - super.end(); - return; + return super.end(); } + // ...if there are no other allowed Pokemon in the player's party to switch with if (!this.scene.getPlayerParty().slice(1).filter(p => p.isActive()).length) { - super.end(); - return; + return super.end(); } - if (pokemon.getTag(BattlerTagType.FRENZY)) { - super.end(); - return; + // ...or if any player Pokemon has an effect that prevents the checked Pokemon from switching + if (pokemon.getTag(BattlerTagType.FRENZY) + || pokemon.isTrapped() + || this.scene.getPlayerField().some(p => p.getTag(BattlerTagType.COMMANDED))) { + return super.end(); } this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? getPokemonNameWithAffix(pokemon) : i18next.t("battle:pokemon") }), null, () => { diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 123f9ded9fc..c4d919c0325 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -36,7 +36,6 @@ import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import i18next from "i18next"; import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; -import { BattlerTagType } from "#enums/battler-tag-type"; export class EncounterPhase extends BattlePhase { private loaded: boolean; @@ -483,7 +482,6 @@ export class EncounterPhase extends BattlePhase { } } else { if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) { - this.scene.getPlayerField().forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED)); this.scene.pushPhase(new ReturnPhase(this.scene, 1)); } this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, false)); From 2bf8acea06bd0e536b3ac9c49127fd6f0797a28c Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Sat, 9 Nov 2024 13:15:24 -0500 Subject: [PATCH 51/98] [Beta][P2] Fix Sketch failing to sketch moves that call other moves virtually (#4823) * [P2][Beta] Fix Sketch failing to sketch Metronome et al * Suggested changes to `getLastXMoves()` * Renamed turnCount to moveCount --- src/data/move.ts | 11 +++- src/field/pokemon.ts | 16 +++++- src/modifier/modifier.ts | 4 +- src/test/abilities/sap_sipper.test.ts | 72 ++++++++++----------------- src/test/moves/sketch.test.ts | 21 +++++++- 5 files changed, 70 insertions(+), 54 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 071d7fa1e65..ed2b176f54c 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6373,10 +6373,17 @@ export class RandomMovesetMoveAttr extends OverrideMoveEffectAttr { } export class RandomMoveAttr extends OverrideMoveEffectAttr { + /** + * This function exists solely to allow tests to override the randomly selected move by mocking this function. + */ + public getMoveOverride(): Moves | null { + return null; + } + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { return new Promise(resolve => { const moveIds = Utils.getEnumValues(Moves).filter(m => !allMoves[m].hasFlag(MoveFlags.IGNORE_VIRTUAL) && !allMoves[m].name.endsWith(" (N)")); - const moveId = moveIds[user.randSeedInt(moveIds.length)]; + const moveId = this.getMoveOverride() ?? moveIds[user.randSeedInt(moveIds.length)]; const moveTargets = getMoveTargets(user, moveId); if (!moveTargets.targets.length) { @@ -6759,7 +6766,7 @@ export class SketchAttr extends MoveEffectAttr { return false; } - const targetMove = target.getLastXMoves(target.battleSummonData.turnCount) + const targetMove = target.getLastXMoves(-1) .find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual); if (!targetMove) { return false; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5d77aea248d..221cc8f818a 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3226,9 +3226,21 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.getMoveHistory().push(turnMove); } - getLastXMoves(turnCount: integer = 0): TurnMove[] { + /** + * Returns a list of the most recent move entries in this Pokemon's move history. + * The retrieved move entries are sorted in order from NEWEST to OLDEST. + * @param moveCount The number of move entries to retrieve. + * If negative, retrieve the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}). + * Default is `1`. + * @returns A list of {@linkcode TurnMove}, as specified above. + */ + getLastXMoves(moveCount: number = 1): TurnMove[] { const moveHistory = this.getMoveHistory(); - return moveHistory.slice(turnCount >= 0 ? Math.max(moveHistory.length - (turnCount || 1), 0) : 0, moveHistory.length).reverse(); + if (moveCount >= 0) { + return moveHistory.slice(Math.max(moveHistory.length - moveCount, 0)).reverse(); + } else { + return moveHistory.slice(0).reverse(); + } } getMoveQueue(): QueuedMove[] { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 9e97c866718..90336780ba6 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -728,10 +728,10 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { //Applies to items with chance of activating secondary effects ie Kings Rock getSecondaryChanceMultiplier(pokemon: Pokemon): number { // Temporary quickfix to stop game from freezing when the opponet uses u-turn while holding on to king's rock - if (!pokemon.getLastXMoves(0)[0]) { + if (!pokemon.getLastXMoves()[0]) { return 1; } - const sheerForceAffected = allMoves[pokemon.getLastXMoves(0)[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE); + const sheerForceAffected = allMoves[pokemon.getLastXMoves()[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE); if (sheerForceAffected) { return 0; diff --git a/src/test/abilities/sap_sipper.test.ts b/src/test/abilities/sap_sipper.test.ts index a4ce0c1b8f6..dc254a54b54 100644 --- a/src/test/abilities/sap_sipper.test.ts +++ b/src/test/abilities/sap_sipper.test.ts @@ -8,7 +8,8 @@ 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 { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { allMoves, RandomMoveAttr } from "#app/data/move"; // See also: TypeImmunityAbAttr describe("Abilities - Sap Sipper", () => { @@ -27,20 +28,20 @@ describe("Abilities - Sap Sipper", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleType("single"); - game.override.disableCrits(); + game.override.battleType("single") + .disableCrits() + .ability(Abilities.SAP_SIPPER) + .enemySpecies(Species.RATTATA) + .enemyAbility(Abilities.SAP_SIPPER) + .enemyMoveset(Moves.SPLASH); }); it("raises ATK stat stage by 1 and block effects when activated against a grass attack", async() => { const moveToUse = Moves.LEAFAGE; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.DUSKULL); - game.override.enemyAbility(enemyAbility); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; const initialEnemyHp = enemyPokemon.hp; @@ -55,14 +56,10 @@ describe("Abilities - Sap Sipper", () => { it("raises ATK stat stage by 1 and block effects when activated against a grass status move", async() => { const moveToUse = Moves.SPORE; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(enemyAbility); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -76,14 +73,10 @@ describe("Abilities - Sap Sipper", () => { it("do not activate against status moves that target the field", async () => { const moveToUse = Moves.GRASSY_TERRAIN; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(enemyAbility); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); game.move.select(moveToUse); @@ -96,14 +89,10 @@ describe("Abilities - Sap Sipper", () => { it("activate once against multi-hit grass attacks", async () => { const moveToUse = Moves.BULLET_SEED; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(enemyAbility); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; const initialEnemyHp = enemyPokemon.hp; @@ -118,15 +107,10 @@ describe("Abilities - Sap Sipper", () => { it("do not activate against status moves that target the user", async () => { const moveToUse = Moves.SPIKY_SHIELD; - const ability = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.ability(ability); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(Abilities.NONE); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const playerPokemon = game.scene.getPlayerPokemon()!; @@ -142,18 +126,15 @@ describe("Abilities - Sap Sipper", () => { expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }); - // TODO Add METRONOME outcome override - // To run this testcase, manually modify the METRONOME move to always give SAP_SIPPER, then uncomment - it.todo("activate once against multi-hit grass attacks (metronome)", async () => { + it("activate once against multi-hit grass attacks (metronome)", async () => { const moveToUse = Moves.METRONOME; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset([ Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE ]); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(enemyAbility); + const randomMoveAttr = allMoves[Moves.METRONOME].findAttr(attr => attr instanceof RandomMoveAttr) as RandomMoveAttr; + vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.BULLET_SEED); - await game.startBattle(); + game.override.moveset(moveToUse); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; const initialEnemyHp = enemyPokemon.hp; @@ -168,11 +149,8 @@ describe("Abilities - Sap Sipper", () => { it("still activates regardless of accuracy check", async () => { game.override.moveset(Moves.LEAF_BLADE); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.MAGIKARP); - game.override.enemyAbility(Abilities.SAP_SIPPER); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; diff --git a/src/test/moves/sketch.test.ts b/src/test/moves/sketch.test.ts index 4386ce5868e..f531f44ef0c 100644 --- a/src/test/moves/sketch.test.ts +++ b/src/test/moves/sketch.test.ts @@ -4,9 +4,10 @@ import { Species } from "#enums/species"; import { MoveResult, PokemonMove } from "#app/field/pokemon"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { StatusEffect } from "#app/enums/status-effect"; import { BattlerIndex } from "#app/battle"; +import { allMoves, RandomMoveAttr } from "#app/data/move"; describe("Moves - Sketch", () => { let phaserGame: Phaser.Game; @@ -76,4 +77,22 @@ describe("Moves - Sketch", () => { expect(playerPokemon.moveset[0]?.moveId).toBe(Moves.SPLASH); expect(playerPokemon.moveset[1]?.moveId).toBe(Moves.GROWL); }); + + it("should sketch moves that call other moves", async () => { + const randomMoveAttr = allMoves[Moves.METRONOME].findAttr(attr => attr instanceof RandomMoveAttr) as RandomMoveAttr; + vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.FALSE_SWIPE); + + game.override.enemyMoveset([ Moves.METRONOME ]); + await game.classicMode.startBattle([ Species.REGIELEKI ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.moveset = [ new PokemonMove(Moves.SKETCH) ]; + + // Opponent uses Metronome -> False Swipe, then player uses Sketch, which should sketch Metronome + game.move.select(Moves.SKETCH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(playerPokemon.moveset[0]?.moveId).toBe(Moves.METRONOME); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); // Make sure opponent actually used False Swipe + }); }); From 198ac2431d11536f20b68d56b06056eecd8c73fa Mon Sep 17 00:00:00 2001 From: damocleas Date: Sat, 9 Nov 2024 17:26:55 -0500 Subject: [PATCH 52/98] Undo Event modifier-type.ts item table changes (#4836) --- src/modifier/modifier-type.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index ae1aef3ff88..4986c1feab1 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1702,10 +1702,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.EVOLUTION_ITEM, (party: Pokemon[]) => { return Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15), 8); }, 8), - new WeightedModifierType(modifierTypes.MAP, - (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? party[0].scene.eventManager.isEventActive() ? 2 : 1 : 0, - (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 2 : 1), - new WeightedModifierType(modifierTypes.SOOTHE_BELL, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 3 : 0), + new WeightedModifierType(modifierTypes.MAP, (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? 1 : 0, 1), new WeightedModifierType(modifierTypes.TM_GREAT, 3), new WeightedModifierType(modifierTypes.MEMORY_MUSHROOM, (party: Pokemon[]) => { if (!party.find(p => p.getLearnableLevelMoves().length)) { @@ -1773,7 +1770,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.CANDY_JAR, skipInLastClassicWaveOrDefault(5)), new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 9), new WeightedModifierType(modifierTypes.TM_ULTRA, 11), - new WeightedModifierType(modifierTypes.RARER_CANDY, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 6 : 4), + new WeightedModifierType(modifierTypes.RARER_CANDY, 4), new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, skipInLastClassicWaveOrDefault(2)), new WeightedModifierType(modifierTypes.IV_SCANNER, skipInLastClassicWaveOrDefault(4)), new WeightedModifierType(modifierTypes.EXP_CHARM, skipInLastClassicWaveOrDefault(8)), @@ -1797,7 +1794,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.SOUL_DEW, 7), //new WeightedModifierType(modifierTypes.OVAL_CHARM, 6), new WeightedModifierType(modifierTypes.CATCHING_CHARM, (party: Pokemon[]) => !party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.getSpeciesCount(d => !!d.caughtAttr) > 100 ? 4 : 0, 4), - new WeightedModifierType(modifierTypes.SOOTHE_BELL, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 0 : 4), + new WeightedModifierType(modifierTypes.SOOTHE_BELL, 4), new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), From 265b3cb938a7f8a5685f5279bb6127d37919adf8 Mon Sep 17 00:00:00 2001 From: Payton Rogers Date: Sat, 9 Nov 2024 23:35:16 -0600 Subject: [PATCH 53/98] [P3] Fix visual bug with level text remaining the same when pokemon levels are reduced in weird dream ME (#4837) --- src/data/mystery-encounters/encounters/weird-dream-encounter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 3c541e20bf4..3d2e8493d44 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -312,6 +312,7 @@ export const WeirdDreamEncounter: MysteryEncounter = pokemon.levelExp = 0; pokemon.calculateStats(); + pokemon.getBattleInfo().setLevel(pokemon.level); await pokemon.updateInfo(); } From 44a68a91bac22b5982f98012846e66174ee16d96 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Sun, 10 Nov 2024 06:35:49 +0100 Subject: [PATCH 54/98] [P1] Fix crash when newly aquired Pokemon are sent in battle (#4835) --- .../encounters/fun-and-games-encounter.ts | 2 +- src/field/pokemon.ts | 8 ++++---- src/phases/encounter-phase.ts | 2 +- src/phases/summon-phase.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 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 7bf48aa5926..c286fffe0de 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -305,7 +305,7 @@ async function showWobbuffetHealthBar(scene: BattleScene) { scene.field.add(wobbuffet); const playerPokemon = scene.getPlayerPokemon() as Pokemon; - if (playerPokemon?.visible) { + if (playerPokemon?.isOnField()) { scene.field.moveBelow(wobbuffet, playerPokemon); } // Show health bar and trigger cry diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 221cc8f818a..daa73164577 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5113,7 +5113,7 @@ export class EnemyPokemon extends Pokemon { /** * Add a new pokemon to the player's party (at `slotIndex` if set). - * If the first slot is replaced, the new pokemon's visibility will be set to `false`. + * The new pokemon's visibility will be set to `false`. * @param pokeballType the type of pokeball the pokemon was caught with * @param slotIndex an optional index to place the pokemon in the party * @returns the pokemon that was added or null if the pokemon could not be added @@ -5131,14 +5131,14 @@ export class EnemyPokemon extends Pokemon { const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this); if (Utils.isBetween(slotIndex, 0, PLAYER_PARTY_MAX_SIZE - 1)) { - if (slotIndex === 0) { - newPokemon.setVisible(false); // Hide if replaced with first pokemon - } party.splice(slotIndex, 0, newPokemon); } else { party.push(newPokemon); } + // Hide the Pokemon since it is not on the field + newPokemon.setVisible(false); + ret = newPokemon; this.scene.triggerPokemonFormChange(newPokemon, SpeciesFormChangeActiveTrigger, true); } diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index c4d919c0325..fc022ab9647 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -202,7 +202,7 @@ export class EncounterPhase extends BattlePhase { this.scene.field.add(enemyPokemon); battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); const playerPokemon = this.scene.getPlayerPokemon(); - if (playerPokemon?.visible) { + if (playerPokemon?.isOnField()) { this.scene.field.moveBelow(enemyPokemon as Pokemon, playerPokemon); } enemyPokemon.tint(0, 0.5); diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index 119e550293c..177e09c4527 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -140,7 +140,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { this.scene.field.add(pokemon); if (!this.player) { const playerPokemon = this.scene.getPlayerPokemon() as Pokemon; - if (playerPokemon?.visible) { + if (playerPokemon?.isOnField()) { this.scene.field.moveBelow(pokemon, playerPokemon); } this.scene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); @@ -193,7 +193,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { this.scene.field.add(pokemon); if (!this.player) { const playerPokemon = this.scene.getPlayerPokemon() as Pokemon; - if (playerPokemon?.visible) { + if (playerPokemon?.isOnField()) { this.scene.field.moveBelow(pokemon, playerPokemon); } this.scene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); From b3a94e6a6bebc8c1b948b4196a410b3f4c23f60e Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:37:09 -0800 Subject: [PATCH 55/98] [Telemetry][Misc] Client-Side changes to log run results at the end of runs (#4834) * Added new telemetry-related parameters * Update test with new parameters. * Removing extra parameters. * Cat in front of keyboar d sorry * Changed variable name to isVictory. * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Thank you Torranx * Condensed if-else pair to else if statement * Update src/phases/game-over-phase.ts Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> * inhale... exhale... corrected variable name to pass linter --------- Co-authored-by: frutescens Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> --- src/@types/PokerogueSessionSavedataApi.ts | 1 + src/phases/game-over-phase.ts | 50 +++++++++---------- .../pokerogue-session-savedata-api.test.ts | 3 +- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/@types/PokerogueSessionSavedataApi.ts b/src/@types/PokerogueSessionSavedataApi.ts index 5fcd8575b15..c4650611c4f 100644 --- a/src/@types/PokerogueSessionSavedataApi.ts +++ b/src/@types/PokerogueSessionSavedataApi.ts @@ -8,6 +8,7 @@ export class UpdateSessionSavedataRequest { /** This is **NOT** similar to {@linkcode ClearSessionSavedataRequest} */ export interface NewClearSessionSavedataRequest { slot: number; + isVictory: boolean; clientSessionId: string; } diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 84fad257897..26a0c45f449 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -26,13 +26,13 @@ import i18next from "i18next"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; export class GameOverPhase extends BattlePhase { - private victory: boolean; + private isVictory: boolean; private firstRibbons: PokemonSpecies[] = []; - constructor(scene: BattleScene, victory?: boolean) { + constructor(scene: BattleScene, isVictory: boolean = false) { super(scene); - this.victory = !!victory; + this.isVictory = isVictory; } start() { @@ -40,22 +40,22 @@ export class GameOverPhase extends BattlePhase { // Failsafe if players somehow skip floor 200 in classic mode if (this.scene.gameMode.isClassic && this.scene.currentBattle.waveIndex > 200) { - this.victory = true; + this.isVictory = true; } // Handle Mystery Encounter special Game Over cases // Situations such as when player lost a battle, but it isn't treated as full Game Over - if (!this.victory && this.scene.currentBattle.mysteryEncounter?.onGameOver && !this.scene.currentBattle.mysteryEncounter.onGameOver(this.scene)) { + if (!this.isVictory && this.scene.currentBattle.mysteryEncounter?.onGameOver && !this.scene.currentBattle.mysteryEncounter.onGameOver(this.scene)) { // Do not end the game return this.end(); } // Otherwise, continue standard Game Over logic - if (this.victory && this.scene.gameMode.isEndless) { + if (this.isVictory && this.scene.gameMode.isEndless) { const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET; const genderStr = PlayerGender[genderIndex].toLowerCase(); this.scene.ui.showDialogue(i18next.t("miscDialogue:ending_endless", { context: genderStr }), i18next.t("miscDialogue:ending_name"), 0, () => this.handleGameOver()); - } else if (this.victory || !this.scene.enableRetries) { + } else if (this.isVictory || !this.scene.enableRetries) { this.handleGameOver(); } else { this.scene.ui.showText(i18next.t("battle:retryBattle"), null, () => { @@ -93,7 +93,7 @@ export class GameOverPhase extends BattlePhase { this.scene.disableMenu = true; this.scene.time.delayedCall(1000, () => { let firstClear = false; - if (this.victory && newClear) { + if (this.isVictory && newClear) { if (this.scene.gameMode.isClassic) { firstClear = this.scene.validateAchv(achvs.CLASSIC_VICTORY); this.scene.validateAchv(achvs.UNEVOLVED_CLASSIC_VICTORY); @@ -109,8 +109,8 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.gameStats.dailyRunSessionsWon++; } } - this.scene.gameData.saveRunHistory(this.scene, this.scene.gameData.getSessionSaveData(this.scene), this.victory); - const fadeDuration = this.victory ? 10000 : 5000; + this.scene.gameData.saveRunHistory(this.scene, this.scene.gameData.getSessionSaveData(this.scene), this.isVictory); + const fadeDuration = this.isVictory ? 10000 : 5000; this.scene.fadeOutBgm(fadeDuration, true); const activeBattlers = this.scene.getField().filter(p => p?.isActive(true)); activeBattlers.map(p => p.hideInfo()); @@ -120,7 +120,7 @@ export class GameOverPhase extends BattlePhase { this.scene.clearPhaseQueue(); this.scene.ui.clearText(); - if (this.victory && this.scene.gameMode.isChallenge) { + if (this.isVictory && this.scene.gameMode.isChallenge) { this.scene.gameMode.challenges.forEach(c => this.scene.validateAchvs(ChallengeAchv, c)); } @@ -128,7 +128,7 @@ export class GameOverPhase extends BattlePhase { if (newClear) { this.handleUnlocks(); } - if (this.victory && newClear) { + if (this.isVictory && newClear) { for (const species of this.firstRibbons) { this.scene.unshiftPhase(new RibbonModifierRewardPhase(this.scene, modifierTypes.VOUCHER_PLUS, species)); } @@ -140,7 +140,7 @@ export class GameOverPhase extends BattlePhase { this.end(); }; - if (this.victory && this.scene.gameMode.isClassic) { + if (this.isVictory && this.scene.gameMode.isClassic) { const dialogueKey = "miscDialogue:ending"; if (!this.scene.ui.shouldSkipDialogue(dialogueKey)) { @@ -173,25 +173,21 @@ export class GameOverPhase extends BattlePhase { }); }; - /* Added a local check to see if the game is running offline on victory + /* Added a local check to see if the game is running offline If Online, execute apiFetch as intended - If Offline, execute offlineNewClear(), a localStorage implementation of newClear daily run checks */ - if (this.victory) { - if (!Utils.isLocal || Utils.isLocalServerConnected) { - pokerogueApi.savedata.session.newclear({ slot: this.scene.sessionSlotId, clientSessionId }) - .then((success) => doGameOver(!!success)); - } else { - this.scene.gameData.offlineNewClear(this.scene).then(result => { - doGameOver(result); - }); - } - } else { - doGameOver(false); + If Offline, execute offlineNewClear() only for victory, a localStorage implementation of newClear daily run checks */ + if (!Utils.isLocal || Utils.isLocalServerConnected) { + pokerogueApi.savedata.session.newclear({ slot: this.scene.sessionSlotId, isVictory: this.isVictory, clientSessionId: clientSessionId }) + .then((success) => doGameOver(!!success)); + } else if (this.isVictory) { + this.scene.gameData.offlineNewClear(this.scene).then(result => { + doGameOver(result); + }); } } handleUnlocks(): void { - if (this.victory && this.scene.gameMode.isClassic) { + if (this.isVictory && this.scene.gameMode.isClassic) { if (!this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.ENDLESS_MODE)); } diff --git a/src/test/plugins/api/pokerogue-session-savedata-api.test.ts b/src/test/plugins/api/pokerogue-session-savedata-api.test.ts index d9f6216c4cf..f453c5edd88 100644 --- a/src/test/plugins/api/pokerogue-session-savedata-api.test.ts +++ b/src/test/plugins/api/pokerogue-session-savedata-api.test.ts @@ -28,7 +28,8 @@ describe("Pokerogue Session Savedata API", () => { describe("Newclear", () => { const params: NewClearSessionSavedataRequest = { clientSessionId: "test-session-id", - slot: 3, + isVictory: true, + slot: 3 }; it("should return true on SUCCESS", async () => { From 2968059814dd714a1a855006adc6016b1f081a1b Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:39:05 -0800 Subject: [PATCH 56/98] [P1] Transform and Imposter will now fail when either Pokemon is fused (#4824) * Transform and Imposter will now fail when either Pokemon is fused * Prevent Ditto from being randomly generated as part of a fusion --- src/data/ability.ts | 27 ++++++++++++++++++++++----- src/data/move.ts | 8 +++++--- src/data/pokemon-species.ts | 19 +++++++++++++------ src/field/pokemon.ts | 18 ++++++++++-------- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 08dc1ed27a4..736f5862530 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2463,12 +2463,15 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr { } } +/** + * Used by Imposter + */ export class PostSummonTransformAbAttr extends PostSummonAbAttr { constructor() { super(true); } - async applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): Promise { + async applyPostSummon(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): Promise { const targets = pokemon.getOpponents(); if (simulated || !targets.length) { return simulated; @@ -2477,17 +2480,31 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { let target: Pokemon; if (targets.length > 1) { - pokemon.scene.executeWithSeedOffset(() => target = Utils.randSeedItem(targets), pokemon.scene.currentBattle.waveIndex); + pokemon.scene.executeWithSeedOffset(() => { + // in a double battle, if one of the opposing pokemon is fused the other one will be chosen + // if both are fused, then Imposter will fail below + if (targets[0].fusionSpecies) { + target = targets[1]; + return; + } else if (targets[1].fusionSpecies) { + target = targets[0]; + return; + } + target = Utils.randSeedItem(targets); + }, pokemon.scene.currentBattle.waveIndex); } else { target = targets[0]; } - target = target!; + + // transforming from or into fusion pokemon causes various problems (including crashes and save corruption) + if (target.fusionSpecies || pokemon.fusionSpecies) { + return false; + } + pokemon.summonData.speciesForm = target.getSpeciesForm(); - pokemon.summonData.fusionSpeciesForm = target.getFusionSpeciesForm(); pokemon.summonData.ability = target.getAbility().id; pokemon.summonData.gender = target.getGender(); - pokemon.summonData.fusionGender = target.getFusionGender(); // Copy all stats (except HP) for (const s of EFFECTIVE_STATS) { diff --git a/src/data/move.ts b/src/data/move.ts index ed2b176f54c..37afe651861 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6995,6 +6995,9 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr { } } +/** + * Used by Transform + */ export class TransformAttr extends MoveEffectAttr { async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { if (!super.apply(user, target, move, args)) { @@ -7003,10 +7006,8 @@ export class TransformAttr extends MoveEffectAttr { const promises: Promise[] = []; user.summonData.speciesForm = target.getSpeciesForm(); - user.summonData.fusionSpeciesForm = target.getFusionSpeciesForm(); user.summonData.ability = target.getAbility().id; user.summonData.gender = target.getGender(); - user.summonData.fusionGender = target.getFusionGender(); // Power Trick's effect will not preserved after using Transform user.removeTag(BattlerTagType.POWER_TRICK); @@ -8077,7 +8078,8 @@ export function initMoves() { .ignoresVirtual(), new StatusMove(Moves.TRANSFORM, Type.NORMAL, -1, 10, -1, 0, 1) .attr(TransformAttr) - .condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE)) + // transforming from or into fusion pokemon causes various problems (such as crashes) + .condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE) && !user.fusionSpecies && !target.fusionSpecies) .ignoresProtect(), new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1) diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index e7fe902956c..ff53fdb9392 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -888,17 +888,24 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali getCompatibleFusionSpeciesFilter(): PokemonSpeciesFilter { const hasEvolution = pokemonEvolutions.hasOwnProperty(this.speciesId); const hasPrevolution = pokemonPrevolutions.hasOwnProperty(this.speciesId); - const pseudoLegendary = this.subLegendary; + const subLegendary = this.subLegendary; const legendary = this.legendary; const mythical = this.mythical; return species => { - return (pseudoLegendary || legendary || mythical || - (pokemonEvolutions.hasOwnProperty(species.speciesId) === hasEvolution - && pokemonPrevolutions.hasOwnProperty(species.speciesId) === hasPrevolution)) - && species.subLegendary === pseudoLegendary + return ( + subLegendary + || legendary + || mythical + || ( + pokemonEvolutions.hasOwnProperty(species.speciesId) === hasEvolution + && pokemonPrevolutions.hasOwnProperty(species.speciesId) === hasPrevolution + ) + ) + && species.subLegendary === subLegendary && species.legendary === legendary && species.mythical === mythical - && (this.isTrainerForbidden() || !species.isTrainerForbidden()); + && (this.isTrainerForbidden() || !species.isTrainerForbidden()) + && species.speciesId !== Species.DITTO; }; } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index daa73164577..d413e618381 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2030,15 +2030,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const hasHiddenAbility = !Utils.randSeedInt(hiddenAbilityChance.value); const randAbilityIndex = Utils.randSeedInt(2); - const filter = !forStarter ? this.species.getCompatibleFusionSpeciesFilter() - : species => { + const filter = !forStarter ? + this.species.getCompatibleFusionSpeciesFilter() + : (species: PokemonSpecies) => { return pokemonEvolutions.hasOwnProperty(species.speciesId) - && !pokemonPrevolutions.hasOwnProperty(species.speciesId) - && !species.pseudoLegendary - && !species.legendary - && !species.mythical - && !species.isTrainerForbidden() - && species.speciesId !== this.species.speciesId; + && !pokemonPrevolutions.hasOwnProperty(species.speciesId) + && !species.subLegendary + && !species.legendary + && !species.mythical + && !species.isTrainerForbidden() + && species.speciesId !== this.species.speciesId + && species.speciesId !== Species.DITTO; }; let fusionOverride: PokemonSpecies | undefined = undefined; From 63ffab027dcddcca9122e7270242e88845703e25 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Sun, 10 Nov 2024 14:21:29 -0500 Subject: [PATCH 57/98] [Beta][P2] Several Unburden bug fixes (#4820) * [P2][Beta] Several Unburden bug fixes * Unburden test adjustments * Some further test cleanup * Add suggested `.bypassFaint()` to Unburden --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/battle-scene.ts | 29 +- src/data/ability.ts | 6 +- src/data/berry.ts | 26 +- src/data/move.ts | 16 +- .../encounters/bug-type-superfan-encounter.ts | 7 +- .../encounters/delibirdy-encounter.ts | 18 +- .../global-trade-system-encounter.ts | 8 +- src/field/pokemon.ts | 28 +- src/phases/berry-phase.ts | 7 +- src/phases/faint-phase.ts | 10 +- src/phases/select-modifier-phase.ts | 2 +- src/phases/stat-stage-change-phase.ts | 5 +- src/phases/switch-summon-phase.ts | 2 +- src/test/abilities/unburden.test.ts | 360 +++++++++++++----- 14 files changed, 346 insertions(+), 178 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c5acadc8eb6..c30ab2e2912 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2572,14 +2572,15 @@ export default class BattleScene extends SceneBase { * The quantity to transfer is automatically capped at how much the recepient can take before reaching the maximum stack size for the item. * A transfer that moves a quantity smaller than what is specified in the transferQuantity parameter is still considered successful. * @param itemModifier {@linkcode PokemonHeldItemModifier} item to transfer (represents the whole stack) - * @param target {@linkcode Pokemon} pokemon recepient in this transfer - * @param playSound {boolean} - * @param transferQuantity {@linkcode integer} how many items of the stack to transfer. Optional, defaults to 1 - * @param instant {boolean} - * @param ignoreUpdate {boolean} - * @returns true if the transfer was successful + * @param target {@linkcode Pokemon} recepient in this transfer + * @param playSound `true` to play a sound when transferring the item + * @param transferQuantity How many items of the stack to transfer. Optional, defaults to `1` + * @param instant ??? (Optional) + * @param ignoreUpdate ??? (Optional) + * @param itemLost If `true`, treat the item's current holder as losing the item (for now, this simply enables Unburden). Default is `true`. + * @returns `true` if the transfer was successful */ - tryTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, playSound: boolean, transferQuantity: integer = 1, instant?: boolean, ignoreUpdate?: boolean): Promise { + tryTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, playSound: boolean, transferQuantity: number = 1, instant?: boolean, ignoreUpdate?: boolean, itemLost: boolean = true): Promise { return new Promise(resolve => { const source = itemModifier.pokemonId ? itemModifier.getPokemon(target.scene) : null; const cancelled = new Utils.BooleanHolder(false); @@ -2612,14 +2613,14 @@ export default class BattleScene extends SceneBase { if (!matchingModifier || this.removeModifier(matchingModifier, !target.isPlayer())) { if (target.isPlayer()) { this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant).then(() => { - if (source) { + if (source && itemLost) { applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false); } resolve(true); }); } else { this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => { - if (source) { + if (source && itemLost) { applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false); } resolve(true); @@ -2791,7 +2792,15 @@ export default class BattleScene extends SceneBase { }); } - removeModifier(modifier: PersistentModifier, enemy?: boolean): boolean { + /** + * Removes a currently owned item. If the item is stacked, the entire item stack + * gets removed. This function does NOT apply in-battle effects, such as Unburden. + * If in-battle effects are needed, use {@linkcode Pokemon.loseHeldItem} instead. + * @param modifier The item to be removed. + * @param enemy If `true`, remove an item owned by the enemy. If `false`, remove an item owned by the player. Default is `false`. + * @returns `true` if the item exists and was successfully removed, `false` otherwise. + */ + removeModifier(modifier: PersistentModifier, enemy: boolean = false): boolean { const modifiers = !enemy ? this.modifiers : this.enemyModifiers; const modifierIndex = modifiers.indexOf(modifier); if (modifierIndex > -1) { diff --git a/src/data/ability.ts b/src/data/ability.ts index 736f5862530..49763991e0e 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4152,7 +4152,7 @@ export class PostBattleLootAbAttr extends PostBattleAbAttr { if (!simulated && postBattleLoot.length) { const randItem = Utils.randSeedItem(postBattleLoot); //@ts-ignore - TODO see below - if (pokemon.scene.tryTransferHeldItemModifier(randItem, pokemon, true, 1, true)) { // TODO: fix. This is a promise!? + if (pokemon.scene.tryTransferHeldItemModifier(randItem, pokemon, true, 1, true, undefined, false)) { // TODO: fix. This is a promise!? postBattleLoot.splice(postBattleLoot.indexOf(randItem), 1); pokemon.scene.queueMessage(i18next.t("abilityTriggers:postBattleLoot", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), itemName: randItem.type.name })); return true; @@ -5616,7 +5616,9 @@ export function initAbilities() { new Ability(Abilities.ANGER_POINT, 4) .attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6), new Ability(Abilities.UNBURDEN, 4) - .attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN), + .attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN) + .bypassFaint() // Allows reviver seed to activate Unburden + .edgeCase(), // Should not restore Unburden boost if Pokemon loses then regains Unburden ability new Ability(Abilities.HEATPROOF, 4) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) .attr(ReduceBurnDamageAbAttr, 0.5) diff --git a/src/data/berry.ts b/src/data/berry.ts index d2bbd0fdd1c..dfd6a7ddcf0 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -61,13 +61,13 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate { } } -export type BerryEffectFunc = (pokemon: Pokemon) => void; +export type BerryEffectFunc = (pokemon: Pokemon, berryOwner?: Pokemon) => void; export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { switch (berryType) { case BerryType.SITRUS: case BerryType.ENIGMA: - return (pokemon: Pokemon) => { + return (pokemon: Pokemon, berryOwner?: Pokemon) => { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } @@ -75,10 +75,10 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed); pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), hpHealed.value, i18next.t("battle:hpHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: getBerryName(berryType) }), true)); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); }; case BerryType.LUM: - return (pokemon: Pokemon) => { + return (pokemon: Pokemon, berryOwner?: Pokemon) => { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } @@ -87,14 +87,14 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { } pokemon.resetStatus(true, true); pokemon.updateInfo(); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); }; case BerryType.LIECHI: case BerryType.GANLON: case BerryType.PETAYA: case BerryType.APICOT: case BerryType.SALAC: - return (pokemon: Pokemon) => { + return (pokemon: Pokemon, berryOwner?: Pokemon) => { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } @@ -103,18 +103,18 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { const statStages = new Utils.NumberHolder(1); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value)); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); }; case BerryType.LANSAT: - return (pokemon: Pokemon) => { + return (pokemon: Pokemon, berryOwner?: Pokemon) => { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } pokemon.addTag(BattlerTagType.CRIT_BOOST); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); }; case BerryType.STARF: - return (pokemon: Pokemon) => { + return (pokemon: Pokemon, berryOwner?: Pokemon) => { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } @@ -122,10 +122,10 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { const stages = new Utils.NumberHolder(2); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], stages.value)); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); }; case BerryType.LEPPA: - return (pokemon: Pokemon) => { + return (pokemon: Pokemon, berryOwner?: Pokemon) => { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } @@ -133,7 +133,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { if (ppRestoreMove !== undefined) { ppRestoreMove!.ppUsed = Math.max(ppRestoreMove!.ppUsed - 10, 0); pokemon.scene.queueMessage(i18next.t("battle:ppHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: ppRestoreMove!.getName(), berryName: getBerryName(berryType) })); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); } }; } diff --git a/src/data/move.ts b/src/data/move.ts index 37afe651861..9cd4881488c 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2417,9 +2417,8 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { const removedItem = heldItems[user.randSeedInt(heldItems.length)]; // Decrease item amount and update icon - !--removedItem.stackCount; + target.loseHeldItem(removedItem); target.scene.updateModifiers(target.isPlayer()); - applyPostItemLostAbAttrs(PostItemLostAbAttr, target, false); if (this.berriesOnly) { @@ -2489,18 +2488,15 @@ export class EatBerryAttr extends MoveEffectAttr { } reduceBerryModifier(target: Pokemon) { - if (this.chosenBerry?.stackCount === 1) { - target.scene.removeModifier(this.chosenBerry, !target.isPlayer()); - } else if (this.chosenBerry !== undefined && this.chosenBerry.stackCount > 1) { - this.chosenBerry.stackCount--; + if (this.chosenBerry) { + target.loseHeldItem(this.chosenBerry); } target.scene.updateModifiers(target.isPlayer()); } - eatBerry(consumer: Pokemon) { - getBerryEffectFunc(this.chosenBerry!.berryType)(consumer); // consumer eats the berry + eatBerry(consumer: Pokemon, berryOwner?: Pokemon) { + getBerryEffectFunc(this.chosenBerry!.berryType)(consumer, berryOwner); // consumer eats the berry applyAbAttrs(HealFromBerryUseAbAttr, consumer, new Utils.BooleanHolder(false)); - applyPostItemLostAbAttrs(PostItemLostAbAttr, consumer, false); } } @@ -2540,7 +2536,7 @@ export class StealEatBerryAttr extends EatBerryAttr { const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); user.scene.queueMessage(message); this.reduceBerryModifier(target); - this.eatBerry(user); + this.eatBerry(user, target); return true; } } diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index 7a03e6efdd2..ecd6972902b 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -477,12 +477,9 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter!; const modifier = encounter.misc.chosenModifier; + const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; - // Remove the modifier if its stacks go to 0 - modifier.stackCount -= 1; - if (modifier.stackCount === 0) { - scene.removeModifier(modifier); - } + chosenPokemon.loseHeldItem(modifier, false); scene.updateModifiers(true, true); const bugNet = generateModifierTypeOption(scene, modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!; diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index d5a938b9cef..a3a97a01238 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -8,7 +8,7 @@ import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/u import { getPokemonSpecies } from "#app/data/pokemon-species"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; -import { BerryModifier, HealingBoosterModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, PokemonHeldItemModifier, PreserveBerryModifier } from "#app/modifier/modifier"; +import { BerryModifier, HealingBoosterModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, PreserveBerryModifier } from "#app/modifier/modifier"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import i18next from "#app/plugins/i18n"; @@ -197,7 +197,8 @@ export const DelibirdyEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter!; - const modifier: BerryModifier | HealingBoosterModifier = encounter.misc.chosenModifier; + const modifier: BerryModifier | PokemonInstantReviveModifier = encounter.misc.chosenModifier; + const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; // Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed if (modifier instanceof BerryModifier) { @@ -228,11 +229,7 @@ export const DelibirdyEncounter: MysteryEncounter = } } - // Remove the modifier if its stacks go to 0 - modifier.stackCount -= 1; - if (modifier.stackCount === 0) { - scene.removeModifier(modifier); - } + chosenPokemon.loseHeldItem(modifier, false); leaveEncounterWithoutBattle(scene, true); }) @@ -292,6 +289,7 @@ export const DelibirdyEncounter: MysteryEncounter = .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter!; const modifier = encounter.misc.chosenModifier; + const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; // Check if the player has max stacks of Healing Charm already const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; @@ -306,11 +304,7 @@ export const DelibirdyEncounter: MysteryEncounter = scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); } - // Remove the modifier if its stacks go to 0 - modifier.stackCount -= 1; - if (modifier.stackCount === 0) { - scene.removeModifier(modifier); - } + chosenPokemon.loseHeldItem(modifier, false); leaveEncounterWithoutBattle(scene, true); }) diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index b0d547e36cf..2d569621449 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -345,6 +345,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = // Pokemon and item selected encounter.setDialogueToken("chosenItem", modifier.type.name); encounter.misc.chosenModifier = modifier; + encounter.misc.chosenPokemon = pokemon; return true; }, }; @@ -370,6 +371,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const modifier = encounter.misc.chosenModifier as PokemonHeldItemModifier; const party = scene.getPlayerParty(); + const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; // Check tier of the traded item, the received item will be one tier up const type = modifier.type.withTierFromPool(ModifierPoolType.PLAYER, party); @@ -397,11 +399,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = encounter.setDialogueToken("itemName", item.type.name); setEncounterRewards(scene, { guaranteedModifierTypeOptions: [ item ], fillRemaining: false }); - // Remove the chosen modifier if its stacks go to 0 - modifier.stackCount -= 1; - if (modifier.stackCount === 0) { - scene.removeModifier(modifier); - } + chosenPokemon.loseHeldItem(modifier, false); await scene.updateModifiers(true, true); // Generate a trainer name diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d413e618381..d806a9b605c 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -23,7 +23,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags"; import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; -import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr, CommanderAbAttr } from "#app/data/ability"; +import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -985,7 +985,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.status && this.status.effect === StatusEffect.PARALYSIS) { ret >>= 1; } - if (this.getTag(BattlerTagType.UNBURDEN) && !this.scene.getField(true).some(pokemon => pokemon !== this && pokemon.hasAbilityWithAttr(SuppressFieldAbilitiesAbAttr))) { + if (this.getTag(BattlerTagType.UNBURDEN) && this.hasAbility(Abilities.UNBURDEN)) { ret *= 2; } break; @@ -4102,6 +4102,28 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } return false; } + + /** + * Reduces one of this Pokemon's held item stacks by 1, and removes the item if applicable. + * Does nothing if this Pokemon is somehow not the owner of the held item. + * @param heldItem The item stack to be reduced by 1. + * @param forBattle If `false`, do not trigger in-battle effects (such as Unburden) from losing the item. For example, set this to `false` if the Pokemon is giving away the held item for a Mystery Encounter. Default is `true`. + * @returns `true` if the item was removed successfully, `false` otherwise. + */ + public loseHeldItem(heldItem: PokemonHeldItemModifier, forBattle: boolean = true): boolean { + if (heldItem.pokemonId === -1 || heldItem.pokemonId === this.id) { + heldItem.stackCount--; + if (heldItem.stackCount <= 0) { + this.scene.removeModifier(heldItem, !this.isPlayer()); + } + if (forBattle) { + applyPostItemLostAbAttrs(PostItemLostAbAttr, this, false); + } + return true; + } else { + return false; + } + } } export default interface Pokemon { @@ -4544,7 +4566,7 @@ export class PlayerPokemon extends Pokemon { && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; const transferModifiers: Promise[] = []; for (const modifier of fusedPartyMemberHeldModifiers) { - transferModifiers.push(this.scene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true)); + transferModifiers.push(this.scene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false)); } Promise.allSettled(transferModifiers).then(() => { this.scene.updateModifiers(true, true).then(() => { diff --git a/src/phases/berry-phase.ts b/src/phases/berry-phase.ts index e419aa6692d..5c33ae4b343 100644 --- a/src/phases/berry-phase.ts +++ b/src/phases/berry-phase.ts @@ -31,11 +31,8 @@ export class BerryPhase extends FieldPhase { for (const berryModifier of this.scene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon)) { if (berryModifier.consumed) { - if (!--berryModifier.stackCount) { - this.scene.removeModifier(berryModifier); - } else { - berryModifier.consumed = false; - } + berryModifier.consumed = false; + pokemon.loseHeldItem(berryModifier); } this.scene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); // Announce a berry was used } diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index d66c5b66144..1c48bdfb37a 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -55,21 +55,21 @@ export class FaintPhase extends PokemonPhase { start() { super.start(); + const faintPokemon = this.getPokemon(); + if (!isNullOrUndefined(this.destinyTag) && !isNullOrUndefined(this.source)) { this.destinyTag.lapse(this.source, BattlerTagLapseType.CUSTOM); } if (!isNullOrUndefined(this.grudgeTag) && !isNullOrUndefined(this.source)) { - this.grudgeTag.lapse(this.getPokemon(), BattlerTagLapseType.CUSTOM, this.source); + this.grudgeTag.lapse(faintPokemon, BattlerTagLapseType.CUSTOM, this.source); } if (!this.preventEndure) { - const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier; + const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, faintPokemon) as PokemonInstantReviveModifier; if (instantReviveModifier) { - if (!--instantReviveModifier.stackCount) { - this.scene.removeModifier(instantReviveModifier); - } + faintPokemon.loseHeldItem(instantReviveModifier); this.scene.updateModifiers(this.player); return this.end(); } diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 98975e30720..19e1ccc12ae 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -103,7 +103,7 @@ export class SelectModifierPhase extends BattlePhase { const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferable && m.pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; const itemModifier = itemModifiers[itemIndex]; - this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity); + this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity, undefined, undefined, false); } else { this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers)); } diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index ce6ebea2442..44144f9d047 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -125,10 +125,7 @@ export class StatStageChangePhase extends PokemonPhase { const whiteHerb = this.scene.applyModifier(ResetNegativeStatStageModifier, this.player, pokemon) as ResetNegativeStatStageModifier; // If the White Herb was applied, consume it if (whiteHerb) { - whiteHerb.stackCount--; - if (whiteHerb.stackCount <= 0) { - this.scene.removeModifier(whiteHerb); - } + pokemon.loseHeldItem(whiteHerb); this.scene.updateModifiers(this.player); } } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 51d54315165..a667e17edf1 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -111,7 +111,7 @@ export class SwitchSummonPhase extends SummonPhase { const batonPassModifier = this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === this.lastPokemon.id) as SwitchEffectTransferModifier; if (batonPassModifier && !this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id)) { - this.scene.tryTransferHeldItemModifier(batonPassModifier, switchedInPokemon, false); + this.scene.tryTransferHeldItemModifier(batonPassModifier, switchedInPokemon, false, undefined, undefined, undefined, false); } } } diff --git a/src/test/abilities/unburden.test.ts b/src/test/abilities/unburden.test.ts index 7cd69f4a075..ba14c7fdcd0 100644 --- a/src/test/abilities/unburden.test.ts +++ b/src/test/abilities/unburden.test.ts @@ -1,18 +1,35 @@ +import { BattlerIndex } from "#app/battle"; +import { PostItemLostAbAttr } from "#app/data/ability"; +import { allMoves, StealHeldItemChanceAttr } from "#app/data/move"; +import Pokemon from "#app/field/pokemon"; +import type { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier"; import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { BerryType } from "#enums/berry-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { Stat } from "#enums/stat"; -import { BerryType } from "#app/enums/berry-type"; -import { allMoves, StealHeldItemChanceAttr } from "#app/data/move"; describe("Abilities - Unburden", () => { let phaserGame: Phaser.Game; let game: GameManager; + /** + * Count the number of held items a Pokemon has, accounting for stacks of multiple items. + */ + function getHeldItemCount(pokemon: Pokemon): number { + const stackCounts = pokemon.getHeldItems().map(m => m.getStackCount()); + if (stackCounts.length) { + return stackCounts.reduce((a, b) => a + b); + } else { + return 0; + } + } + beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -27,9 +44,9 @@ describe("Abilities - Unburden", () => { game = new GameManager(phaserGame); game.override .battleType("single") - .starterSpecies(Species.TREECKO) .startingLevel(1) - .moveset([ Moves.POPULATION_BOMB, Moves.KNOCK_OFF, Moves.PLUCK, Moves.THIEF ]) + .ability(Abilities.UNBURDEN) + .moveset([ Moves.SPLASH, Moves.KNOCK_OFF, Moves.PLUCK, Moves.FALSE_SWIPE ]) .startingHeldItems([ { name: "BERRY", count: 1, type: BerryType.SITRUS }, { name: "BERRY", count: 2, type: BerryType.APICOT }, @@ -37,209 +54,348 @@ describe("Abilities - Unburden", () => { ]) .enemySpecies(Species.NINJASK) .enemyLevel(100) - .enemyMoveset([ Moves.FALSE_SWIPE ]) + .enemyMoveset(Moves.SPLASH) .enemyAbility(Abilities.UNBURDEN) .enemyPassiveAbility(Abilities.NO_GUARD) .enemyHeldItems([ { name: "BERRY", type: BerryType.SITRUS, count: 1 }, { name: "BERRY", type: BerryType.LUM, count: 1 }, ]); + // For the various tests that use Thief, give it a 100% steal rate + vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([ new StealHeldItemChanceAttr(1.0) ]); }); it("should activate when a berry is eaten", async () => { - await game.classicMode.startBattle(); + game.override.enemyMoveset(Moves.FALSE_SWIPE); + await game.classicMode.startBattle([ Species.TREECKO ]); const playerPokemon = game.scene.getPlayerPokemon()!; - playerPokemon.abilityIndex = 2; - const playerHeldItems = playerPokemon.getHeldItems().length; + const playerHeldItems = getHeldItemCount(playerPokemon); const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); - game.move.select(Moves.FALSE_SWIPE); + // Player gets hit by False Swipe and eats its own Sitrus Berry + game.move.select(Moves.SPLASH); await game.toNextTurn(); - expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); }); - it("should activate when a berry is stolen", async () => { - await game.classicMode.startBattle(); + it("should activate when a berry is eaten, even if Berry Pouch preserves the berry", async () => { + game.override.enemyMoveset(Moves.FALSE_SWIPE) + .startingModifier([{ name: "BERRY_POUCH", count: 5850 }]); + await game.classicMode.startBattle([ Species.TREECKO ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = getHeldItemCount(playerPokemon); + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + // Player gets hit by False Swipe and eats its own Sitrus Berry + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(getHeldItemCount(playerPokemon)).toBe(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); + }); + + it("should activate for the target, and not the stealer, when a berry is stolen", async () => { + await game.classicMode.startBattle([ Species.TREECKO ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); const enemyPokemon = game.scene.getEnemyPokemon()!; - const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const enemyHeldItemCt = getHeldItemCount(enemyPokemon); const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + // Player uses Pluck and eats the opponent's berry game.move.select(Moves.PLUCK); await game.toNextTurn(); - expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + expect(getHeldItemCount(enemyPokemon)).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBe(initialEnemySpeed * 2); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed); }); it("should activate when an item is knocked off", async () => { - await game.classicMode.startBattle(); + await game.classicMode.startBattle([ Species.TREECKO ]); const enemyPokemon = game.scene.getEnemyPokemon()!; - const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const enemyHeldItemCt = getHeldItemCount(enemyPokemon); const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + // Player uses Knock Off and removes the opponent's item game.move.select(Moves.KNOCK_OFF); await game.toNextTurn(); - expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + expect(getHeldItemCount(enemyPokemon)).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBe(initialEnemySpeed * 2); }); it("should activate when an item is stolen via attacking ability", async () => { game.override .ability(Abilities.MAGICIAN) - .startingHeldItems([ - { name: "MULTI_LENS", count: 3 }, - ]); - await game.classicMode.startBattle(); + .startingHeldItems([]); // Remove player's full stacks of held items so it can steal opponent's held items + await game.classicMode.startBattle([ Species.TREECKO ]); const enemyPokemon = game.scene.getEnemyPokemon()!; - const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const enemyHeldItemCt = getHeldItemCount(enemyPokemon); const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); - game.move.select(Moves.POPULATION_BOMB); + // Player steals the opponent's item via ability Magician + game.move.select(Moves.FALSE_SWIPE); await game.toNextTurn(); - expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + expect(getHeldItemCount(enemyPokemon)).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBe(initialEnemySpeed * 2); }); it("should activate when an item is stolen via defending ability", async () => { game.override - .startingLevel(45) .enemyAbility(Abilities.PICKPOCKET) - .startingHeldItems([ - { name: "MULTI_LENS", count: 3 }, - { name: "SOUL_DEW", count: 1 }, - { name: "LUCKY_EGG", count: 1 }, - ]); - await game.classicMode.startBattle(); + .enemyHeldItems([]); // Remove opponent's full stacks of held items so it can steal player's held items + await game.classicMode.startBattle([ Species.TREECKO ]); const playerPokemon = game.scene.getPlayerPokemon()!; - playerPokemon.abilityIndex = 2; - const playerHeldItems = playerPokemon.getHeldItems().length; + const playerHeldItems = getHeldItemCount(playerPokemon); const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); - game.move.select(Moves.POPULATION_BOMB); + // Player's item gets stolen via ability Pickpocket + game.move.select(Moves.FALSE_SWIPE); await game.toNextTurn(); - expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); }); it("should activate when an item is stolen via move", async () => { - vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([ new StealHeldItemChanceAttr(1.0) ]); // give Thief 100% steal rate - game.override.startingHeldItems([ - { name: "MULTI_LENS", count: 3 }, - ]); - await game.classicMode.startBattle(); + game.override.moveset(Moves.THIEF) + .startingHeldItems([]); // Remove player's full stacks of held items so it can steal opponent's held items + await game.classicMode.startBattle([ Species.TREECKO ]); const enemyPokemon = game.scene.getEnemyPokemon()!; - const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const enemyHeldItemCt = getHeldItemCount(enemyPokemon); const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + // Player uses Thief and steals the opponent's item game.move.select(Moves.THIEF); await game.toNextTurn(); - expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + expect(getHeldItemCount(enemyPokemon)).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBe(initialEnemySpeed * 2); }); it("should activate when an item is stolen via grip claw", async () => { game.override - .startingLevel(5) .startingHeldItems([ - { name: "GRIP_CLAW", count: 5 }, - { name: "MULTI_LENS", count: 3 }, - ]) - .enemyHeldItems([ - { name: "SOUL_DEW", count: 1 }, - { name: "LUCKY_EGG", count: 1 }, - { name: "LEFTOVERS", count: 1 }, { name: "GRIP_CLAW", count: 1 }, - { name: "MULTI_LENS", count: 1 }, - { name: "BERRY", type: BerryType.SITRUS, count: 1 }, - { name: "BERRY", type: BerryType.LUM, count: 1 }, ]); - await game.classicMode.startBattle(); - - const enemyPokemon = game.scene.getEnemyPokemon()!; - const enemyHeldItemCt = enemyPokemon.getHeldItems().length; - const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); - - while (enemyPokemon.getHeldItems().length === enemyHeldItemCt) { - game.move.select(Moves.POPULATION_BOMB); - await game.toNextTurn(); - } - - expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); - }); - - it("should not activate when a neutralizing ability is present", async () => { - game.override.enemyAbility(Abilities.NEUTRALIZING_GAS); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([ Species.TREECKO ]); const playerPokemon = game.scene.getPlayerPokemon()!; - const playerHeldItems = playerPokemon.getHeldItems().length; - const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; + vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = getHeldItemCount(enemyPokemon); + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + // Player steals the opponent's item using Grip Claw game.move.select(Moves.FALSE_SWIPE); await game.toNextTurn(); - expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed); + expect(getHeldItemCount(enemyPokemon)).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBe(initialEnemySpeed * 2); + }); + + it("should not activate when a neutralizing ability is present", async () => { + game.override.enemyAbility(Abilities.NEUTRALIZING_GAS) + .enemyMoveset(Moves.FALSE_SWIPE); + await game.classicMode.startBattle([ Species.TREECKO ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = getHeldItemCount(playerPokemon); + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + // Player gets hit by False Swipe and eats Sitrus Berry, which should not trigger Unburden + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed); + expect(playerPokemon.getTag(BattlerTagType.UNBURDEN)).toBeUndefined(); }); it("should activate when a move that consumes a berry is used", async () => { - game.override.enemyMoveset([ Moves.STUFF_CHEEKS ]); - await game.classicMode.startBattle(); + game.override.moveset(Moves.STUFF_CHEEKS); + await game.classicMode.startBattle([ Species.TREECKO ]); - const enemyPokemon = game.scene.getEnemyPokemon()!; - const enemyHeldItemCt = enemyPokemon.getHeldItems().length; - const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItemCt = getHeldItemCount(playerPokemon); + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + // Player uses Stuff Cheeks and eats its own berry + // Caution: Do not test this using opponent, there is a known issue where opponent can randomly generate with Salac Berry game.move.select(Moves.STUFF_CHEEKS); await game.toNextTurn(); - expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItemCt); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); }); - it("should deactivate when a neutralizing gas user enters the field", async () => { + it("should deactivate temporarily when a neutralizing gas user is on the field", async () => { game.override .battleType("double") - .moveset([ Moves.SPLASH ]); + .ability(Abilities.NONE); // Disable ability override so that we can properly set abilities below await game.classicMode.startBattle([ Species.TREECKO, Species.MEOWTH, Species.WEEZING ]); - const playerPokemon = game.scene.getPlayerParty(); - const treecko = playerPokemon[0]; - const weezing = playerPokemon[2]; - treecko.abilityIndex = 2; - weezing.abilityIndex = 1; - const playerHeldItems = treecko.getHeldItems().length; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [ treecko, _meowth, weezing ] = game.scene.getPlayerParty(); + treecko.abilityIndex = 2; // Treecko has Unburden + weezing.abilityIndex = 1; // Weezing has Neutralizing Gas + const playerHeldItems = getHeldItemCount(treecko); const initialPlayerSpeed = treecko.getStat(Stat.SPD); + // Turn 1: Treecko gets hit by False Swipe and eats Sitrus Berry, activating Unburden game.move.select(Moves.SPLASH); - game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); await game.forceEnemyMove(Moves.FALSE_SWIPE, 0); await game.forceEnemyMove(Moves.FALSE_SWIPE, 0); await game.phaseInterceptor.to("TurnEndPhase"); - expect(treecko.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(treecko.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + expect(getHeldItemCount(treecko)).toBeLessThan(playerHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); + // Turn 2: Switch Meowth to Weezing, activating Neutralizing Gas await game.toNextTurn(); game.move.select(Moves.SPLASH); game.doSwitchPokemon(2); await game.phaseInterceptor.to("TurnEndPhase"); - expect(treecko.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(treecko.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed); + expect(getHeldItemCount(treecko)).toBeLessThan(playerHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed); + + // Turn 3: Switch Weezing to Meowth, deactivating Neutralizing Gas + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + game.doSwitchPokemon(2); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(getHeldItemCount(treecko)).toBeLessThan(playerHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); }); + it("should not activate when passing a baton to a teammate switching in", async () => { + game.override.startingHeldItems([{ name: "BATON" }]) + .moveset(Moves.BATON_PASS); + await game.classicMode.startBattle([ Species.TREECKO, Species.PURRLOIN ]); + + const [ treecko, purrloin ] = game.scene.getPlayerParty(); + const initialTreeckoSpeed = treecko.getStat(Stat.SPD); + const initialPurrloinSpeed = purrloin.getStat(Stat.SPD); + const unburdenAttr = treecko.getAbilityAttrs(PostItemLostAbAttr)[0]; + vi.spyOn(unburdenAttr, "applyPostItemLost"); + + // Player uses Baton Pass, which also passes the Baton item + game.move.select(Moves.BATON_PASS); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + expect(getHeldItemCount(treecko)).toBe(0); + expect(getHeldItemCount(purrloin)).toBe(1); + expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialTreeckoSpeed); + expect(purrloin.getEffectiveStat(Stat.SPD)).toBe(initialPurrloinSpeed); + expect(unburdenAttr.applyPostItemLost).not.toHaveBeenCalled(); + }); + + it("should not speed up a Pokemon after it loses the ability Unburden", async () => { + game.override.enemyMoveset([ Moves.FALSE_SWIPE, Moves.WORRY_SEED ]); + await game.classicMode.startBattle([ Species.PURRLOIN ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = getHeldItemCount(playerPokemon); + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + // Turn 1: Get hit by False Swipe and eat Sitrus Berry, activating Unburden + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.FALSE_SWIPE); + await game.toNextTurn(); + + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); + + // Turn 2: Get hit by Worry Seed, deactivating Unburden + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.WORRY_SEED); + await game.toNextTurn(); + + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed); + }); + + it("should activate when a reviver seed is used", async () => { + game.override.startingHeldItems([{ name: "REVIVER_SEED" }]) + .enemyMoveset([ Moves.WING_ATTACK ]); + await game.classicMode.startBattle([ Species.TREECKO ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = getHeldItemCount(playerPokemon); + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + // Turn 1: Get hit by Wing Attack and faint, activating Reviver Seed + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); + }); + + // test for `.bypassFaint()` - singles + it("shouldn't persist when revived normally if activated while fainting", async () => { + game.override.enemyMoveset([ Moves.SPLASH, Moves.THIEF ]); + await game.classicMode.startBattle([ Species.TREECKO, Species.FEEBAS ]); + + const treecko = game.scene.getPlayerPokemon()!; + const treeckoInitialHeldItems = getHeldItemCount(treecko); + const initialSpeed = treecko.getStat(Stat.SPD); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.THIEF); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.doRevivePokemon(1); + game.doSwitchPokemon(1); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + + expect(game.scene.getPlayerPokemon()!).toBe(treecko); + expect(getHeldItemCount(treecko)).toBeLessThan(treeckoInitialHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialSpeed); + }); + + // test for `.bypassFaint()` - doubles + it("shouldn't persist when revived by revival blessing if activated while fainting", async () => { + game.override + .battleType("double") + .enemyMoveset([ Moves.SPLASH, Moves.THIEF ]) + .moveset([ Moves.SPLASH, Moves.REVIVAL_BLESSING ]) + .startingHeldItems([{ name: "WIDE_LENS" }]); + await game.classicMode.startBattle([ Species.TREECKO, Species.FEEBAS, Species.MILOTIC ]); + + const treecko = game.scene.getPlayerField()[0]; + const treeckoInitialHeldItems = getHeldItemCount(treecko); + const initialSpeed = treecko.getStat(Stat.SPD); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.REVIVAL_BLESSING, 1); + await game.forceEnemyMove(Moves.THIEF, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2 ]); + game.doSelectPartyPokemon(0, "MoveEffectPhase"); + await game.toNextTurn(); + + expect(game.scene.getPlayerField()[0]).toBe(treecko); + expect(getHeldItemCount(treecko)).toBeLessThan(treeckoInitialHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialSpeed); + }); }); From f2a2281ff11c5b644698121f6ce33f32830898b0 Mon Sep 17 00:00:00 2001 From: chaosgrimmon <31082757+chaosgrimmon@users.noreply.github.com> Date: Sun, 10 Nov 2024 14:37:21 -0500 Subject: [PATCH 58/98] [Sprite] Implement female icon assets for Meganium and Doduo + Torchic lines (#4841) * [Sprite] Implement more female icons * [Sprite] Add female Doduo/Dodrio icons * [Sprite] Add female Meganium icons * [Sprite] Add female Torchic line icons * [Sprite] Add female Meganium icons * [Sprite] Add female Torchic line icons Identical to male counterpart icons --- public/images/pokemon/icons/2/154-f.png | Bin 0 -> 427 bytes public/images/pokemon/icons/2/154s-f.png | Bin 0 -> 464 bytes public/images/pokemon/icons/3/255-f.png | Bin 0 -> 267 bytes public/images/pokemon/icons/3/255s-f.png | Bin 0 -> 268 bytes public/images/pokemon/icons/3/256-f.png | Bin 0 -> 373 bytes public/images/pokemon/icons/3/256s-f.png | Bin 0 -> 451 bytes public/images/pokemon/icons/3/257-f-mega.png | Bin 0 -> 493 bytes public/images/pokemon/icons/3/257-f.png | Bin 0 -> 451 bytes public/images/pokemon/icons/3/257s-f-mega.png | Bin 0 -> 524 bytes public/images/pokemon/icons/3/257s-f.png | Bin 0 -> 530 bytes public/images/pokemon_icons_1.json | 84 ++++++++++++ public/images/pokemon_icons_2.json | 42 ++++++ public/images/pokemon_icons_3.json | 126 ++++++++++++++++++ src/data/pokemon-species.ts | 6 + 14 files changed, 258 insertions(+) create mode 100644 public/images/pokemon/icons/2/154-f.png create mode 100644 public/images/pokemon/icons/2/154s-f.png create mode 100644 public/images/pokemon/icons/3/255-f.png create mode 100644 public/images/pokemon/icons/3/255s-f.png create mode 100644 public/images/pokemon/icons/3/256-f.png create mode 100644 public/images/pokemon/icons/3/256s-f.png create mode 100644 public/images/pokemon/icons/3/257-f-mega.png create mode 100644 public/images/pokemon/icons/3/257-f.png create mode 100644 public/images/pokemon/icons/3/257s-f-mega.png create mode 100644 public/images/pokemon/icons/3/257s-f.png diff --git a/public/images/pokemon/icons/2/154-f.png b/public/images/pokemon/icons/2/154-f.png new file mode 100644 index 0000000000000000000000000000000000000000..6481cdd8a00bbba52c035ebd8b9867d44a82ef37 GIT binary patch literal 427 zcmV;c0aX5pP)X0004UNklb>!3`Ko}9KmPln9qguG1rl}A06mWOg+BJP{@*(Z> z*U#S8x+pO~00cn7X+CwTv7v{Tg2X20oMx2_ zw1mNw@a3K%Z-Bn!wY4UIghXOU2xsc>W$LpBBwSbzLNWTC9|{e&K{()iav;Q%S#G>jIK2-6 z0WbcEP)X0004(Nkllfr0~AoPrB*0NimODkP2q1r-!7({Tnwl_StlEdCpR>u0nZM`U)rBaMQ_e2=M9gCO(cYSKsaYI&Oy2Wi6fA0 zjq7C!MdDnB&(}lF$keiT2mbEgmMTT)6cICpZTEtlfwtv(%@s%_5<^0uJ&UO5&-48e zRI%nb_yF+_6{r|{jn}^(t|OF4fD`A{cSy9I`hmGBS|GoWJJ6mTcHif10hS9?Z$hG)8|!y91bFN*V(1a)r719vX0002eNkl}K_3|xr&Rb$_)Au<X0002fNkl+%NOFXINQwd!$zv4G=~ z>)W)`AU&>EIw0}|5Z(Mal4s9uXEKP)X0003zNklNN?OW{zzQi@!B`^SD8|W!!j2B2yMMrOhhHT9AJQ~? z?e$%?ZR;UbPFxz9Ug%m&fhO4f(=&5sJV*)Zs@H) z+v$i^IF{g~RCR4moA2EcoLWFK2=AZt(!6GqQRZ};oL}bZk~g!;$jlU zoEtaoY7P`DQXG;Fa^__e0OpJWqs8w-6$OMrNN&tO5@%u3Rk^Rcv!SrlRRASqm%`fV zUoHSviysMz4gpF^0@Vv|cVwlmOd&VBCojbXZiTo2m5-i$_X&W-4e?XY$A4K*f(EN@ Twsxr800000NkvXXu0mjf8iAT> literal 0 HcmV?d00001 diff --git a/public/images/pokemon/icons/3/256s-f.png b/public/images/pokemon/icons/3/256s-f.png new file mode 100644 index 0000000000000000000000000000000000000000..ce6608f7bc5c4a2cb11ca0705cf6b7f442acbe70 GIT binary patch literal 451 zcmV;!0X+VRP)X0004sNklQLj4P1H&kDG(<1EUoLXyy&F zu)Lr5&xXP|S3I+d7oauZt)nbUM@^lZ6i~1K?3Y1+5uaXMgNCk~1&~k@j>ptP0FRr6 z7s1+jfRl5=ngjlx-lKwX0-evbYuEnaJ}+>wedd7QhjkDj0N2jl*T8SlaQN?yM8Jb3GM?*^1jm#V6&2Z$j7aRXzJ z$pB#lVMF*Rz{#c@Lb_dFa>JY=ooN8vM<-VkV%t7se z?h!Uw%|ozB0b_751~JUBa3fb^piB`nK2#W79OaWOS5CHMk0^wMD zRpx646>>TYpoH|zTEKEHA?ZES9E?HILxhr&MaH>#k<@kVrQ3&wH1C6B9vm{xM#vU-`U8!~)f{J@Yb*c&002ovPDHLkV1gct%q#!^ literal 0 HcmV?d00001 diff --git a/public/images/pokemon/icons/3/257-f-mega.png b/public/images/pokemon/icons/3/257-f-mega.png new file mode 100644 index 0000000000000000000000000000000000000000..ed64fe8f41f3cedeca980ad3befcf181ac75f327 GIT binary patch literal 493 zcmVX0005BNkl*MZf5rBvT3fMi{R$gRx5+FvJ0Exi} zhi)5S&TcL)2Ll4BdjM)KtMscI6KHY3wqqvd(7}Uv)yojqF?1Ym@G)zF8)G9cH6DB1 z^Q7?yV!b~Qdo-QaAWs6OBb7KDfbKnx8#%w6>UEk4lSyz6DT6W$AfE8I3dPwIhcG}t zLgvC~R41e1apet!gGR`xFE|IBrK@q{OX0004sNkl zF>b>!3`KK{j=n}lUqM&t*fC?*jv0H6jvh01%~jNp%@ckI*GU1JS|WguV;S+&C;8>w z?m3djFbuUf!y;ULE%hFxt#{eTBY=-?A`K9=h;DFmkbYe!k5Y=?{}@OA-3<~z2MTag z**Cy$kN_IY_w?4_P|xlG2xKa#z0Ba6iUH1MRnfxkr$Yh;oIwMwwHVb*fm1;0t{l#_ zp2izBiOo~4Hex`QT2~0AE?^FQ1>_lpRM1sZ?oeN!9>@1Xo1=;bXHW$7XwL#@*Q=%+ zIKX&0f2@{cK^;bgHRTGq3Lud7Qk09&pqQ$s4Z|jY)J?9EVvV_V(iyGAo9AjpwmF4N z>*xk|n)g2xqf}N^@1CmETWb{YKTaA{q>hQrAOX{$A#O}sW)##p1&|YjPPMfN3m~ea ziX0005gNkl{prMAI1<+7ZrDX+_)QDx!A$CBS3K<MzwOx4 z$j;UE_{-c!oK;a2t0l+50iA($N0w#v9Y#2M1BH8i+!k$+>O(9F2YD<-Y&V5s5YUq}0V7U*QWaFr&)rBs5&L}1|^=pm0xTzrefe(|^FG)f=lAKeqx_ebWexIJGO9wxZF1`}dF3daON(#t{K-jXD9*M3 O0000X0005mNkllc3X0T`H~<9&H5Wj%IRW*uZ?&KG7&#F?_5u!U_6}i z;Uz~R4x_`qUKLfVhs6 zO=A$q&Bf6Oq}EFhkbeBBf0&oUquKkQCy2mYu|873X@MN>+r6-Qjf`-5I2v6VprV>P zSSsWZ?hvFF188%=5>$i(D7rX{gIS8Hf;a{<)bv8p6!5bbArSAY{BZyOZ{`Df1F=YV U=XA_GX#fBK07*qoM6N<$g1^J=Hvj+t literal 0 HcmV?d00001 diff --git a/public/images/pokemon_icons_1.json b/public/images/pokemon_icons_1.json index 49e471514cd..12e26b380a5 100644 --- a/public/images/pokemon_icons_1.json +++ b/public/images/pokemon_icons_1.json @@ -1647,6 +1647,27 @@ "h": 25 } }, + { + "filename": "85-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 5, + "y": 3, + "w": 29, + "h": 25 + }, + "frame": { + "x": 55, + "y": 270, + "w": 29, + "h": 25 + } + }, { "filename": "22s", "rotated": false, @@ -1731,6 +1752,27 @@ "h": 25 } }, + { + "filename": "85s-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 5, + "y": 3, + "w": 29, + "h": 25 + }, + "frame": { + "x": 56, + "y": 317, + "w": 29, + "h": 25 + } + }, { "filename": "9s", "rotated": false, @@ -6456,6 +6498,27 @@ "h": 18 } }, + { + "filename": "84-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 10, + "w": 21, + "h": 18 + }, + "frame": { + "x": 98, + "y": 712, + "w": 21, + "h": 18 + } + }, { "filename": "107", "rotated": false, @@ -6519,6 +6582,27 @@ "h": 18 } }, + { + "filename": "84s-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 10, + "w": 21, + "h": 18 + }, + "frame": { + "x": 96, + "y": 770, + "w": 21, + "h": 18 + } + }, { "filename": "88", "rotated": false, diff --git a/public/images/pokemon_icons_2.json b/public/images/pokemon_icons_2.json index 5a389362bc0..c5ebfe61487 100644 --- a/public/images/pokemon_icons_2.json +++ b/public/images/pokemon_icons_2.json @@ -786,6 +786,27 @@ "h": 27 } }, + { + "filename": "154-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 1, + "w": 23, + "h": 27 + }, + "frame": { + "x": 29, + "y": 147, + "w": 23, + "h": 27 + } + }, { "filename": "154s", "rotated": false, @@ -807,6 +828,27 @@ "h": 27 } }, + { + "filename": "154s-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 1, + "w": 23, + "h": 27 + }, + "frame": { + "x": 29, + "y": 174, + "w": 23, + "h": 27 + } + }, { "filename": "229-mega", "rotated": false, diff --git a/public/images/pokemon_icons_3.json b/public/images/pokemon_icons_3.json index 220d91f5222..a1aefa0ff0b 100644 --- a/public/images/pokemon_icons_3.json +++ b/public/images/pokemon_icons_3.json @@ -198,6 +198,27 @@ "h": 27 } }, + { + "filename": "257-f-mega", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 4, + "y": 2, + "w": 32, + "h": 27 + }, + "frame": { + "x": 0, + "y": 79, + "w": 32, + "h": 27 + } + }, { "filename": "257s-mega", "rotated": false, @@ -219,6 +240,27 @@ "h": 27 } }, + { + "filename": "257s-f-mega", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 4, + "y": 2, + "w": 32, + "h": 27 + }, + "frame": { + "x": 0, + "y": 106, + "w": 32, + "h": 27 + } + }, { "filename": "323-mega", "rotated": false, @@ -1248,6 +1290,27 @@ "h": 26 } }, + { + "filename": "257-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 2, + "w": 25, + "h": 26 + }, + "frame": { + "x": 28, + "y": 556, + "w": 25, + "h": 26 + } + }, { "filename": "257s", "rotated": false, @@ -1269,6 +1332,27 @@ "h": 26 } }, + { + "filename": "257s-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 2, + "w": 25, + "h": 26 + }, + "frame": { + "x": 28, + "y": 582, + "w": 25, + "h": 26 + } + }, { "filename": "359-mega", "rotated": false, @@ -1605,6 +1689,27 @@ "h": 25 } }, + { + "filename": "256-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 3, + "w": 23, + "h": 25 + }, + "frame": { + "x": 98, + "y": 72, + "w": 23, + "h": 25 + } + }, { "filename": "282s-mega", "rotated": false, @@ -5553,6 +5658,27 @@ "h": 19 } }, + { + "filename": "255-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 13, + "y": 9, + "w": 13, + "h": 19 + }, + "frame": { + "x": 204, + "y": 342, + "w": 13, + "h": 19 + } + }, { "filename": "307s", "rotated": false, diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index ff53fdb9392..203e545503a 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -363,6 +363,12 @@ export abstract class PokemonSpeciesForm { } switch (this.speciesId) { + case Species.DODUO: + case Species.DODRIO: + case Species.MEGANIUM: + case Species.TORCHIC: + case Species.COMBUSKEN: + case Species.BLAZIKEN: case Species.HIPPOPOTAS: case Species.HIPPOWDON: case Species.UNFEZANT: From efa9f119a0e803ff13409e67adaa9f1dbef206b0 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Mon, 11 Nov 2024 02:18:57 -0500 Subject: [PATCH 59/98] [Beta][P3] Fix shiny Pokemon being displayed before shiny colours are loaded (#4843) --- src/field/pokemon.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d806a9b605c..9e5103656d3 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -442,7 +442,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }; if (this.shiny) { const populateVariantColors = (isBackSprite: boolean = false): Promise => { - return new Promise(resolve => { + return new Promise(async resolve => { const battleSpritePath = this.getBattleSpriteAtlasPath(isBackSprite, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, ""); let config = variantData; const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(isBackSprite, ignoreOverride)); @@ -451,7 +451,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (variantSet && variantSet[this.variant] === 1) { const cacheKey = this.getBattleSpriteKey(isBackSprite); if (!variantColorCache.hasOwnProperty(cacheKey)) { - this.populateVariantColorCache(cacheKey, useExpSprite, battleSpritePath); + await this.populateVariantColorCache(cacheKey, useExpSprite, battleSpritePath); } } resolve(); @@ -483,10 +483,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param battleSpritePath the filename of the sprite * @param optionalParams any additional params to log */ - fallbackVariantColor(cacheKey: string, attemptedSpritePath: string, useExpSprite: boolean, battleSpritePath: string, ...optionalParams: any[]) { + async fallbackVariantColor(cacheKey: string, attemptedSpritePath: string, useExpSprite: boolean, battleSpritePath: string, ...optionalParams: any[]) { console.warn(`Could not load ${attemptedSpritePath}!`, ...optionalParams); if (useExpSprite) { - this.populateVariantColorCache(cacheKey, false, battleSpritePath); + await this.populateVariantColorCache(cacheKey, false, battleSpritePath); } } @@ -497,18 +497,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param useExpSprite should the experimental sprite be used * @param battleSpritePath the filename of the sprite */ - populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) { + async populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) { const spritePath = `./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`; - this.scene.cachedFetch(spritePath).then(res => { + return this.scene.cachedFetch(spritePath).then(res => { // Prevent the JSON from processing if it failed to load if (!res.ok) { return this.fallbackVariantColor(cacheKey, res.url, useExpSprite, battleSpritePath, res.status, res.statusText); } return res.json(); }).catch(error => { - this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error); + return this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error); }).then(c => { - variantColorCache[cacheKey] = c; + if (!isNullOrUndefined(c)) { + variantColorCache[cacheKey] = c; + } }); } From 6799594bbb43a8a7a8431a5f7c52062ee917dd3e Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 10 Nov 2024 23:21:06 -0800 Subject: [PATCH 60/98] [Test] Update Zen Mode test (#4845) --- src/test/abilities/zen_mode.test.ts | 87 ++++++++++------------------- 1 file changed, 31 insertions(+), 56 deletions(-) diff --git a/src/test/abilities/zen_mode.test.ts b/src/test/abilities/zen_mode.test.ts index 4ba5e3d5929..e0cc457c4d5 100644 --- a/src/test/abilities/zen_mode.test.ts +++ b/src/test/abilities/zen_mode.test.ts @@ -1,14 +1,8 @@ -import { BattlerIndex } from "#app/battle"; import { Status } from "#app/data/status-effect"; -import { DamagePhase } from "#app/phases/damage-phase"; -import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; -import { Mode } from "#app/ui/ui"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; -import { SwitchType } from "#enums/switch-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -34,78 +28,60 @@ describe("Abilities - ZEN MODE", () => { game = new GameManager(phaserGame); game.override .battleType("single") - .enemySpecies(Species.RATTATA) - .enemyAbility(Abilities.HYDRATION) + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyLevel(5) .ability(Abilities.ZEN_MODE) - .startingLevel(100) .moveset(Moves.SPLASH) - .enemyMoveset(Moves.TACKLE); + .enemyMoveset(Moves.SEISMIC_TOSS); }); it("shouldn't change form when taking damage if not dropping below 50% HP", async () => { await game.classicMode.startBattle([ Species.DARMANITAN ]); - const player = game.scene.getPlayerPokemon()!; - player.stats[Stat.HP] = 100; - player.hp = 100; - expect(player.formIndex).toBe(baseForm); + const darmanitan = game.scene.getPlayerPokemon()!; + expect(darmanitan.formIndex).toBe(baseForm); game.move.select(Moves.SPLASH); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to("BerryPhase"); + await game.toNextTurn(); - expect(player.hp).toBeLessThan(100); - expect(player.formIndex).toBe(baseForm); + expect(darmanitan.getHpRatio()).toBeLessThan(1); + expect(darmanitan.getHpRatio()).toBeGreaterThan(0.5); + expect(darmanitan.formIndex).toBe(baseForm); }); it("should change form when falling below 50% HP", async () => { await game.classicMode.startBattle([ Species.DARMANITAN ]); - const player = game.scene.getPlayerPokemon()!; - player.stats[Stat.HP] = 1000; - player.hp = 100; - expect(player.formIndex).toBe(baseForm); + const darmanitan = game.scene.getPlayerPokemon()!; + darmanitan.hp = (darmanitan.getMaxHp() / 2) + 1; + expect(darmanitan.formIndex).toBe(baseForm); game.move.select(Moves.SPLASH); + await game.toNextTurn(); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to("QuietFormChangePhase"); - await game.phaseInterceptor.to("TurnInitPhase", false); - - expect(player.hp).not.toBe(100); - expect(player.formIndex).toBe(zenForm); + expect(darmanitan.getHpRatio()).toBeLessThan(0.5); + expect(darmanitan.formIndex).toBe(zenForm); }); it("should stay zen mode when fainted", async () => { await game.classicMode.startBattle([ Species.DARMANITAN, Species.CHARIZARD ]); - const player = game.scene.getPlayerPokemon()!; - player.stats[Stat.HP] = 1000; - player.hp = 100; - expect(player.formIndex).toBe(baseForm); + const darmanitan = game.scene.getPlayerPokemon()!; + darmanitan.hp = (darmanitan.getMaxHp() / 2) + 1; + expect(darmanitan.formIndex).toBe(baseForm); game.move.select(Moves.SPLASH); + await game.toNextTurn(); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to(DamagePhase, false); - const damagePhase = game.scene.getCurrentPhase() as DamagePhase; - damagePhase.updateAmount(80); - await game.phaseInterceptor.to("QuietFormChangePhase"); + expect(darmanitan.getHpRatio()).toBeLessThan(0.5); + expect(darmanitan.formIndex).toBe(zenForm); - expect(player.hp).not.toBe(100); - expect(player.formIndex).toBe(zenForm); - - await game.killPokemon(player); - expect(player.isFainted()).toBe(true); - - await game.phaseInterceptor.to("TurnStartPhase"); - game.onNextPrompt("SwitchPhase", Mode.PARTY, () => { - game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, SwitchType.SWITCH, 0, 1, false)); - game.scene.ui.setMode(Mode.MESSAGE); - }); - game.onNextPrompt("SwitchPhase", Mode.MESSAGE, () => { - game.endPhase(); - }); - await game.phaseInterceptor.to("PostSummonPhase"); + game.move.select(Moves.SPLASH); + await game.killPokemon(darmanitan); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + expect(darmanitan.isFainted()).toBe(true); expect(game.scene.getPlayerParty()[1].formIndex).toBe(zenForm); }); @@ -117,7 +93,8 @@ describe("Abilities - ZEN MODE", () => { await game.classicMode.startBattle([ Species.MAGIKARP, Species.DARMANITAN ]); - const darmanitan = game.scene.getPlayerParty().find((p) => p.species.speciesId === Species.DARMANITAN)!; + const darmanitan = game.scene.getPlayerParty()[1]; + darmanitan.hp = 1; expect(darmanitan.formIndex).toBe(zenForm); darmanitan.hp = 0; @@ -126,9 +103,7 @@ describe("Abilities - ZEN MODE", () => { game.move.select(Moves.SPLASH); await game.doKillOpponents(); - await game.phaseInterceptor.to("TurnEndPhase"); - game.doSelectModifier(); - await game.phaseInterceptor.to("QuietFormChangePhase"); + await game.toNextWave(); expect(darmanitan.formIndex).toBe(baseForm); }); From 6feb63484c308d3f376e1f77ed2c05ae760d9fb3 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Mon, 11 Nov 2024 09:29:20 -0800 Subject: [PATCH 61/98] [P3] Added `failIfSingleBattle` condtion to Doubles-only moves and display failure message when used in singles (#4839) * Added failIfSingleBattle condtion to Helping Hand * Added failIfSingleBattle conditions to Doubles-Only moves * Adjusted canMove failure condition. * Updated moves that failIfSingleBattle * Fixed condtional. --------- Co-authored-by: frutescens --- src/data/move.ts | 11 ++++++++--- src/phases/move-phase.ts | 19 ++++++------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 9cd4881488c..a79ac386a7e 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8480,7 +8480,8 @@ export function initMoves() { new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3) .attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND) .ignoresSubstitute() - .target(MoveTarget.NEAR_ALLY), + .target(MoveTarget.NEAR_ALLY) + .condition(failIfSingleBattle), new StatusMove(Moves.TRICK, Type.PSYCHIC, 100, 10, -1, 0, 3) .unimplemented(), new StatusMove(Moves.ROLE_PLAY, Type.PSYCHIC, -1, 10, -1, 0, 3) @@ -9172,6 +9173,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES) .attr(RemoveHeldItemAttr, true), new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5) + .condition(failIfSingleBattle) .unimplemented(), new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))), @@ -9459,6 +9461,7 @@ export function initMoves() { new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1) .ignoresSubstitute() + .condition(failIfSingleBattle) .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2), @@ -9687,7 +9690,8 @@ export function initMoves() { new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7) .makesContact(false), new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7) - .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false), + .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false) + .condition(failIfSingleBattle), new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7) .attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatStageChangeAttr, [ Stat.SPD ], -1), @@ -10144,7 +10148,8 @@ export function initMoves() { .unimplemented(), new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1) - .target(MoveTarget.NEAR_ALLY), + .target(MoveTarget.NEAR_ALLY) + .condition(failIfSingleBattle), new AttackMove(Moves.FLIP_TURN, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 8) .attr(ForceSwitchOutAttr, true), new AttackMove(Moves.TRIPLE_AXEL, Type.ICE, MoveCategory.PHYSICAL, 20, 90, 10, -1, 0, 8) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 6bdef281d70..7cfa3b12476 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -120,13 +120,10 @@ export class MovePhase extends BattlePhase { console.log(Moves[this.move.moveId]); // Check if move is unusable (e.g. because it's out of PP due to a mid-turn Spite). - if (!this.canMove(true)) { - if (this.pokemon.isActive(true) && this.move.ppUsed >= this.move.getMovePp()) { - this.fail(); - this.showMoveText(); - this.showFailedText(); - } - + if (!this.canMove(true) && (this.pokemon.isActive(true) || this.move.ppUsed >= this.move.getMovePp())) { + this.fail(); + this.showMoveText(); + this.showFailedText(); return this.end(); } @@ -378,16 +375,12 @@ export class MovePhase extends BattlePhase { } else { this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual }); - let failedText: string | undefined; const failureMessage = move.getFailedText(this.pokemon, targets[0], move, new BooleanHolder(false)); - if (failureMessage) { - failedText = failureMessage; + this.showMoveText(); + this.showFailedText(failureMessage); } - this.showMoveText(); - this.showFailedText(failedText); - // Remove the user from its semi-invulnerable state (if applicable) this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); } From e5e392617615ec5023afe5662ceb3cd7c0720f6d Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:13:15 -0800 Subject: [PATCH 62/98] [beta] Fix MovePhase not ending properly. (#4848) Co-authored-by: frutescens --- src/phases/move-phase.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 7cfa3b12476..378b72e1f56 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -120,10 +120,12 @@ export class MovePhase extends BattlePhase { console.log(Moves[this.move.moveId]); // Check if move is unusable (e.g. because it's out of PP due to a mid-turn Spite). - if (!this.canMove(true) && (this.pokemon.isActive(true) || this.move.ppUsed >= this.move.getMovePp())) { - this.fail(); - this.showMoveText(); - this.showFailedText(); + if (!this.canMove(true)) { + if (this.pokemon.isActive(true)) { + this.fail(); + this.showMoveText(); + this.showFailedText(); + } return this.end(); } From cebedd220bd4259b4bee449bb88cc686853b3474 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:56:16 -0800 Subject: [PATCH 63/98] [Balance] Rework Multi-Lens (#4831) * Rework Multi-Lens * Multi-Lens integration tests * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fix obsolete tests related to Multi-Lens * Fix flaky unburden tests * maybe fix flaky ceaseless edge test? * Fixed Multi-Lens apply comment * Fix ceaseless edge test for real this time * Update locales * Another locale update --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- public/locales | 2 +- src/data/ability.ts | 61 ++++----------- src/data/move.ts | 38 ++++++++- src/field/pokemon.ts | 9 ++- src/modifier/modifier.ts | 64 +++++++++++----- src/phases/move-effect-phase.ts | 11 +-- src/test/abilities/parental_bond.test.ts | 60 ++------------- src/test/items/multi_lens.test.ts | 98 ++++++++++++++++++++++++ src/test/moves/beat_up.test.ts | 25 ------ src/test/moves/ceaseless_edge.test.ts | 12 +-- src/test/moves/dragon_rage.test.ts | 11 --- src/test/moves/electro_shot.test.ts | 2 +- 12 files changed, 215 insertions(+), 178 deletions(-) create mode 100644 src/test/items/multi_lens.test.ts diff --git a/public/locales b/public/locales index d600913dbf1..5775faa6b31 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit d600913dbf1f8b47dae8dccbd8296df78f1c51b5 +Subproject commit 5775faa6b3184082df73f6cdb96b253ea7dae3fe diff --git a/src/data/ability.ts b/src/data/ability.ts index 49763991e0e..4194be31405 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -7,7 +7,7 @@ import { Weather } from "#app/data/weather"; import { BattlerTag, BattlerTagLapseType, GroundedTag } from "./battler-tags"; import { getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "#app/data/status-effect"; import { Gender } from "./gender"; -import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; +import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { TerrainType } from "./terrain"; @@ -1351,65 +1351,30 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr { this.damageMultiplier = damageMultiplier; } - /** - * Determines whether this attribute can apply to a given move. - * @param {Move} move the move to which this attribute may apply - * @param numTargets the number of {@linkcode Pokemon} targeted by this move - * @returns true if the attribute can apply to the move, false otherwise - */ - canApplyPreAttack(move: Move, numTargets: integer): boolean { - /** - * Parental Bond cannot apply to multi-hit moves, charging moves, or - * moves that cause the user to faint. - */ - const exceptAttrs: Constructor[] = [ - MultiHitAttr, - SacrificialAttr, - SacrificialAttrOnHit - ]; - - /** Parental Bond cannot apply to these specific moves */ - const exceptMoves: Moves[] = [ - Moves.FLING, - Moves.UPROAR, - Moves.ROLLOUT, - Moves.ICE_BALL, - Moves.ENDEAVOR - ]; - - /** Also check if this move is an Attack move and if it's only targeting one Pokemon */ - return numTargets === 1 - && !move.isChargingMove() - && !exceptAttrs.some(attr => move.hasAttr(attr)) - && !exceptMoves.some(id => move.id === id) - && move.category !== MoveCategory.STATUS; - } - /** * If conditions are met, this doubles the move's hit count (via args[1]) * or multiplies the damage of secondary strikes (via args[2]) - * @param {Pokemon} pokemon the Pokemon using the move + * @param pokemon the {@linkcode Pokemon} using the move * @param passive n/a * @param defender n/a - * @param {Move} move the move used by the ability source - * @param args\[0\] the number of Pokemon this move is targeting - * @param {Utils.IntegerHolder} args\[1\] the number of strikes with this move - * @param {Utils.NumberHolder} args\[2\] the damage multiplier for the current strike + * @param move the {@linkcode Move} used by the ability source + * @param args Additional arguments: + * - `[0]` the number of strikes this move currently has ({@linkcode Utils.NumberHolder}) + * - `[1]` the damage multiplier for the current strike ({@linkcode Utils.NumberHolder}) * @returns */ applyPreAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, args: any[]): boolean { - const numTargets = args[0] as integer; - const hitCount = args[1] as Utils.IntegerHolder; - const multiplier = args[2] as Utils.NumberHolder; + const hitCount = args[0] as Utils.NumberHolder; + const multiplier = args[1] as Utils.NumberHolder; - if (this.canApplyPreAttack(move, numTargets)) { + if (move.canBeMultiStrikeEnhanced(pokemon)) { this.showAbility = !!hitCount?.value; - if (!!hitCount?.value) { - hitCount.value *= 2; + if (hitCount?.value) { + hitCount.value += 1; } - if (!!multiplier?.value && pokemon.turnData.hitsLeft % 2 === 1 && pokemon.turnData.hitsLeft !== pokemon.turnData.hitCount) { - multiplier.value *= this.damageMultiplier; + if (multiplier?.value && pokemon.turnData.hitsLeft === 1) { + multiplier.value = this.damageMultiplier; } return true; } diff --git a/src/data/move.ts b/src/data/move.ts index a79ac386a7e..0de9d9b53a2 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -818,7 +818,7 @@ export default class Move implements Localizable { applyMoveAttrs(VariablePowerAttr, source, target, this, power); - source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power); + source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, this.id, null, power); if (!this.hasAttr(TypelessAttr)) { source.scene.arena.applyTags(WeakenMoveTypeTag, simulated, this.type, power); @@ -840,6 +840,42 @@ export default class Move implements Localizable { return priority.value; } + + /** + * Returns `true` if this move can be given additional strikes + * by enhancing effects. + * Currently used for {@link https://bulbapedia.bulbagarden.net/wiki/Parental_Bond_(Ability) | Parental Bond} + * and {@linkcode PokemonMultiHitModifier | Multi-Lens}. + */ + canBeMultiStrikeEnhanced(user: Pokemon): boolean { + // Multi-strike enhancers... + + // ...cannot enhance moves that hit multiple targets + const { targets, multiple } = getMoveTargets(user, this.id); + const isMultiTarget = multiple && targets.length > 1; + + // ...cannot enhance multi-hit or sacrificial moves + const exceptAttrs: Constructor[] = [ + MultiHitAttr, + SacrificialAttr, + SacrificialAttrOnHit + ]; + + // ...and cannot enhance these specific moves. + const exceptMoves: Moves[] = [ + Moves.FLING, + Moves.UPROAR, + Moves.ROLLOUT, + Moves.ICE_BALL, + Moves.ENDEAVOR + ]; + + return !isMultiTarget + && !this.isChargingMove() + && !exceptAttrs.some(attr => this.hasAttr(attr)) + && !exceptMoves.some(id => this.id === id) + && this.category !== MoveCategory.STATUS; + } } export class AttackMove extends Move { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9e5103656d3..5d912f7d6e6 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2642,10 +2642,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const numTargets = multiple ? targets.length : 1; const targetMultiplier = (numTargets > 1) ? 0.75 : 1; - /** 0.25x multiplier if this is an added strike from the attacker's Parental Bond */ - const parentalBondMultiplier = new Utils.NumberHolder(1); + /** Multiplier for moves enhanced by Multi-Lens and/or Parental Bond */ + const multiStrikeEnhancementMultiplier = new Utils.NumberHolder(1); + source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, move.id, null, multiStrikeEnhancementMultiplier); if (!ignoreSourceAbility) { - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, simulated, numTargets, new Utils.IntegerHolder(0), parentalBondMultiplier); + applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, simulated, null, multiStrikeEnhancementMultiplier); } /** Doubles damage if this Pokemon's last move was Glaive Rush */ @@ -2722,7 +2723,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { damage.value = Utils.toDmgValue( baseDamage * targetMultiplier - * parentalBondMultiplier.value + * multiStrikeEnhancementMultiplier.value * arenaAttackTypeMultiplier.value * glaiveRushMultiplier.value * criticalMultiplier.value diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 90336780ba6..5e60d888072 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -6,7 +6,6 @@ import { allMoves } from "#app/data/move"; import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; import { type FormChangeItem, SpeciesFormChangeItemTrigger, SpeciesFormChangeLapseTeraTrigger, SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectHealText } from "#app/data/status-effect"; -import { Type } from "#enums/type"; import Pokemon, { type PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; @@ -22,11 +21,13 @@ import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, toDmgValue } import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; +import { Moves } from "#enums/moves"; import type { Nature } from "#enums/nature"; import type { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; import { type PermanentStat, type TempBattleStat, BATTLE_STATS, Stat, TEMP_BATTLE_STATS } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; +import { Type } from "#enums/type"; import i18next from "i18next"; import { type DoubleBattleChanceBoosterModifierType, type EvolutionItemModifierType, type FormChangeItemModifierType, type ModifierOverride, type ModifierType, type PokemonBaseStatTotalModifierType, type PokemonExpBoosterModifierType, type PokemonFriendshipBoosterModifierType, type PokemonMoveAccuracyBoosterModifierType, type PokemonMultiHitModifierType, type TerastallizeModifierType, type TmModifierType, getModifierType, ModifierPoolType, ModifierTypeGenerator, modifierTypes, PokemonHeldItemModifierType } from "./modifier-type"; import { Color, ShadowColor } from "#enums/color"; @@ -2689,32 +2690,57 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { } /** - * Applies {@linkcode PokemonMultiHitModifier} - * @param _pokemon The {@linkcode Pokemon} using the move - * @param count {@linkcode NumberHolder} holding the number of items - * @param power {@linkcode NumberHolder} holding the power of the move + * For each stack, converts 25 percent of attack damage into an additional strike. + * @param pokemon The {@linkcode Pokemon} using the move + * @param moveId The {@linkcode Moves | identifier} for the move being used + * @param count {@linkcode NumberHolder} holding the move's hit count for this turn + * @param damageMultiplier {@linkcode NumberHolder} holding a damage multiplier applied to a strike of this move * @returns always `true` */ - override apply(_pokemon: Pokemon, count: NumberHolder, power: NumberHolder): boolean { - count.value *= (this.getStackCount() + 1); - - switch (this.getStackCount()) { - case 1: - power.value *= 0.4; - break; - case 2: - power.value *= 0.25; - break; - case 3: - power.value *= 0.175; - break; + override apply(pokemon: Pokemon, moveId: Moves, count: NumberHolder | null = null, damageMultiplier: NumberHolder | null = null): boolean { + const move = allMoves[moveId]; + /** + * The move must meet Parental Bond's restrictions for this item + * to apply. This means + * - Only attacks are boosted + * - Multi-strike moves, charge moves, and self-sacrificial moves are not boosted + * (though Multi-Lens can still affect moves boosted by Parental Bond) + * - Multi-target moves are not boosted *unless* they can only hit a single Pokemon + * - Fling, Uproar, Rollout, Ice Ball, and Endeavor are not boosted + */ + if (!move.canBeMultiStrikeEnhanced(pokemon)) { + return false; } + if (!isNullOrUndefined(count)) { + return this.applyHitCountBoost(count); + } else if (!isNullOrUndefined(damageMultiplier)) { + return this.applyPowerModifier(pokemon, damageMultiplier); + } + + return false; + } + + /** Adds strikes to a move equal to the number of stacked Multi-Lenses */ + private applyHitCountBoost(count: NumberHolder): boolean { + count.value += this.getStackCount(); + return true; + } + + /** + * If applied to the first hit of a move, sets the damage multiplier + * equal to (1 - the number of stacked Multi-Lenses). + * Additional strikes beyond that are given a 0.25x damage multiplier + */ + private applyPowerModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean { + damageMultiplier.value = (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount) + ? (1 - (0.25 * this.getStackCount())) + : 0.25; return true; } getMaxHeldItemCount(pokemon: Pokemon): number { - return 3; + return 2; } } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index ef863d64c50..24a0b51da96 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -26,7 +26,6 @@ import { applyMoveAttrs, AttackMove, DelayedAttackAttr, - FixedDamageAttr, HitsTagAttr, MissEffectAttr, MoveAttr, @@ -122,12 +121,10 @@ export class MoveEffectPhase extends PokemonPhase { const hitCount = new NumberHolder(1); // Assume single target for multi hit applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount); - // If Parental Bond is applicable, double the hit count - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new NumberHolder(0)); - // If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user - if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) { - this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new NumberHolder(0)); - } + // If Parental Bond is applicable, add another hit + applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, hitCount, null); + // If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses + this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount); // Set the user's relevant turnData fields to reflect the final hit count user.turnData.hitCount = hitCount.value; user.turnData.hitsLeft = hitCount.value; diff --git a/src/test/abilities/parental_bond.test.ts b/src/test/abilities/parental_bond.test.ts index d8f952ae6ad..4189941a51e 100644 --- a/src/test/abilities/parental_bond.test.ts +++ b/src/test/abilities/parental_bond.test.ts @@ -274,7 +274,7 @@ describe("Abilities - Parental Bond", () => { ); it( - "Moves boosted by this ability and Multi-Lens should strike 4 times", + "Moves boosted by this ability and Multi-Lens should strike 3 times", async () => { game.override.moveset([ Moves.TACKLE ]); game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); @@ -287,36 +287,12 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("DamagePhase"); - expect(leadPokemon.turnData.hitCount).toBe(4); + expect(leadPokemon.turnData.hitCount).toBe(3); } ); it( - "Super Fang boosted by this ability and Multi-Lens should strike twice", - async () => { - game.override.moveset([ Moves.SUPER_FANG ]); - game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); - - await game.classicMode.startBattle([ Species.MAGIKARP ]); - - const leadPokemon = game.scene.getPlayerPokemon()!; - const enemyPokemon = game.scene.getEnemyPokemon()!; - - game.move.select(Moves.SUPER_FANG); - await game.move.forceHit(); - - await game.phaseInterceptor.to("DamagePhase"); - - expect(leadPokemon.turnData.hitCount).toBe(2); - - await game.phaseInterceptor.to("MoveEndPhase", false); - - expect(enemyPokemon.hp).toBe(Math.ceil(enemyPokemon.getMaxHp() * 0.25)); - } - ); - - it( - "Seismic Toss boosted by this ability and Multi-Lens should strike twice", + "Seismic Toss boosted by this ability and Multi-Lens should strike 3 times", async () => { game.override.moveset([ Moves.SEISMIC_TOSS ]); game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); @@ -333,11 +309,11 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("DamagePhase"); - expect(leadPokemon.turnData.hitCount).toBe(2); + expect(leadPokemon.turnData.hitCount).toBe(3); await game.phaseInterceptor.to("MoveEndPhase", false); - expect(enemyPokemon.hp).toBe(enemyStartingHp - 200); + expect(enemyPokemon.hp).toBe(enemyStartingHp - 300); } ); @@ -494,30 +470,4 @@ describe("Abilities - Parental Bond", () => { expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(1); } ); - - it( - "should not apply to multi-target moves with Multi-Lens", - async () => { - game.override.battleType("double"); - game.override.moveset([ Moves.EARTHQUAKE, Moves.SPLASH ]); - game.override.passiveAbility(Abilities.LEVITATE); - game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); - - await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); - - const enemyPokemon = game.scene.getEnemyField(); - - const enemyStartingHp = enemyPokemon.map(p => p.hp); - - game.move.select(Moves.EARTHQUAKE); - game.move.select(Moves.SPLASH, 1); - - await game.phaseInterceptor.to("DamagePhase"); - const enemyFirstHitDamage = enemyStartingHp.map((hp, i) => hp - enemyPokemon[i].hp); - - await game.phaseInterceptor.to("BerryPhase", false); - - enemyPokemon.forEach((p, i) => expect(enemyStartingHp[i] - p.hp).toBe(2 * enemyFirstHitDamage[i])); - } - ); }); diff --git a/src/test/items/multi_lens.test.ts b/src/test/items/multi_lens.test.ts new file mode 100644 index 00000000000..e4e4ab9863e --- /dev/null +++ b/src/test/items/multi_lens.test.ts @@ -0,0 +1,98 @@ +import { BattlerIndex } from "#app/battle"; +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, vi } from "vitest"; + +describe("Items - Multi Lens", () => { + 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.TACKLE, Moves.TRAILBLAZE, Moves.TACHYON_CUTTER ]) + .ability(Abilities.BALL_FETCH) + .startingHeldItems([{ name: "MULTI_LENS" }]) + .battleType("single") + .disableCrits() + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingLevel(100) + .enemyLevel(100); + }); + + it.each([ + { stackCount: 1, firstHitDamage: 0.75 }, + { stackCount: 2, firstHitDamage: 0.50 } + ])("$stackCount count: should deal {$firstHitDamage}x damage on the first hit, then hit $stackCount times for 0.25x", + async ({ stackCount, firstHitDamage }) => { + game.override.startingHeldItems([{ name: "MULTI_LENS", count: stackCount }]); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const spy = vi.spyOn(enemyPokemon, "getAttackDamage"); + vi.spyOn(enemyPokemon, "getBaseDamage").mockReturnValue(100); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to("MoveEndPhase"); + const damageResults = spy.mock.results.map(result => result.value?.damage); + + expect(damageResults).toHaveLength(1 + stackCount); + expect(damageResults[0]).toBe(firstHitDamage * 100); + damageResults.slice(1).forEach(dmg => expect(dmg).toBe(25)); + }); + + it("should stack additively with Parental Bond", async () => { + game.override.ability(Abilities.PARENTAL_BOND); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.turnData.hitCount).toBe(3); + }); + + it("should apply secondary effects on each hit", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.TRAILBLAZE); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(2); + }); + + it("should not enhance multi-hit moves", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.TACHYON_CUTTER); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(playerPokemon.turnData.hitCount).toBe(2); + }); +}); diff --git a/src/test/moves/beat_up.test.ts b/src/test/moves/beat_up.test.ts index a0129621f0e..41e5b63471f 100644 --- a/src/test/moves/beat_up.test.ts +++ b/src/test/moves/beat_up.test.ts @@ -74,29 +74,4 @@ describe("Moves - Beat Up", () => { expect(playerPokemon.turnData.hitCount).toBe(5); } ); - - it( - "should hit twice for each player Pokemon if the user has Multi-Lens", - async () => { - game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); - await game.startBattle([ Species.MAGIKARP, Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.PIKACHU, Species.EEVEE ]); - - const playerPokemon = game.scene.getPlayerPokemon()!; - const enemyPokemon = game.scene.getEnemyPokemon()!; - let enemyStartingHp = enemyPokemon.hp; - - game.move.select(Moves.BEAT_UP); - - await game.phaseInterceptor.to(MoveEffectPhase); - - expect(playerPokemon.turnData.hitCount).toBe(12); - expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); - - while (playerPokemon.turnData.hitsLeft > 0) { - enemyStartingHp = enemyPokemon.hp; - await game.phaseInterceptor.to(MoveEffectPhase); - expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); - } - } - ); }); diff --git a/src/test/moves/ceaseless_edge.test.ts b/src/test/moves/ceaseless_edge.test.ts index 88c8c8cf011..3fbbb7b0aaf 100644 --- a/src/test/moves/ceaseless_edge.test.ts +++ b/src/test/moves/ceaseless_edge.test.ts @@ -34,7 +34,7 @@ describe("Moves - Ceaseless Edge", () => { game.override.startingLevel(100); game.override.enemyLevel(100); game.override.moveset([ Moves.CEASELESS_EDGE, Moves.SPLASH, Moves.ROAR ]); - game.override.enemyMoveset([ Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH ]); + game.override.enemyMoveset(Moves.SPLASH); vi.spyOn(allMoves[Moves.CEASELESS_EDGE], "accuracy", "get").mockReturnValue(100); }); @@ -42,7 +42,7 @@ describe("Moves - Ceaseless Edge", () => { test( "move should hit and apply spikes", async () => { - await game.startBattle([ Species.ILLUMISE ]); + await game.classicMode.startBattle([ Species.ILLUMISE ]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -67,7 +67,7 @@ describe("Moves - Ceaseless Edge", () => { "move should hit twice with multi lens and apply two layers of spikes", async () => { game.override.startingHeldItems([{ name: "MULTI_LENS" }]); - await game.startBattle([ Species.ILLUMISE ]); + await game.classicMode.startBattle([ Species.ILLUMISE ]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -92,9 +92,9 @@ describe("Moves - Ceaseless Edge", () => { "trainer - move should hit twice, apply two layers of spikes, force switch opponent - opponent takes damage", async () => { game.override.startingHeldItems([{ name: "MULTI_LENS" }]); - game.override.startingWave(5); + game.override.startingWave(25); - await game.startBattle([ Species.ILLUMISE ]); + await game.classicMode.startBattle([ Species.ILLUMISE ]); game.move.select(Moves.CEASELESS_EDGE); await game.phaseInterceptor.to(MoveEffectPhase, false); @@ -102,7 +102,7 @@ describe("Moves - Ceaseless Edge", () => { const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; expect(tagBefore instanceof ArenaTrapTag).toBeFalsy(); - await game.phaseInterceptor.to(TurnEndPhase, false); + await game.toNextTurn(); const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; expect(tagAfter instanceof ArenaTrapTag).toBeTruthy(); expect(tagAfter.layers).toBe(2); diff --git a/src/test/moves/dragon_rage.test.ts b/src/test/moves/dragon_rage.test.ts index e8185f013e5..d5536ff9d2f 100644 --- a/src/test/moves/dragon_rage.test.ts +++ b/src/test/moves/dragon_rage.test.ts @@ -2,7 +2,6 @@ import { Stat } from "#enums/stat"; import { Type } from "#enums/type"; import { Species } from "#app/enums/species"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; -import { modifierTypes } from "#app/modifier/modifier-type"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -114,14 +113,4 @@ describe("Moves - Dragon Rage", () => { expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage); }); - - it("ignores multi hit", async () => { - game.override.disableCrits(); - game.scene.addModifier(modifierTypes.MULTI_LENS().newModifier(partyPokemon), false); - - game.move.select(Moves.DRAGON_RAGE); - await game.phaseInterceptor.to(TurnEndPhase); - - expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage); - }); }); diff --git a/src/test/moves/electro_shot.test.ts b/src/test/moves/electro_shot.test.ts index 1373b4941eb..283154b3408 100644 --- a/src/test/moves/electro_shot.test.ts +++ b/src/test/moves/electro_shot.test.ts @@ -98,7 +98,7 @@ describe("Moves - Electro Shot", () => { game.move.select(Moves.ELECTRO_SHOT); await game.phaseInterceptor.to("MoveEndPhase"); - expect(playerPokemon.turnData.hitCount).toBe(2); + expect(playerPokemon.turnData.hitCount).toBe(1); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); }); }); From 4802f512ff7925037ecb90bf4d19505d1831d2a4 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:22:27 -0500 Subject: [PATCH 64/98] [P1][Beta] Fix softlock when losing a run on local build (#4846) --- src/phases/game-over-phase.ts | 7 ++- src/test/phases/game-over-phase.test.ts | 77 +++++++++++++++++++++++++ src/test/utils/phaseInterceptor.ts | 24 +++++++- 3 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 src/test/phases/game-over-phase.test.ts diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 26a0c45f449..84a4a4e8ef9 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -125,10 +125,9 @@ export class GameOverPhase extends BattlePhase { } const clear = (endCardPhase?: EndCardPhase) => { - if (newClear) { - this.handleUnlocks(); - } if (this.isVictory && newClear) { + this.handleUnlocks(); + for (const species of this.firstRibbons) { this.scene.unshiftPhase(new RibbonModifierRewardPhase(this.scene, modifierTypes.VOUCHER_PLUS, species)); } @@ -183,6 +182,8 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.offlineNewClear(this.scene).then(result => { doGameOver(result); }); + } else { + doGameOver(false); } } diff --git a/src/test/phases/game-over-phase.test.ts b/src/test/phases/game-over-phase.test.ts new file mode 100644 index 00000000000..2e19d5fe954 --- /dev/null +++ b/src/test/phases/game-over-phase.test.ts @@ -0,0 +1,77 @@ +import { Biome } from "#enums/biome"; +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, vi } from "vitest"; +import { achvs } from "#app/system/achv"; +import { Unlockables } from "#app/system/unlockables"; + +describe("Game Over Phase", () => { + 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.MEMENTO, Moves.ICE_BEAM, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingWave(200) + .startingBiome(Biome.END) + .startingLevel(10000); + }); + + it("winning a run should give rewards", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR ]); + vi.spyOn(game.scene, "validateAchv"); + + // Note: `game.doKillOpponents()` does not properly handle final boss + // Final boss phase 1 + game.move.select(Moves.ICE_BEAM); + await game.toNextTurn(); + + // Final boss phase 2 + game.move.select(Moves.ICE_BEAM); + await game.phaseInterceptor.to("PostGameOverPhase", false); + + // The game refused to actually give the vouchers during tests, + // so the best we can do is to check that their reward phases occurred. + expect(game.phaseInterceptor.log.includes("GameOverPhase")).toBe(true); + expect(game.phaseInterceptor.log.includes("UnlockPhase")).toBe(true); + expect(game.phaseInterceptor.log.includes("RibbonModifierRewardPhase")).toBe(true); + expect(game.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]).toBe(true); + expect(game.scene.validateAchv).toHaveBeenCalledWith(achvs.CLASSIC_VICTORY); + expect(game.scene.gameData.achvUnlocks[achvs.CLASSIC_VICTORY.id]).toBeTruthy(); + }); + + it("losing a run should not give rewards", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR ]); + vi.spyOn(game.scene, "validateAchv"); + + game.move.select(Moves.MEMENTO); + await game.phaseInterceptor.to("PostGameOverPhase", false); + + expect(game.phaseInterceptor.log.includes("GameOverPhase")).toBe(true); + expect(game.phaseInterceptor.log.includes("UnlockPhase")).toBe(false); + expect(game.phaseInterceptor.log.includes("RibbonModifierRewardPhase")).toBe(false); + expect(game.phaseInterceptor.log.includes("GameOverModifierRewardPhase")).toBe(false); + expect(game.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]).toBe(false); + expect(game.scene.validateAchv).not.toHaveBeenCalledWith(achvs.CLASSIC_VICTORY); + expect(game.scene.gameData.achvUnlocks[achvs.CLASSIC_VICTORY.id]).toBeFalsy(); + }); +}); diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index 17b6c7a6c81..4029e5e168c 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -55,6 +55,11 @@ import { import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { PartyExpPhase } from "#app/phases/party-exp-phase"; import { ExpPhase } from "#app/phases/exp-phase"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { RibbonModifierRewardPhase } from "#app/phases/ribbon-modifier-reward-phase"; +import { GameOverModifierRewardPhase } from "#app/phases/game-over-modifier-reward-phase"; +import { UnlockPhase } from "#app/phases/unlock-phase"; +import { PostGameOverPhase } from "#app/phases/post-game-over-phase"; export interface PromptHandler { phaseTarget?: string; @@ -113,10 +118,15 @@ type PhaseClass = | typeof MysteryEncounterBattlePhase | typeof MysteryEncounterRewardsPhase | typeof PostMysteryEncounterPhase + | typeof RibbonModifierRewardPhase + | typeof GameOverModifierRewardPhase | typeof ModifierRewardPhase | typeof PartyExpPhase | typeof ExpPhase - | typeof EncounterPhase; + | typeof EncounterPhase + | typeof GameOverPhase + | typeof UnlockPhase + | typeof PostGameOverPhase; type PhaseString = | "LoginPhase" @@ -167,10 +177,15 @@ type PhaseString = | "MysteryEncounterBattlePhase" | "MysteryEncounterRewardsPhase" | "PostMysteryEncounterPhase" + | "RibbonModifierRewardPhase" + | "GameOverModifierRewardPhase" | "ModifierRewardPhase" | "PartyExpPhase" | "ExpPhase" - | "EncounterPhase"; + | "EncounterPhase" + | "GameOverPhase" + | "UnlockPhase" + | "PostGameOverPhase"; type PhaseInterceptorPhase = PhaseClass | PhaseString; @@ -245,10 +260,15 @@ export default class PhaseInterceptor { [ MysteryEncounterBattlePhase, this.startPhase ], [ MysteryEncounterRewardsPhase, this.startPhase ], [ PostMysteryEncounterPhase, this.startPhase ], + [ RibbonModifierRewardPhase, this.startPhase ], + [ GameOverModifierRewardPhase, this.startPhase ], [ ModifierRewardPhase, this.startPhase ], [ PartyExpPhase, this.startPhase ], [ ExpPhase, this.startPhase ], [ EncounterPhase, this.startPhase ], + [ GameOverPhase, this.startPhase ], + [ UnlockPhase, this.startPhase ], + [ PostGameOverPhase, this.startPhase ], ]; private endBySetMode = [ From 8e26db944d27ee031de891ea834d0c32c426d7a8 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:13:37 -0800 Subject: [PATCH 65/98] [Balance][Beta] Revert Spread Move Restriction on Multi-Lens (#4851) * Multi-Lens now applies to spread moves * Fix Multi-Lens applying to both damage and power --- src/data/ability.ts | 2 +- src/data/move.ts | 9 +++++---- src/modifier/modifier.ts | 4 ++-- src/test/items/multi_lens.test.ts | 19 +++++++++++++++++++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 4194be31405..d58c6c5c9b9 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1367,7 +1367,7 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr { const hitCount = args[0] as Utils.NumberHolder; const multiplier = args[1] as Utils.NumberHolder; - if (move.canBeMultiStrikeEnhanced(pokemon)) { + if (move.canBeMultiStrikeEnhanced(pokemon, true)) { this.showAbility = !!hitCount?.value; if (hitCount?.value) { hitCount.value += 1; diff --git a/src/data/move.ts b/src/data/move.ts index 0de9d9b53a2..98c679b923e 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -818,8 +818,6 @@ export default class Move implements Localizable { applyMoveAttrs(VariablePowerAttr, source, target, this, power); - source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, this.id, null, power); - if (!this.hasAttr(TypelessAttr)) { source.scene.arena.applyTags(WeakenMoveTypeTag, simulated, this.type, power); source.scene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, this.type, power); @@ -846,8 +844,11 @@ export default class Move implements Localizable { * by enhancing effects. * Currently used for {@link https://bulbapedia.bulbagarden.net/wiki/Parental_Bond_(Ability) | Parental Bond} * and {@linkcode PokemonMultiHitModifier | Multi-Lens}. + * @param user The {@linkcode Pokemon} using the move + * @param restrictSpread `true` if the enhancing effect + * should not affect multi-target moves (default `false`) */ - canBeMultiStrikeEnhanced(user: Pokemon): boolean { + canBeMultiStrikeEnhanced(user: Pokemon, restrictSpread: boolean = false): boolean { // Multi-strike enhancers... // ...cannot enhance moves that hit multiple targets @@ -870,7 +871,7 @@ export default class Move implements Localizable { Moves.ENDEAVOR ]; - return !isMultiTarget + return (!restrictSpread || !isMultiTarget) && !this.isChargingMove() && !exceptAttrs.some(attr => this.hasAttr(attr)) && !exceptMoves.some(id => this.id === id) diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 5e60d888072..7aa4b9308d1 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -2715,7 +2715,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { if (!isNullOrUndefined(count)) { return this.applyHitCountBoost(count); } else if (!isNullOrUndefined(damageMultiplier)) { - return this.applyPowerModifier(pokemon, damageMultiplier); + return this.applyDamageModifier(pokemon, damageMultiplier); } return false; @@ -2732,7 +2732,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { * equal to (1 - the number of stacked Multi-Lenses). * Additional strikes beyond that are given a 0.25x damage multiplier */ - private applyPowerModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean { + private applyDamageModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean { damageMultiplier.value = (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount) ? (1 - (0.25 * this.getStackCount())) : 0.25; diff --git a/src/test/items/multi_lens.test.ts b/src/test/items/multi_lens.test.ts index e4e4ab9863e..d389ca70555 100644 --- a/src/test/items/multi_lens.test.ts +++ b/src/test/items/multi_lens.test.ts @@ -95,4 +95,23 @@ describe("Items - Multi Lens", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(playerPokemon.turnData.hitCount).toBe(2); }); + + it("should enhance multi-target moves", async () => { + game.override + .battleType("double") + .moveset([ Moves.SWIFT, Moves.SPLASH ]); + + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + + const [ magikarp, ] = game.scene.getPlayerField(); + + game.move.select(Moves.SWIFT, 0); + game.move.select(Moves.SPLASH, 1); + + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); + + await game.phaseInterceptor.to("MoveEndPhase"); + + expect(magikarp.turnData.hitCount).toBe(2); + }); }); From 6f3fd0f138c554a21ea3f37a02e4457ae9219815 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Tue, 12 Nov 2024 06:29:37 -0500 Subject: [PATCH 66/98] [Beta][P3] Fix failed charge moves not displaying failed text (#4853) --- src/data/move.ts | 4 ++-- src/phases/move-phase.ts | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 98c679b923e..089bb51bf5e 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -668,12 +668,12 @@ export default class Move implements Localizable { } /** - * Sees if, given the target pokemon, a move fails on it (by looking at each {@linkcode MoveAttr} of this move + * Sees if a move has a custom failure text (by looking at each {@linkcode MoveAttr} of this move) * @param user {@linkcode Pokemon} using the move * @param target {@linkcode Pokemon} receiving the move * @param move {@linkcode Move} using the move * @param cancelled {@linkcode Utils.BooleanHolder} to hold boolean value - * @returns string of the failed text, or null + * @returns string of the custom failure text, or `null` if it uses the default text ("But it failed!") */ getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { for (const attr of this.attrs) { diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 378b72e1f56..005cdbe1716 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -378,10 +378,8 @@ export class MovePhase extends BattlePhase { this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual }); const failureMessage = move.getFailedText(this.pokemon, targets[0], move, new BooleanHolder(false)); - if (failureMessage) { - this.showMoveText(); - this.showFailedText(failureMessage); - } + this.showMoveText(); + this.showFailedText(failureMessage ?? undefined); // Remove the user from its semi-invulnerable state (if applicable) this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); From b6b756a1620c488557e9c6bf5e2b0990242e138e Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 12 Nov 2024 03:44:28 -0800 Subject: [PATCH 67/98] [P2] Fix issue with Pokemon not evolving until the next floor and clean up `LevelUpPhase` (#4854) --- src/phases/level-up-phase.ts | 47 +++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/phases/level-up-phase.ts b/src/phases/level-up-phase.ts index a2fa8a16533..4f26abc5af3 100644 --- a/src/phases/level-up-phase.ts +++ b/src/phases/level-up-phase.ts @@ -1,59 +1,66 @@ -import BattleScene from "#app/battle-scene"; +import type BattleScene from "#app/battle-scene"; import { ExpNotification } from "#app/enums/exp-notification"; -import { EvolutionPhase } from "#app/phases/evolution-phase"; -import { PlayerPokemon } from "#app/field/pokemon"; +import type { PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; +import { EvolutionPhase } from "#app/phases/evolution-phase"; +import { LearnMovePhase } from "#app/phases/learn-move-phase"; +import { PlayerPartyMemberPokemonPhase } from "#app/phases/player-party-member-pokemon-phase"; import { LevelAchv } from "#app/system/achv"; +import { NumberHolder } from "#app/utils"; import i18next from "i18next"; -import * as Utils from "#app/utils"; -import { PlayerPartyMemberPokemonPhase } from "./player-party-member-pokemon-phase"; -import { LearnMovePhase } from "./learn-move-phase"; export class LevelUpPhase extends PlayerPartyMemberPokemonPhase { - private lastLevel: integer; - private level: integer; + protected lastLevel: number; + protected level: number; + protected pokemon: PlayerPokemon = this.getPlayerPokemon(); - constructor(scene: BattleScene, partyMemberIndex: integer, lastLevel: integer, level: integer) { + constructor(scene: BattleScene, partyMemberIndex: number, lastLevel: number, level: number) { super(scene, partyMemberIndex); this.lastLevel = lastLevel; this.level = level; - this.scene = scene; } - start() { + public override start() { super.start(); if (this.level > this.scene.gameData.gameStats.highestLevel) { this.scene.gameData.gameStats.highestLevel = this.level; } - this.scene.validateAchvs(LevelAchv, new Utils.NumberHolder(this.level)); + this.scene.validateAchvs(LevelAchv, new NumberHolder(this.level)); - const pokemon = this.getPokemon(); - const prevStats = pokemon.stats.slice(0); - pokemon.calculateStats(); - pokemon.updateInfo(); + const prevStats = this.pokemon.stats.slice(0); + this.pokemon.calculateStats(); + this.pokemon.updateInfo(); if (this.scene.expParty === ExpNotification.DEFAULT) { this.scene.playSound("level_up_fanfare"); - this.scene.ui.showText(i18next.t("battle:levelUp", { pokemonName: getPokemonNameWithAffix(this.getPokemon()), level: this.level }), null, () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()), null, true); + this.scene.ui.showText( + i18next.t("battle:levelUp", { pokemonName: getPokemonNameWithAffix(this.pokemon), level: this.level }), + null, + () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false) + .then(() => this.end()), null, true); } else if (this.scene.expParty === ExpNotification.SKIP) { this.end(); } else { // we still want to display the stats if activated this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()); } + } + + public override end() { if (this.lastLevel < 100) { // this feels like an unnecessary optimization const levelMoves = this.getPokemon().getLevelMoves(this.lastLevel + 1); for (const lm of levelMoves) { this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, lm[1])); } } - if (!pokemon.pauseEvolutions) { - const evolution = pokemon.getEvolution(); + if (!this.pokemon.pauseEvolutions) { + const evolution = this.pokemon.getEvolution(); if (evolution) { - this.scene.unshiftPhase(new EvolutionPhase(this.scene, pokemon as PlayerPokemon, evolution, this.lastLevel)); + this.scene.unshiftPhase(new EvolutionPhase(this.scene, this.pokemon, evolution, this.lastLevel)); } } + return super.end(); } } From e45cb42f7ee86899e45d9aa40aa390b65251dd76 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:42:47 -0800 Subject: [PATCH 68/98] [Balance] Disable King's Rock for moves that can already flinch (#4860) --- src/phases/move-effect-phase.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 24a0b51da96..afc8dd0475d 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -26,6 +26,7 @@ import { applyMoveAttrs, AttackMove, DelayedAttackAttr, + FlinchAttr, HitsTagAttr, MissEffectAttr, MoveAttr, @@ -502,6 +503,10 @@ export class MoveEffectPhase extends PokemonPhase { */ protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void { return () => { + if (this.move.getMove().hasAttr(FlinchAttr)) { + return; + } + if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) { const flinched = new BooleanHolder(false); user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); From 162eea500dcfaa5e39b06481339a60ebfb2d0c78 Mon Sep 17 00:00:00 2001 From: muscode Date: Wed, 13 Nov 2024 00:28:22 -0600 Subject: [PATCH 69/98] Fixed wild form changes messages, and form-changed Cramorant crashing the game when both sides faint at the same time (#4859) --- src/phases/quiet-form-change-phase.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index c28cc28b592..6c84c0d1a8a 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -29,10 +29,14 @@ export class QuietFormChangePhase extends BattlePhase { const preName = getPokemonNameWithAffix(this.pokemon); - if (!this.pokemon.isOnField() || this.pokemon.getTag(SemiInvulnerableTag)) { - this.pokemon.changeForm(this.formChange).then(() => { - this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500); - }); + if (!this.pokemon.isOnField() || this.pokemon.getTag(SemiInvulnerableTag) || this.pokemon.isFainted()) { + if (this.pokemon.isPlayer() || this.pokemon.isActive()) { + this.pokemon.changeForm(this.formChange).then(() => { + this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500); + }); + } else { + this.end(); + } return; } From 0c521bbe0828746ace0dd7310340fb6340bee6e5 Mon Sep 17 00:00:00 2001 From: geeilhan <107366005+geeilhan@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:41:39 +0100 Subject: [PATCH 70/98] [Move] Implement Freeze Dry type-changed interactions (#4840) * Full implementation of freeze-dry including edge cases such as Normalize and Electrify plus tests * Update comments * renamed WaterSuperEffectTypeMultiplierAttr to FreezeDryAttr * Added test case for freeze dry during inverse battles * cleaned up code making it more general * Added some more documentation * implementing reviewed changes * used getMoveType() instead of move.type * added additional test cases to freeze dry * Revert "used getMoveType() instead of move.type" This reverts commit 03445dfab4db52b0dddbe7abf7d4b4dfa8b9c583. * added reviewed changes without changing public/locales --------- Co-authored-by: ga27lok --- src/data/move.ts | 40 ++++++-- src/test/moves/freeze_dry.test.ts | 163 +++++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 11 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 089bb51bf5e..74ac61af884 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4971,16 +4971,42 @@ export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTy } } -export class WaterSuperEffectTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr { +/** + * This class forces Freeze-Dry to be super effective against Water Type. + * It considers if target is Mono or Dual Type and calculates the new Multiplier accordingly. + * @see {@linkcode apply} + */ +export class FreezeDryAttr extends VariableMoveTypeMultiplierAttr { + /** + * If the target is Mono Type (Water only) then a 2x Multiplier is always forced. + * If target is Dual Type (containing Water) then only a 2x Multiplier is forced for the Water Type. + * + * Additionally Freeze-Dry's effectiveness against water is always forced during {@linkcode InverseBattleChallenge}. + * The multiplier is recalculated for the non-Water Type in case of Dual Type targets containing Water Type. + * + * @param user The {@linkcode Pokemon} applying the move + * @param target The {@linkcode Pokemon} targeted by the move + * @param move The move used by the user + * @param args `[0]` a {@linkcode Utils.NumberHolder | NumberHolder} containing a type effectiveness multiplier + * @returns `true` if super effectiveness on water type is forced; `false` otherwise + */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const multiplier = args[0] as Utils.NumberHolder; - if (target.isOfType(Type.WATER)) { - const effectivenessAgainstWater = new Utils.NumberHolder(getTypeDamageMultiplier(move.type, Type.WATER)); - applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstWater); - if (effectivenessAgainstWater.value !== 0) { - multiplier.value *= 2 / effectivenessAgainstWater.value; + if (target.isOfType(Type.WATER) && multiplier.value !== 0) { + const multipleTypes = (target.getTypes().length > 1); + + if (multipleTypes) { + const nonWaterType = target.getTypes().filter(type => type !== Type.WATER)[0]; + const effectivenessAgainstTarget = new Utils.NumberHolder(getTypeDamageMultiplier(user.getMoveType(move), nonWaterType)); + + applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstTarget); + + multiplier.value = effectivenessAgainstTarget.value * 2; return true; } + + multiplier.value = 2; + return true; } return false; @@ -9422,7 +9448,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FREEZE_DRY, Type.ICE, MoveCategory.SPECIAL, 70, 100, 20, 10, 0, 6) .attr(StatusEffectAttr, StatusEffect.FREEZE) - .attr(WaterSuperEffectTypeMultiplierAttr) + .attr(FreezeDryAttr) .edgeCase(), // This currently just multiplies the move's power instead of changing its effectiveness. It also doesn't account for abilities that modify type effectiveness such as tera shell. new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6) .soundBased() diff --git a/src/test/moves/freeze_dry.test.ts b/src/test/moves/freeze_dry.test.ts index f766ed41a82..8bc6717f435 100644 --- a/src/test/moves/freeze_dry.test.ts +++ b/src/test/moves/freeze_dry.test.ts @@ -2,6 +2,7 @@ import { BattlerIndex } from "#app/battle"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; +import { Challenges } from "#enums/challenges"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -97,8 +98,7 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); }); - // enable if this is ever fixed (lol) - it.todo("should deal 2x damage to water types under Normalize", async () => { + it("should deal 2x damage to water type under Normalize", async () => { game.override.ability(Abilities.NORMALIZE); await game.classicMode.startBattle(); @@ -112,8 +112,39 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); }); - // enable once Electrify is implemented (and the interaction is fixed, as above) - it.todo("should deal 2x damage to water types under Electrify", async () => { + it("should deal 0.25x damage to rock/steel type under Normalize", async () => { + game.override + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.SHIELDON); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25); + }); + + it("should deal 0x damage to water/ghost type under Normalize", async () => { + game.override + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.JELLICENT); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0); + }); + + it("should deal 2x damage to water type under Electrify", async () => { game.override.enemyMoveset([ Moves.ELECTRIFY ]); await game.classicMode.startBattle(); @@ -126,4 +157,128 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); }); + + it("should deal 4x damage to water/flying type under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.GYARADOS); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4); + }); + + it("should deal 0x damage to water/ground type under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.BARBOACH); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0); + }); + + it("should deal 0.25x damage to Grass/Dragon type under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.FLAPPLE); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25); + }); + + it("should deal 2x damage to Water type during inverse battle", async () => { + game.override + .moveset([ Moves.FREEZE_DRY ]) + .enemySpecies(Species.MAGIKARP); + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); + }); + + it("should deal 2x damage to Water type during inverse battle under Normalize", async () => { + game.override + .moveset([ Moves.FREEZE_DRY ]) + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.MAGIKARP); + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); + }); + + it("should deal 2x damage to Water type during inverse battle under Electrify", async () => { + game.override + .moveset([ Moves.FREEZE_DRY ]) + .enemySpecies(Species.MAGIKARP) + .enemyMoveset([ Moves.ELECTRIFY ]); + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); + }); + + it("should deal 1x damage to water/flying type during inverse battle under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.GYARADOS); + + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1); + }); }); From d0d9eb78da7beadb980f32a6c466f521f9e600cf Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:35:13 -0800 Subject: [PATCH 71/98] Set the IVs of default starters to 15. (#4861) Co-authored-by: frutescens --- src/data/challenge.ts | 2 +- src/system/game-data.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/challenge.ts b/src/data/challenge.ts index af6bbf5b00f..4301ea7b375 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -653,7 +653,7 @@ export class FreshStartChallenge extends Challenge { pokemon.shiny = false; // Not shiny pokemon.variant = 0; // Not shiny pokemon.formIndex = 0; // Froakie should be base form - pokemon.ivs = [ 10, 10, 10, 10, 10, 10 ]; // Default IVs of 10 for all stats + pokemon.ivs = [ 15, 15, 15, 15, 15, 15 ]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0) return true; } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 8f179ddb677..a9726ac0713 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1540,7 +1540,7 @@ export class GameData { entry.caughtAttr = defaultStarterAttr; entry.natureAttr = 1 << (defaultStarterNatures[ds] + 1); for (const i in entry.ivs) { - entry.ivs[i] = 10; + entry.ivs[i] = 15; } } From 640ac237412dd82863f7ef7e704e8406c2d3c3fe Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:40:01 -0500 Subject: [PATCH 72/98] [Dev] Add overrides for alternating between single and double battles (#4833) * [Dev] Add overrides for alternating between single and double battles * PR feedback * Add type `DoubleType` * Fixed Gastro Acid test using `game.override.battleType(null)` * Changed new type name to `SingleOrDoubleType` * `SingleOrDoubleType` is now `BattleStyle` * Update src/test/utils/helpers/overridesHelper.ts --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/battle-scene.ts | 34 ++++++++++++++---- src/overrides.ts | 15 +++++++- src/test/battle/double_battle.test.ts | 42 ++++++++++++++++++++++- src/test/moves/gastro_acid.test.ts | 2 +- src/test/utils/helpers/overridesHelper.ts | 9 ++--- 5 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c30ab2e2912..e659b588208 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1233,12 +1233,34 @@ export default class BattleScene extends SceneBase { newDouble = !!double; } - if (Overrides.BATTLE_TYPE_OVERRIDE === "double") { - newDouble = true; - } - /* Override battles into single only if not fighting with trainers */ - if (newBattleType !== BattleType.TRAINER && Overrides.BATTLE_TYPE_OVERRIDE === "single") { - newDouble = false; + if (!isNullOrUndefined(Overrides.BATTLE_TYPE_OVERRIDE)) { + let doubleOverrideForWave: "single" | "double" | null = null; + + switch (Overrides.BATTLE_TYPE_OVERRIDE) { + case "double": + doubleOverrideForWave = "double"; + break; + case "single": + doubleOverrideForWave = "single"; + break; + case "even-doubles": + doubleOverrideForWave = (newWaveIndex % 2) ? "single" : "double"; + break; + case "odd-doubles": + doubleOverrideForWave = (newWaveIndex % 2) ? "double" : "single"; + break; + } + + if (doubleOverrideForWave === "double") { + newDouble = true; + } + /** + * Override battles into single only if not fighting with trainers. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/1948 | GitHub Issue #1948} + */ + if (newBattleType !== BattleType.TRAINER && doubleOverrideForWave === "single") { + newDouble = false; + } } const lastBattle = this.currentBattle; diff --git a/src/overrides.ts b/src/overrides.ts index d7a8ee18f15..7b73cd47b03 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -47,7 +47,18 @@ class DefaultOverrides { /** a specific seed (default: a random string of 24 characters) */ readonly SEED_OVERRIDE: string = ""; readonly WEATHER_OVERRIDE: WeatherType = WeatherType.NONE; - readonly BATTLE_TYPE_OVERRIDE: "double" | "single" | null = null; + /** + * If `null`, ignore this override. + * + * If `"single"`, set every non-trainer battle to be a single battle. + * + * If `"double"`, set every battle (including trainer battles) to be a double battle. + * + * If `"even-doubles"`, follow the `"double"` rule on even wave numbers, and follow the `"single"` rule on odd wave numbers. + * + * If `"odd-doubles"`, follow the `"double"` rule on odd wave numbers, and follow the `"single"` rule on even wave numbers. + */ + readonly BATTLE_TYPE_OVERRIDE: BattleStyle | null = null; readonly STARTING_WAVE_OVERRIDE: number = 0; readonly STARTING_BIOME_OVERRIDE: Biome = Biome.TOWN; readonly ARENA_TINT_OVERRIDE: TimeOfDay | null = null; @@ -229,3 +240,5 @@ export default { ...defaultOverrides, ...overrides } satisfies InstanceType; + +export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles"; diff --git a/src/test/battle/double_battle.test.ts b/src/test/battle/double_battle.test.ts index 1fc24bfc027..b48f2a96a5b 100644 --- a/src/test/battle/double_battle.test.ts +++ b/src/test/battle/double_battle.test.ts @@ -1,4 +1,6 @@ import { Status } from "#app/data/status-effect"; +import { Abilities } from "#enums/abilities"; +import { GameModes, getGameMode } from "#app/game-mode"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { Moves } from "#enums/moves"; @@ -6,9 +8,11 @@ import { Species } from "#enums/species"; import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Double Battles", () => { + const DOUBLE_CHANCE = 8; // Normal chance of double battle is 1/8 + let phaserGame: Phaser.Game; let game: GameManager; @@ -56,4 +60,40 @@ describe("Double Battles", () => { await game.phaseInterceptor.to(TurnInitPhase); expect(game.scene.getPlayerField().filter(p => !p.isFainted())).toHaveLength(2); }, 20000); + + it("randomly chooses between single and double battles if there is no battle type override", async () => { + let rngSweepProgress = 0; // Will simulate RNG rolls by slowly increasing from 0 to 1 + let doubleCount = 0; + let singleCount = 0; + + vi.spyOn(Phaser.Math.RND, "realInRange").mockImplementation((min: number, max: number) => { + return rngSweepProgress * (max - min) + min; + }); + + game.override.enemyMoveset(Moves.SPLASH) + .moveset(Moves.SPLASH) + .enemyAbility(Abilities.BALL_FETCH) + .ability(Abilities.BALL_FETCH); + + // Play through endless, waves 1 to 9, counting number of double battles from waves 2 to 9 + await game.classicMode.startBattle([ Species.BULBASAUR ]); + game.scene.gameMode = getGameMode(GameModes.ENDLESS); + + for (let i = 0; i < DOUBLE_CHANCE; i++) { + rngSweepProgress = (i + 0.5) / DOUBLE_CHANCE; + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + + if (game.scene.getEnemyParty().length === 1) { + singleCount++; + } else if (game.scene.getEnemyParty().length === 2) { + doubleCount++; + } + } + + expect(doubleCount).toBe(1); + expect(singleCount).toBe(DOUBLE_CHANCE - 1); + }); }); diff --git a/src/test/moves/gastro_acid.test.ts b/src/test/moves/gastro_acid.test.ts index fdd75b90b13..ec9246c855c 100644 --- a/src/test/moves/gastro_acid.test.ts +++ b/src/test/moves/gastro_acid.test.ts @@ -62,7 +62,7 @@ describe("Moves - Gastro Acid", () => { }); it("fails if used on an enemy with an already-suppressed ability", async () => { - game.override.battleType(null); + game.override.battleType("single"); await game.startBattle(); diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index 02950d497ee..1c05f92a334 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -4,7 +4,7 @@ import { Abilities } from "#app/enums/abilities"; import * as GameMode from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode"; import { ModifierOverride } from "#app/modifier/modifier-type"; -import Overrides from "#app/overrides"; +import Overrides, { BattleStyle } from "#app/overrides"; import { Unlockables } from "#app/system/unlockables"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; @@ -238,13 +238,14 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the battle type (single or double) + * Override the battle type (e.g., single or double). + * @see {@linkcode Overrides.BATTLE_TYPE_OVERRIDE} * @param battleType battle type to set * @returns `this` */ - public battleType(battleType: "single" | "double" | null): this { + public battleType(battleType: BattleStyle | null): this { vi.spyOn(Overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue(battleType); - this.log(`Battle type set to ${battleType} only!`); + this.log(battleType === null ? "Battle type override disabled!" : `Battle type set to ${battleType}!`); return this; } From 58912db8f1044a8ea5fa23a6f4989bfdc1cc426f Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:16:05 -0800 Subject: [PATCH 73/98] [P2] Telekinesis now sets FloatingTag to 3 turns instead of 5 turns (#4869) Co-authored-by: frutescens --- src/data/battler-tags.ts | 6 +++--- src/data/move.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 25d65fbc372..5c6d9d66b7c 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1809,8 +1809,8 @@ export class TypeImmuneTag extends BattlerTag { * @see {@link https://bulbapedia.bulbagarden.net/wiki/Telekinesis_(move) | Moves.TELEKINESIS} */ export class FloatingTag extends TypeImmuneTag { - constructor(tagType: BattlerTagType, sourceMove: Moves) { - super(tagType, sourceMove, Type.GROUND, 5); + constructor(tagType: BattlerTagType, sourceMove: Moves, turnCount: number) { + super(tagType, sourceMove, Type.GROUND, turnCount); } onAdd(pokemon: Pokemon): void { @@ -3053,7 +3053,7 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source case BattlerTagType.CHARGED: return new TypeBoostTag(tagType, sourceMove, Type.ELECTRIC, 2, true); case BattlerTagType.FLOATING: - return new FloatingTag(tagType, sourceMove); + return new FloatingTag(tagType, sourceMove, turnCount); case BattlerTagType.MINIMIZED: return new MinimizeTag(); case BattlerTagType.DESTINY_BOND: diff --git a/src/data/move.ts b/src/data/move.ts index 74ac61af884..a288c0e9618 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8897,7 +8897,7 @@ export function initMoves() { new SelfStatusMove(Moves.AQUA_RING, Type.WATER, -1, 20, -1, 0, 4) .attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true), new SelfStatusMove(Moves.MAGNET_RISE, Type.ELECTRIC, -1, 10, -1, 0, 4) - .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true) + .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5) .condition((user, target, move) => !user.scene.arena.getTag(ArenaTagType.GRAVITY) && [ BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING, BattlerTagType.INGRAIN ].every((tag) => !user.getTag(tag))), new AttackMove(Moves.FLARE_BLITZ, Type.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4) .attr(RecoilAttr, false, 0.33) From f778bd587700c5def856d038a51b1b25fc42da90 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Fri, 15 Nov 2024 00:01:04 +0100 Subject: [PATCH 74/98] Prevent crash from null dexData attributes (#4871) --- src/system/game-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index a9726ac0713..4d01ae9998a 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -679,7 +679,7 @@ export class GameData { return ret; } - return k.endsWith("Attr") && ![ "natureAttr", "abilityAttr", "passiveAttr" ].includes(k) ? BigInt(v) : v; + return k.endsWith("Attr") && ![ "natureAttr", "abilityAttr", "passiveAttr" ].includes(k) ? BigInt(v ?? 0) : v; }) as SystemSaveData; } From b1138c1d70a9428e767539ff64c858bf5dc80f36 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Fri, 15 Nov 2024 00:07:19 -0500 Subject: [PATCH 75/98] [P2][Beta] Freeze-dry Re-implementation (#4874) --- src/data/ability.ts | 4 +- src/data/move.ts | 82 ++++++++++++++----------------- src/field/pokemon.ts | 10 ++-- src/test/moves/freeze_dry.test.ts | 68 ++++++++++++++++++++++++- 4 files changed, 113 insertions(+), 51 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index d58c6c5c9b9..b77b18947be 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -516,7 +516,7 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr { applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { const modifierValue = args.length > 0 ? (args[0] as Utils.NumberHolder).value - : pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker); + : pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker, undefined, undefined, move); if (move instanceof AttackMove && modifierValue < 2) { cancelled.value = true; // Suppresses "No Effect" message @@ -3180,7 +3180,7 @@ function getAnticipationCondition(): AbAttrCondition { continue; } // the move's base type (not accounting for variable type changes) is super effective - if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true) >= 2) { + if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true, undefined, move.getMove()) >= 2) { return true; } // move is a OHKO diff --git a/src/data/move.ts b/src/data/move.ts index a288c0e9618..e7cd9d10615 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -897,7 +897,7 @@ export class AttackMove extends Move { let attackScore = 0; - const effectiveness = target.getAttackTypeEffectiveness(this.type, user); + const effectiveness = target.getAttackTypeEffectiveness(this.type, user, undefined, undefined, this); attackScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2; if (attackScore) { if (this.category === MoveCategory.PHYSICAL) { @@ -4971,48 +4971,6 @@ export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTy } } -/** - * This class forces Freeze-Dry to be super effective against Water Type. - * It considers if target is Mono or Dual Type and calculates the new Multiplier accordingly. - * @see {@linkcode apply} - */ -export class FreezeDryAttr extends VariableMoveTypeMultiplierAttr { - /** - * If the target is Mono Type (Water only) then a 2x Multiplier is always forced. - * If target is Dual Type (containing Water) then only a 2x Multiplier is forced for the Water Type. - * - * Additionally Freeze-Dry's effectiveness against water is always forced during {@linkcode InverseBattleChallenge}. - * The multiplier is recalculated for the non-Water Type in case of Dual Type targets containing Water Type. - * - * @param user The {@linkcode Pokemon} applying the move - * @param target The {@linkcode Pokemon} targeted by the move - * @param move The move used by the user - * @param args `[0]` a {@linkcode Utils.NumberHolder | NumberHolder} containing a type effectiveness multiplier - * @returns `true` if super effectiveness on water type is forced; `false` otherwise - */ - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const multiplier = args[0] as Utils.NumberHolder; - if (target.isOfType(Type.WATER) && multiplier.value !== 0) { - const multipleTypes = (target.getTypes().length > 1); - - if (multipleTypes) { - const nonWaterType = target.getTypes().filter(type => type !== Type.WATER)[0]; - const effectivenessAgainstTarget = new Utils.NumberHolder(getTypeDamageMultiplier(user.getMoveType(move), nonWaterType)); - - applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstTarget); - - multiplier.value = effectivenessAgainstTarget.value * 2; - return true; - } - - multiplier.value = 2; - return true; - } - - return false; - } -} - export class IceNoEffectTypeAttr extends VariableMoveTypeMultiplierAttr { /** * Checks to see if the Target is Ice-Type or not. If so, the move will have no effect. @@ -5040,6 +4998,41 @@ export class FlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr { } } +/** + * Attribute for moves which have a custom type chart interaction. + */ +export class VariableMoveTypeChartAttr extends MoveAttr { + /** + * @param user {@linkcode Pokemon} using the move + * @param target {@linkcode Pokemon} target of the move + * @param move {@linkcode Move} with this attribute + * @param args [0] {@linkcode NumberHolder} holding the type effectiveness + * @param args [1] A single defensive type of the target + * + * @returns true if application of the attribute succeeds + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + return false; + } +} + +/** + * This class forces Freeze-Dry to be super effective against Water Type. + */ +export class FreezeDryAttr extends VariableMoveTypeChartAttr { + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + const multiplier = args[0] as Utils.NumberHolder; + const defType = args[1] as Type; + + if (defType === Type.WATER) { + multiplier.value = 2; + return true; + } else { + return false; + } + } +} + export class OneHitKOAccuracyAttr extends VariableAccuracyAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const accuracy = args[0] as Utils.NumberHolder; @@ -9448,8 +9441,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FREEZE_DRY, Type.ICE, MoveCategory.SPECIAL, 70, 100, 20, 10, 0, 6) .attr(StatusEffectAttr, StatusEffect.FREEZE) - .attr(FreezeDryAttr) - .edgeCase(), // This currently just multiplies the move's power instead of changing its effectiveness. It also doesn't account for abilities that modify type effectiveness such as tera shell. + .attr(FreezeDryAttr), new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5d912f7d6e6..bc7e844a290 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "#app/battle-scene"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; -import Move, { 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 } from "#app/data/move"; +import Move, { 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 } from "#app/data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { starterPassiveAbilities } from "#app/data/balance/passives"; @@ -1632,7 +1632,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const moveType = source.getMoveType(move); const typeMultiplier = new Utils.NumberHolder((move.category !== MoveCategory.STATUS || move.hasAttr(RespectAttackTypeImmunityAttr)) - ? this.getAttackTypeEffectiveness(moveType, source, false, simulated) + ? this.getAttackTypeEffectiveness(moveType, source, false, simulated, move) : 1); applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier); @@ -1684,9 +1684,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param source {@linkcode Pokemon} the Pokemon using the move * @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks) * @param simulated tag to only apply the strong winds effect message when the move is used + * @param move (optional) the move whose type effectiveness is to be checked. Used for applying {@linkcode VariableMoveTypeChartAttr} * @returns a multiplier for the type effectiveness */ - getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true): TypeDamageMultiplier { + getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true, move?: Move): TypeDamageMultiplier { if (moveType === Type.STELLAR) { return this.isTerastallized() ? 2 : 1; } @@ -1705,6 +1706,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { let multiplier = types.map(defType => { const multiplier = new Utils.NumberHolder(getTypeDamageMultiplier(moveType, defType)); applyChallenges(this.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, multiplier); + if (move) { + applyMoveAttrs(VariableMoveTypeChartAttr, null, this, move, multiplier, defType); + } if (source) { const ignoreImmunity = new Utils.BooleanHolder(false); if (source.isActive(true) && source.hasAbilityWithAttr(IgnoreTypeImmunityAbAttr)) { diff --git a/src/test/moves/freeze_dry.test.ts b/src/test/moves/freeze_dry.test.ts index 8bc6717f435..9206a103a35 100644 --- a/src/test/moves/freeze_dry.test.ts +++ b/src/test/moves/freeze_dry.test.ts @@ -2,6 +2,7 @@ import { BattlerIndex } from "#app/battle"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; +import { Type } from "#enums/type"; import { Challenges } from "#enums/challenges"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; @@ -29,7 +30,7 @@ describe("Moves - Freeze-Dry", () => { .enemyMoveset(Moves.SPLASH) .starterSpecies(Species.FEEBAS) .ability(Abilities.BALL_FETCH) - .moveset([ Moves.FREEZE_DRY ]); + .moveset([ Moves.FREEZE_DRY, Moves.FORESTS_CURSE, Moves.SOAK ]); }); it("should deal 2x damage to pure water types", async () => { @@ -98,6 +99,71 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); }); + it("should deal 8x damage to water/ground/grass type under Forest's Curse", async () => { + game.override.enemySpecies(Species.QUAGSIRE); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FORESTS_CURSE); + await game.toNextTurn(); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(8); + }); + + it("should deal 2x damage to steel type terastallized into water", async () => { + game.override.enemySpecies(Species.SKARMORY) + .enemyHeldItems([{ name: "TERA_SHARD", type: Type.WATER }]); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); + }); + + it("should deal 0.5x damage to water type terastallized into fire", async () => { + game.override.enemySpecies(Species.PELIPPER) + .enemyHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }]); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5); + }); + + it("should deal 0.5x damage to water type Terapagos with Tera Shell", async () => { + game.override.enemySpecies(Species.TERAPAGOS) + .enemyAbility(Abilities.TERA_SHELL); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.SOAK); + await game.toNextTurn(); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5); + }); + it("should deal 2x damage to water type under Normalize", async () => { game.override.ability(Abilities.NORMALIZE); await game.classicMode.startBattle(); From 6dec84e39c0e1229160af14dddb0b9df6aeedfcb Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:13:23 -0800 Subject: [PATCH 76/98] [Balance] No more double 50x bosses in Endless + cleaning up RNG (#4862) * Added double battle exclusion to Endless bosses. * Brought Endure token RNG in line with game RNG formatting. * Corrected incorrect modulo condition in isEndlessBoss * Moved new doubles conditional to be above overrides. * Fixed bad RNG calls for Covet and Thief too. * Updated unburden test. * Revert "Updated unburden test." This reverts commit 01788d40c2f5c32d89bd0cb899bfc67fc77f881e. * Revert "Fixed bad RNG calls for Covet and Thief too." This reverts commit c7fcfd195de7e98c1e582ec67c829e003663cad0. --------- Co-authored-by: frutescens --- src/battle-scene.ts | 5 +++++ src/game-mode.ts | 2 +- src/modifier/modifier.ts | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index e659b588208..061fc6e28f2 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1233,6 +1233,11 @@ export default class BattleScene extends SceneBase { newDouble = !!double; } + // Disable double battles on Endless/Endless Spliced Wave 50x boss battles (Introduced 1.2.0) + if (this.gameMode.isEndlessBoss(newWaveIndex)) { + newDouble = false; + } + if (!isNullOrUndefined(Overrides.BATTLE_TYPE_OVERRIDE)) { let doubleOverrideForWave: "single" | "double" | null = null; diff --git a/src/game-mode.ts b/src/game-mode.ts index 8f1bb9356e6..0f997bf651e 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -236,7 +236,7 @@ export class GameMode implements GameModeConfig { * @returns true if waveIndex is a multiple of 50 in Endless */ isEndlessBoss(waveIndex: integer): boolean { - return !!(waveIndex % 50) && + return waveIndex % 50 === 0 && (this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS); } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 7aa4b9308d1..ac8dc556b98 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -3631,7 +3631,7 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { super(type, stackCount || 10); //Hardcode temporarily - this.chance = .02; + this.chance = 2; } match(modifier: Modifier) { @@ -3639,11 +3639,11 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { } clone() { - return new EnemyEndureChanceModifier(this.type, this.chance * 100, this.stackCount); + return new EnemyEndureChanceModifier(this.type, this.chance, this.stackCount); } getArgs(): any[] { - return [ this.chance * 100 ]; + return [ this.chance ]; } /** @@ -3652,7 +3652,7 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { * @returns `true` if {@linkcode Pokemon} endured */ override apply(target: Pokemon): boolean { - if (target.battleData.endured || Phaser.Math.RND.realInRange(0, 1) >= (this.chance * this.getStackCount())) { + if (target.battleData.endured || target.randSeedInt(100) >= (this.chance * this.getStackCount())) { return false; } From 8326e3556b90c95379bf7c5c95ed31c33485d2cc Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:29:52 -0800 Subject: [PATCH 77/98] Remove `.edgeCase()` from fully implemented moves (#4876) This includes Sunsteel Strike, Moongeist Beam and Photon Geyser --- src/data/move.ts | 9 ++-- src/test/moves/moongeist_beam.test.ts | 59 +++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 src/test/moves/moongeist_beam.test.ts diff --git a/src/data/move.ts b/src/data/move.ts index e7cd9d10615..ae2ba919b52 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -9876,11 +9876,9 @@ export function initMoves() { .ignoresSubstitute() .partial(), // Does not steal stats new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7) - .ignoresAbilities() - .edgeCase(), // Should not ignore abilities when called virtually (metronome) + .ignoresAbilities(), new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) - .ignoresAbilities() - .edgeCase(), // Should not ignore abilities when called virtually (metronome) + .ignoresAbilities(), new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1), new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7) @@ -9903,8 +9901,7 @@ export function initMoves() { .punchingMove(), new AttackMove(Moves.PHOTON_GEYSER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) .attr(PhotonGeyserCategoryAttr) - .ignoresAbilities() - .edgeCase(), // Should not ignore abilities when called virtually (metronome) + .ignoresAbilities(), /* Unused */ new AttackMove(Moves.LIGHT_THAT_BURNS_THE_SKY, Type.PSYCHIC, MoveCategory.SPECIAL, 200, -1, 1, -1, 0, 7) .attr(PhotonGeyserCategoryAttr) diff --git a/src/test/moves/moongeist_beam.test.ts b/src/test/moves/moongeist_beam.test.ts new file mode 100644 index 00000000000..216eee482fb --- /dev/null +++ b/src/test/moves/moongeist_beam.test.ts @@ -0,0 +1,59 @@ +import { allMoves, RandomMoveAttr } from "#app/data/move"; +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, vi } from "vitest"; + +describe("Moves - Moongeist Beam", () => { + 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.MOONGEIST_BEAM, Moves.METRONOME ]) + .ability(Abilities.BALL_FETCH) + .startingLevel(200) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.STURDY) + .enemyMoveset(Moves.SPLASH); + }); + + // Also covers Photon Geyser and Sunsteel Strike + it("should ignore enemy abilities", async () => { + await game.classicMode.startBattle([ Species.MILOTIC ]); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.MOONGEIST_BEAM); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.isFainted()).toBe(true); + }); + + // Also covers Photon Geyser and Sunsteel Strike + it("should not ignore enemy abilities when called by another move, such as metronome", async () => { + await game.classicMode.startBattle([ Species.MILOTIC ]); + vi.spyOn(allMoves[Moves.METRONOME].getAttrs(RandomMoveAttr)[0], "getMoveOverride").mockReturnValue(Moves.MOONGEIST_BEAM); + + game.move.select(Moves.METRONOME); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.isFainted()).toBe(false); + expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].move).toBe(Moves.MOONGEIST_BEAM); + }); +}); From 9273b4930d929cd9c8fb67466ea2501e8ffb8bf4 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:56:05 -0800 Subject: [PATCH 78/98] [Beta][P1] Fix crash when resetting Commanded Dondozo before Trainer battles (#4873) * Add failsafe to Commander remove anim * Commanded tag saves Tatsu form on reload --- src/data/battler-tags.ts | 5 +++++ src/phases/pokemon-anim-phase.ts | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 5c6d9d66b7c..28ab5ff2a4f 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2158,6 +2158,11 @@ export class CommandedTag extends BattlerTag { pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_REMOVE); } } + + override loadTag(source: BattlerTag | any): void { + super.loadTag(source); + this._tatsugiriFormKey = source._tatsugiriFormKey; + } } /** diff --git a/src/phases/pokemon-anim-phase.ts b/src/phases/pokemon-anim-phase.ts index ad0be34af7d..eb5431cbc56 100644 --- a/src/phases/pokemon-anim-phase.ts +++ b/src/phases/pokemon-anim-phase.ts @@ -312,6 +312,10 @@ export class PokemonAnimPhase extends BattlePhase { // Note: unlike the other Commander animation, this is played through the // Dondozo instead of the Tatsugiri. const tatsugiri = this.pokemon.getAlly(); + if (isNullOrUndefined(tatsugiri)) { + console.warn("Aborting COMMANDER_REMOVE anim: Tatsugiri is undefined"); + return this.end(); + } const tatsuSprite = this.scene.addPokemonSprite( tatsugiri, From ef7d860166136e126973a69bd80450dfd92c1c25 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:57:02 -0500 Subject: [PATCH 79/98] [Balance] Remove from trainers: Pika/Eevee forms before 30, BB Greninja, Rival starter HA (#4863) * Remove Pika/Eevee forms from Trainers before wave 30, and BB Gren * Fix `egg` test * Ban hidden ability from Rival starter --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/battle-scene.ts | 9 +++++++++ src/data/trainer-config.ts | 18 +++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 061fc6e28f2..2f062667808 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1424,10 +1424,19 @@ export default class BattleScene extends SceneBase { case Species.PALDEA_TAUROS: return Utils.randSeedInt(species.forms.length); case Species.PIKACHU: + if (this.currentBattle?.battleType === BattleType.TRAINER && this.currentBattle?.waveIndex < 30) { + return 0; // Ban Cosplay and Partner Pika from Trainers before wave 30 + } return Utils.randSeedInt(8); case Species.EEVEE: + if (this.currentBattle?.battleType === BattleType.TRAINER && this.currentBattle?.waveIndex < 30) { + return 0; // No Partner Eevee for Wave 12 Preschoolers + } return Utils.randSeedInt(2); case Species.GRENINJA: + if (this.currentBattle?.battleType === BattleType.TRAINER) { + return 0; // Don't give trainers Battle Bond Greninja + } return Utils.randSeedInt(2); case Species.ZYGARDE: return Utils.randSeedInt(4); diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index d82d568ecc6..5e5f38bd00d 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -1841,21 +1841,25 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.RIVAL]: new TrainerConfig((t = TrainerType.RIVAL)).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL) .setModifierRewardFuncs(() => modifierTypes.SUPER_EXP_CHARM, () => modifierTypes.EXP_SHARE) .setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM, () => modifierTypes.ABILITY_CHARM) - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, Species.TREECKO, Species.TORCHIC, Species.MUDKIP, Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, Species.SNIVY, Species.TEPIG, Species.OSHAWOTT, Species.CHESPIN, Species.FENNEKIN, Species.FROAKIE, Species.ROWLET, Species.LITTEN, Species.POPPLIO, Species.GROOKEY, Species.SCORBUNNY, Species.SOBBLE, Species.SPRIGATITO, Species.FUECOCO, Species.QUAXLY ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, Species.TREECKO, Species.TORCHIC, Species.MUDKIP, Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, Species.SNIVY, Species.TEPIG, Species.OSHAWOTT, Species.CHESPIN, Species.FENNEKIN, Species.FROAKIE, Species.ROWLET, Species.LITTEN, Species.POPPLIO, Species.GROOKEY, Species.SCORBUNNY, Species.SOBBLE, Species.SPRIGATITO, Species.FUECOCO, Species.QUAXLY ], TrainerSlot.TRAINER, true, + (p => p.abilityIndex = 0))) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEY, Species.HOOTHOOT, Species.TAILLOW, Species.STARLY, Species.PIDOVE, Species.FLETCHLING, Species.PIKIPEK, Species.ROOKIDEE, Species.WATTREL ], TrainerSlot.TRAINER, true)), [TrainerType.RIVAL_2]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_2) .setModifierRewardFuncs(() => modifierTypes.EXP_SHARE) .setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM) - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.IVYSAUR, Species.CHARMELEON, Species.WARTORTLE, Species.BAYLEEF, Species.QUILAVA, Species.CROCONAW, Species.GROVYLE, Species.COMBUSKEN, Species.MARSHTOMP, Species.GROTLE, Species.MONFERNO, Species.PRINPLUP, Species.SERVINE, Species.PIGNITE, Species.DEWOTT, Species.QUILLADIN, Species.BRAIXEN, Species.FROGADIER, Species.DARTRIX, Species.TORRACAT, Species.BRIONNE, Species.THWACKEY, Species.RABOOT, Species.DRIZZILE, Species.FLORAGATO, Species.CROCALOR, Species.QUAXWELL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.IVYSAUR, Species.CHARMELEON, Species.WARTORTLE, Species.BAYLEEF, Species.QUILAVA, Species.CROCONAW, Species.GROVYLE, Species.COMBUSKEN, Species.MARSHTOMP, Species.GROTLE, Species.MONFERNO, Species.PRINPLUP, Species.SERVINE, Species.PIGNITE, Species.DEWOTT, Species.QUILLADIN, Species.BRAIXEN, Species.FROGADIER, Species.DARTRIX, Species.TORRACAT, Species.BRIONNE, Species.THWACKEY, Species.RABOOT, Species.DRIZZILE, Species.FLORAGATO, Species.CROCALOR, Species.QUAXWELL ], TrainerSlot.TRAINER, true, + (p => p.abilityIndex = 0))) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOTTO, Species.HOOTHOOT, Species.TAILLOW, Species.STARAVIA, Species.TRANQUILL, Species.FLETCHINDER, Species.TRUMBEAK, Species.CORVISQUIRE, Species.WATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)), [TrainerType.RIVAL_3]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.5).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_3) - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, + (p => p.abilityIndex = 0))) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setSpeciesFilter(species => species.baseTotal >= 540), [TrainerType.RIVAL_4]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(1.75).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_2").setMixedBattleBgm("battle_rival_2").setPartyTemplates(trainerPartyTemplates.RIVAL_4) - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, + (p => p.abilityIndex = 0))) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setSpeciesFilter(species => species.baseTotal >= 540) @@ -1865,7 +1869,10 @@ export const trainerConfigs: TrainerConfigs = { }), [TrainerType.RIVAL_5]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_3").setMixedBattleBgm("battle_rival_3").setPartyTemplates(trainerPartyTemplates.RIVAL_5) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, - p => p.setBoss(true, 2))) + p => { + p.setBoss(true, 2); + p.abilityIndex = 0; + })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setSpeciesFilter(species => species.baseTotal >= 540) @@ -1883,6 +1890,7 @@ export const trainerConfigs: TrainerConfigs = { .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 3); + p.abilityIndex = 0; p.generateAndPopulateMoveset(); })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true, From 5ca1fd5cfd8cc8f98c9cfc939a484cea6399a57d Mon Sep 17 00:00:00 2001 From: pom-eranian Date: Fri, 15 Nov 2024 11:58:50 -0500 Subject: [PATCH 80/98] [Sprite] Set default fps to 10 instead of 12 on pokemon animations (#4842) * Set default fps to 10 instead of 12 for pokemon sprites * [Sprite] Set pokemon animation framerate to 10 where assigned --- src/data/pokemon-species.ts | 4 ++-- src/field/mystery-encounter-intro.ts | 2 +- src/field/pokemon.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 203e545503a..ec104d4d4aa 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -505,11 +505,11 @@ export abstract class PokemonSpeciesForm { scene.anims.create({ key: this.getSpriteKey(female, formIndex, shiny, variant), frames: frameNames, - frameRate: 12, + frameRate: 10, repeat: -1 }); } else { - scene.anims.get(spriteKey).frameRate = 12; + scene.anims.get(spriteKey).frameRate = 10; } let spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, ""); const useExpSprite = scene.experimentalSprites && scene.hasExpSprite(spriteKey); diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 12bcace500c..1577d1157d7 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -212,7 +212,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con this.scene.anims.create({ key: config.spriteKey, frames: frameNames, - frameRate: 12, + frameRate: 10, repeat: -1 }); } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index bc7e844a290..30d1aceea4b 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -427,7 +427,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.anims.create({ key: this.getBattleSpriteKey(), frames: battleFrameNames, - frameRate: 12, + frameRate: 10, repeat: -1 }); } @@ -3612,7 +3612,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } this.status = null; if (lastStatus === StatusEffect.SLEEP) { - this.setFrameRate(12); + this.setFrameRate(10); if (this.getTag(BattlerTagType.NIGHTMARE)) { this.lapseTag(BattlerTagType.NIGHTMARE); } From eb3c0d731a38178722636391f175002c9b467f5d Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:17:46 -0800 Subject: [PATCH 81/98] [P2] Lunar Blessing / Jungle Healing now heal Freeze (#4877) * Added Freeze to statuses healed by Jungle Healing / Lunar Blessing * Fixed up documentation. --------- Co-authored-by: frutescens --- src/data/move.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index ae2ba919b52..2ac4d74b712 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2589,12 +2589,11 @@ export class HealStatusEffectAttr extends MoveEffectAttr { /** * @param selfTarget - Whether this move targets the user - * @param ...effects - List of status effects to cure + * @param effects - status effect or list of status effects to cure */ - constructor(selfTarget: boolean, ...effects: StatusEffect[]) { + constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) { super(selfTarget, { lastHitOnly: true }); - - this.effects = effects; + this.effects = [ effects ].flat(1); } /** @@ -8583,7 +8582,7 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false) .target(MoveTarget.ENEMY_SIDE), new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3) - .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN) + .attr(HealStatusEffectAttr, true, [ StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN ]) .condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)), new SelfStatusMove(Moves.GRUDGE, Type.GHOST, -1, 5, -1, 0, 3) .attr(AddBattlerTagAttr, BattlerTagType.GRUDGE, true, undefined, 1), @@ -9792,7 +9791,7 @@ export function initMoves() { .condition( (user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct? .attr(HealAttr, 0.5) - .attr(HealStatusEffectAttr, false, ...getNonVolatileStatusEffects()) + .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .triageMove(), new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7) .danceMove() @@ -10216,7 +10215,7 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.BURN), new StatusMove(Moves.JUNGLE_HEALING, Type.GRASS, -1, 10, -1, 0, 8) .attr(HealAttr, 0.25, true, false) - .attr(HealStatusEffectAttr, false, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP) + .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .target(MoveTarget.USER_AND_ALLIES), new AttackMove(Moves.WICKED_BLOW, Type.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8) .attr(CritOnlyAttr) @@ -10320,12 +10319,12 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.LUNAR_BLESSING, Type.PSYCHIC, -1, 5, -1, 0, 8) .attr(HealAttr, 0.25, true, false) - .attr(HealStatusEffectAttr, false, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP) + .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .target(MoveTarget.USER_AND_ALLIES) .triageMove(), new SelfStatusMove(Moves.TAKE_HEART, Type.PSYCHIC, -1, 10, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF ], 1, true) - .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP), + .attr(HealStatusEffectAttr, true, [ StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP ]), /* Unused new AttackMove(Moves.G_MAX_WILDFIRE, Type.FIRE, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.ALL_NEAR_ENEMIES) From c535e928d84688b361c54e9bb8992a079f71013d Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:45:21 -0800 Subject: [PATCH 82/98] [Beta][QoL] Improved cursor memory for target selection in Doubles (#4849) * Added more intelligent cursor memory for target selection in Doubles * Added documentation * Fixed variable name. * Apply suggestions from code review Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --------- Co-authored-by: frutescens Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/ui/target-select-ui-handler.ts | 40 +++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index ecc15e5985e..249ae7b8b01 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -13,9 +13,11 @@ import { SubstituteTag } from "#app/data/battler-tags"; export type TargetSelectCallback = (targets: BattlerIndex[]) => void; export default class TargetSelectUiHandler extends UiHandler { - private fieldIndex: integer; + private fieldIndex: number; private move: Moves; private targetSelectCallback: TargetSelectCallback; + private cursor0: number; // associated with BattlerIndex.PLAYER + private cursor1: number; // associated with BattlerIndex.PLAYER_2 private isMultipleTargets: boolean = false; private targets: BattlerIndex[]; @@ -42,8 +44,9 @@ export default class TargetSelectUiHandler extends UiHandler { this.fieldIndex = args[0] as integer; this.move = args[1] as Moves; this.targetSelectCallback = args[2] as TargetSelectCallback; + const user = this.scene.getPlayerField()[this.fieldIndex]; - const moveTargets = getMoveTargets(this.scene.getPlayerField()[this.fieldIndex], this.move); + const moveTargets = getMoveTargets(user, this.move); this.targets = moveTargets.targets; this.isMultipleTargets = moveTargets.multiple ?? false; @@ -53,11 +56,29 @@ export default class TargetSelectUiHandler extends UiHandler { this.enemyModifiers = this.scene.getModifierBar(true); - this.setCursor(this.targets.includes(this.cursor) ? this.cursor : this.targets[0]); - + if (this.fieldIndex === BattlerIndex.PLAYER) { + this.resetCursor(this.cursor0, user); + } else if (this.fieldIndex === BattlerIndex.PLAYER_2) { + this.resetCursor(this.cursor1, user); + } return true; } + /** + * Determines what value to assign the main cursor based on the previous turn's target or the user's status + * @param cursorN the cursor associated with the user's field index + * @param user the Pokemon using the move + */ + resetCursor(cursorN: number, user: Pokemon): void { + if (!Utils.isNullOrUndefined(cursorN)) { + if ([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2 ].includes(cursorN) || user.battleSummonData.waveTurnCount === 1) { + // Reset cursor on the first turn of a fight or if an ally was targeted last turn + cursorN = -1; + } + } + this.setCursor(this.targets.includes(cursorN) ? cursorN : this.targets[0]); + } + processInput(button: Button): boolean { const ui = this.getUi(); @@ -67,6 +88,15 @@ export default class TargetSelectUiHandler extends UiHandler { const targetIndexes: BattlerIndex[] = this.isMultipleTargets ? this.targets : [ this.cursor ]; this.targetSelectCallback(button === Button.ACTION ? targetIndexes : []); success = true; + if (this.fieldIndex === BattlerIndex.PLAYER) { + if (Utils.isNullOrUndefined(this.cursor0) || this.cursor0 !== this.cursor) { + this.cursor0 = this.cursor; + } + } else if (this.fieldIndex === BattlerIndex.PLAYER_2) { + if (Utils.isNullOrUndefined(this.cursor1) || this.cursor1 !== this.cursor) { + this.cursor1 = this.cursor; + } + } } else if (this.isMultipleTargets) { success = false; } else { @@ -152,7 +182,6 @@ export default class TargetSelectUiHandler extends UiHandler { yoyo: true })); }); - return ret; } @@ -184,7 +213,6 @@ export default class TargetSelectUiHandler extends UiHandler { } clear() { - this.cursor = -1; super.clear(); this.eraseCursor(); } From 5e7d44abfdabe78157a3641aaa036eb32a033e56 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:07:02 -0500 Subject: [PATCH 83/98] [Balance] Fix Liquidation, Double-Edge, Body Press compatibility (#4879) --- src/data/balance/tms.ts | 124 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/src/data/balance/tms.ts b/src/data/balance/tms.ts index 7b65ae65ec4..4882cf4f652 100644 --- a/src/data/balance/tms.ts +++ b/src/data/balance/tms.ts @@ -3302,11 +3302,23 @@ export const tmSpecies: TmSpecies = { Species.FERALIGATR, Species.SENTRET, Species.FURRET, + Species.HOOTHOOT, Species.NOCTOWL, + Species.LEDYBA, + Species.LEDIAN, + Species.SPINARAK, + Species.ARIADOS, Species.CROBAT, Species.CHINCHOU, Species.LANTURN, + Species.PICHU, + Species.CLEFFA, Species.IGGLYBUFF, + Species.TYROGUE, + Species.TOGEPI, + Species.TOGETIC, + Species.NATU, + Species.XATU, Species.MAREEP, Species.FLAAFFY, Species.AMPHAROS, @@ -3328,6 +3340,7 @@ export const tmSpecies: TmSpecies = { Species.UMBREON, Species.MURKROW, Species.SLOWKING, + Species.MISDREAVUS, Species.GIRAFARIG, Species.PINECO, Species.FORRETRESS, @@ -3338,11 +3351,21 @@ export const tmSpecies: TmSpecies = { Species.GRANBULL, Species.QWILFISH, Species.SCIZOR, + Species.SHUCKLE, Species.HERACROSS, + Species.SNEASEL, Species.TEDDIURSA, Species.URSARING, + Species.SLUGMA, + Species.MAGCARGO, Species.SWINUB, Species.PILOSWINE, + Species.CORSOLA, + Species.REMORAID, + Species.OCTILLERY, + Species.DELIBIRD, + Species.MANTINE, + Species.SKARMORY, Species.HOUNDOUR, Species.HOUNDOOM, Species.KINGDRA, @@ -3350,9 +3373,12 @@ export const tmSpecies: TmSpecies = { Species.DONPHAN, Species.PORYGON2, Species.STANTLER, + Species.TYROGUE, Species.HITMONTOP, + Species.SMOOCHUM, Species.ELEKID, Species.MAGBY, + Species.MILTANK, Species.BLISSEY, Species.RAIKOU, Species.ENTEI, @@ -3362,6 +3388,9 @@ export const tmSpecies: TmSpecies = { Species.TYRANITAR, Species.LUGIA, Species.HO_OH, + Species.CELEBI, + Species.TREECKO, + Species.GROVYLE, Species.SCEPTILE, Species.TORCHIC, Species.COMBUSKEN, @@ -3371,41 +3400,116 @@ export const tmSpecies: TmSpecies = { Species.SWAMPERT, Species.POOCHYENA, Species.MIGHTYENA, + Species.ZIGZAGOON, + Species.LINOONE, + Species.BEAUTIFLY, + Species.DUSTOX, Species.LOTAD, Species.LOMBRE, Species.LUDICOLO, Species.SEEDOT, Species.NUZLEAF, Species.SHIFTRY, + Species.TAILLOW, + Species.SWELLOW, + Species.WINGULL, + Species.PELIPPER, + Species.RALTS, + Species.KIRLIA, + Species.GARDEVOIR, + Species.SURSKIT, + Species.MASQUERAIN, + Species.SHROOMISH, + Species.BRELOOM, Species.VIGOROTH, Species.SLAKING, + Species.NINCADA, + Species.NINJASK, + Species.SHEDINJA, + Species.WHISMUR, + Species.LOUDRED, + Species.EXPLOUD, Species.MAKUHITA, Species.HARIYAMA, + Species.AZURILL, Species.NOSEPASS, + Species.SKITTY, + Species.DELCATTY, + Species.SABLEYE, + Species.MAWILE, + Species.ARON, + Species.LAIRON, + Species.AGGRON, + Species.MEDITITE, + Species.MEDICHAM, + Species.ELECTRIKE, + Species.MANECTRIC, + Species.PLUSLE, + Species.MINUN, Species.VOLBEAT, Species.ILLUMISE, + Species.ROSELIA, + Species.GULPIN, Species.SWALOT, + Species.CARVANHA, + Species.SHARPEDO, + Species.WAILMER, + Species.WAILORD, Species.NUMEL, Species.CAMERUPT, Species.TORKOAL, + Species.SPOINK, + Species.GRUMPIG, + Species.SPINDA, + Species.TRAPINCH, + Species.VIBRAVA, Species.FLYGON, + Species.CACNEA, + Species.CACTURNE, + Species.SWABLU, Species.ALTARIA, Species.ZANGOOSE, Species.SEVIPER, + Species.LUNATONE, + Species.SOLROCK, Species.BARBOACH, Species.WHISCASH, Species.CORPHISH, Species.CRAWDAUNT, + Species.BALTOY, + Species.CLAYDOL, + Species.LILEEP, + Species.CRADILY, + Species.ANORITH, + Species.ARMALDO, Species.FEEBAS, Species.MILOTIC, + Species.CASTFORM, + Species.KECLEON, + Species.SHUPPET, + Species.BANETTE, + Species.DUSKULL, + Species.DUSCLOPS, Species.TROPIUS, Species.CHIMECHO, + Species.ABSOL, + Species.SNORUNT, + Species.GLALIE, + Species.SPHEAL, + Species.SEALEO, + Species.WALREIN, + Species.CLAMPERL, + Species.HUNTAIL, + Species.GOREBYSS, + Species.RELICANTH, + Species.LUVDISC, Species.BAGON, Species.SHELGON, Species.SALAMENCE, Species.METANG, Species.METAGROSS, Species.REGIROCK, + Species.REGICE, Species.REGISTEEL, Species.LATIAS, Species.LATIOS, @@ -3413,6 +3517,7 @@ export const tmSpecies: TmSpecies = { Species.GROUDON, Species.RAYQUAZA, Species.JIRACHI, + Species.DEOXYS, Species.TURTWIG, Species.GROTLE, Species.TORTERRA, @@ -64246,12 +64351,16 @@ export const tmSpecies: TmSpecies = { Species.BLOODMOON_URSALUNA, ], [Moves.LIQUIDATION]: [ + Species.SQUIRTLE, + Species.WARTORTLE, Species.BLASTOISE, Species.PSYDUCK, Species.GOLDUCK, Species.POLIWAG, Species.POLIWHIRL, Species.POLIWRATH, + Species.TENTACOOL, + Species.TENTACRUEL, Species.SLOWPOKE, Species.SLOWBRO, Species.DEWGONG, @@ -64267,7 +64376,11 @@ export const tmSpecies: TmSpecies = { Species.KABUTO, Species.KABUTOPS, Species.MEW, + Species.TOTODILE, + Species.CROCONAW, Species.FERALIGATR, + Species.CHINCHOU, + Species.LANTURN, Species.MARILL, Species.AZUMARILL, Species.POLITOED, @@ -64280,6 +64393,9 @@ export const tmSpecies: TmSpecies = { Species.MANTINE, Species.KINGDRA, Species.SUICUNE, + Species.LUGIA, + Species.MUDKIP, + Species.MARSHTOMP, Species.SWAMPERT, Species.WINGULL, Species.PELIPPER, @@ -64296,6 +64412,8 @@ export const tmSpecies: TmSpecies = { Species.WALREIN, Species.RELICANTH, Species.LUVDISC, + Species.LATIAS, + Species.LATIOS, Species.KYOGRE, Species.PIPLUP, Species.PRINPLUP, @@ -64407,11 +64525,13 @@ export const tmSpecies: TmSpecies = { Species.ONIX, Species.HYPNO, Species.LICKITUNG, + Species.RHYHORN, Species.RHYDON, Species.LAPRAS, Species.SNORLAX, Species.DRAGONITE, Species.MEW, + Species.MEGANIUM, Species.SUDOWOODO, Species.QUAGSIRE, Species.FORRETRESS, @@ -64445,6 +64565,8 @@ export const tmSpecies: TmSpecies = { Species.REGISTEEL, Species.GROUDON, Species.TORTERRA, + Species.RAMPARDOS, + Species.BASTIODON, Species.BRONZONG, Species.HIPPOPOTAS, Species.HIPPOWDON, @@ -64459,6 +64581,7 @@ export const tmSpecies: TmSpecies = { Species.HEATRAN, Species.REGIGIGAS, Species.ARCEUS, + Species.EMBOAR, Species.ROGGENROLA, Species.BOLDORE, Species.GIGALITH, @@ -64471,6 +64594,7 @@ export const tmSpecies: TmSpecies = { Species.CUBCHOO, Species.BEARTIC, Species.GOLURK, + Species.COBALION, Species.RESHIRAM, Species.ZEKROM, Species.KYUREM, From 413f2b80c8ba5bc181c603cd7ae45c5b40ab21f8 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:26:58 -0500 Subject: [PATCH 84/98] [Balance] Increase Gimmighoul's evo counter more with Big Nugget/Relic Gold (#4856) * Increase Gimmighoul's evo counter more with Big Nugget/Relic Gold * Update src/modifier/modifier.ts Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --------- Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/modifier/modifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index ac8dc556b98..6b449c38ada 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -2818,7 +2818,7 @@ export class MoneyRewardModifier extends ConsumableModifier { battleScene.getPlayerParty().map(p => { if (p.species?.speciesId === Species.GIMMIGHOUL || p.fusionSpecies?.speciesId === Species.GIMMIGHOUL) { - p.evoCounter ? p.evoCounter++ : p.evoCounter = 1; + p.evoCounter ? p.evoCounter += Math.min(Math.floor(this.moneyMultiplier), 3) : p.evoCounter = Math.min(Math.floor(this.moneyMultiplier), 3); const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier(p) as EvoTrackerModifier; battleScene.addModifier(modifier); } From 360a897ed2f53a0f54dc96f3a16fc5bade166956 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:11:46 -0800 Subject: [PATCH 85/98] [Balance] Endure Tokens only endure one hit (#4875) * Endure Tokens only endure one hit * Add tests for Endure * Update docs --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/battler-tags.ts | 13 +++++-- src/enums/battler-tag-type.ts | 1 + src/field/pokemon.ts | 2 ++ src/modifier/modifier.ts | 6 ++-- src/test/moves/endure.test.ts | 65 +++++++++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 src/test/moves/endure.test.ts diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 28ab5ff2a4f..ce25b56157c 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1502,9 +1502,14 @@ export class ContactBurnProtectedTag extends DamageProtectedTag { } } +/** + * `BattlerTag` class for effects that cause the affected Pokemon to survive lethal attacks at 1 HP. + * Used for {@link https://bulbapedia.bulbagarden.net/wiki/Endure_(move) | Endure} and + * Endure Tokens. + */ export class EnduringTag extends BattlerTag { - constructor(sourceMove: Moves) { - super(BattlerTagType.ENDURING, BattlerTagLapseType.TURN_END, 0, sourceMove); + constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, sourceMove: Moves) { + super(tagType, lapseType, 0, sourceMove); } onAdd(pokemon: Pokemon): void { @@ -3009,7 +3014,9 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source case BattlerTagType.BURNING_BULWARK: return new ContactBurnProtectedTag(sourceMove); case BattlerTagType.ENDURING: - return new EnduringTag(sourceMove); + return new EnduringTag(tagType, BattlerTagLapseType.TURN_END, sourceMove); + case BattlerTagType.ENDURE_TOKEN: + return new EnduringTag(tagType, BattlerTagLapseType.AFTER_HIT, sourceMove); case BattlerTagType.STURDY: return new SturdyTag(sourceMove); case BattlerTagType.PERISH_SONG: diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index b2bbc1e6189..bb969386630 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -92,4 +92,5 @@ export enum BattlerTagType { COMMANDED = "COMMANDED", GRUDGE = "GRUDGE", PSYCHO_SHIFT = "PSYCHO_SHIFT", + ENDURE_TOKEN = "ENDURE_TOKEN", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 30d1aceea4b..07a958fe27b 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2955,6 +2955,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { surviveDamage.value = this.lapseTag(BattlerTagType.ENDURING); } else if (this.hp > 1 && this.getTag(BattlerTagType.STURDY)) { surviveDamage.value = this.lapseTag(BattlerTagType.STURDY); + } else if (this.hp >= 1 && this.getTag(BattlerTagType.ENDURE_TOKEN)) { + surviveDamage.value = this.lapseTag(BattlerTagType.ENDURE_TOKEN); } if (!surviveDamage.value) { this.scene.applyModifiers(SurviveDamageModifier, this.isPlayer(), this, surviveDamage); diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 6b449c38ada..4e59d6c01f0 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -3647,8 +3647,8 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { } /** - * Applies {@linkcode EnemyEndureChanceModifier} - * @param target {@linkcode Pokemon} to apply the {@linkcode BattlerTagType.ENDURING} chance to + * Applies a chance of enduring a lethal hit of an attack + * @param target the {@linkcode Pokemon} to apply the {@linkcode BattlerTagType.ENDURING} chance to * @returns `true` if {@linkcode Pokemon} endured */ override apply(target: Pokemon): boolean { @@ -3656,7 +3656,7 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { return false; } - target.addTag(BattlerTagType.ENDURING, 1); + target.addTag(BattlerTagType.ENDURE_TOKEN, 1); target.battleData.endured = true; diff --git a/src/test/moves/endure.test.ts b/src/test/moves/endure.test.ts new file mode 100644 index 00000000000..bde5a26f68e --- /dev/null +++ b/src/test/moves/endure.test.ts @@ -0,0 +1,65 @@ +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"; + +describe("Moves - Endure", () => { + 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.THUNDER, Moves.BULLET_SEED, Moves.TOXIC ]) + .ability(Abilities.SKILL_LINK) + .startingLevel(100) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.NO_GUARD) + .enemyMoveset(Moves.ENDURE); + }); + + it("should let the pokemon survive with 1 HP", async () => { + await game.classicMode.startBattle([ Species.ARCEUS ]); + + game.move.select(Moves.THUNDER); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.hp).toBe(1); + }); + + it("should let the pokemon survive with 1 HP when hit with a multihit move", async () => { + await game.classicMode.startBattle([ Species.ARCEUS ]); + + game.move.select(Moves.BULLET_SEED); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.hp).toBe(1); + }); + + it("shouldn't prevent fainting from indirect damage", async () => { + game.override.enemyLevel(100); + await game.classicMode.startBattle([ Species.ARCEUS ]); + + const enemy = game.scene.getEnemyPokemon()!; + enemy.hp = 2; + + game.move.select(Moves.TOXIC); + await game.phaseInterceptor.to("VictoryPhase"); + + expect(enemy.isFainted()).toBe(true); + }); +}); From 77b94bf45d9b3b92d44fb2c27936a4fd2d8cf390 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Sat, 16 Nov 2024 17:46:02 +0100 Subject: [PATCH 86/98] bump version to 1.2.0 and update locale submodule to latest commit (#4884) --- package-lock.json | 4 ++-- package.json | 2 +- public/locales | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9e512884922..792720200a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pokemon-rogue-battle", - "version": "1.1.6", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pokemon-rogue-battle", - "version": "1.1.6", + "version": "1.2.0", "hasInstallScript": true, "dependencies": { "@material/material-color-utilities": "^0.2.7", diff --git a/package.json b/package.json index f106fb1a773..3c65fe1a8b9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pokemon-rogue-battle", "private": true, - "version": "1.1.6", + "version": "1.2.0", "type": "module", "scripts": { "start": "vite", diff --git a/public/locales b/public/locales index 5775faa6b31..ed1b1df4776 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 5775faa6b3184082df73f6cdb96b253ea7dae3fe +Subproject commit ed1b1df4776ccd4330e8ac1d2f44de611d04c2bc From 4aff2ffa1d4bfe82494b3489f21e46b6a88adc6a Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sat, 16 Nov 2024 15:30:47 -0800 Subject: [PATCH 87/98] [Dev] Sessions will now sync with the server at the start of every 5 waves (#4808) * Replaced conditional with `true` so that sync-to-server occurs at the start of every wave. * Update src/phases/encounter-phase.ts Co-authored-by: Frederico Santos * Fixed errors from previous commit. * Revised condition to include the first wave of a run. * Apply suggestions from code review Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --------- Co-authored-by: frutescens Co-authored-by: Frederico Santos Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/phases/encounter-phase.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index fc022ab9647..e58500c934e 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -225,7 +225,8 @@ export class EncounterPhase extends BattlePhase { this.scene.ui.setMode(Mode.MESSAGE).then(() => { if (!this.loaded) { this.trySetWeatherIfNewBiome(); // Set weather before session gets saved - this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || (this.scene.lastSavePlayTime ?? 0) >= 300).then(success => { + // Game syncs to server on waves X1 and X6 (As of 1.2.0) + this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 5 === 1 || (this.scene.lastSavePlayTime ?? 0) >= 300).then(success => { this.scene.disableMenu = false; if (!success) { return this.scene.reset(true); From c22b48f9032c2c5930abcc62fe2a593794814a96 Mon Sep 17 00:00:00 2001 From: "Amani H." <109637146+xsn34kzx@users.noreply.github.com> Date: Sat, 16 Nov 2024 18:31:28 -0500 Subject: [PATCH 88/98] [P1] Fix System Data Conversion Failure 3 (#4866) * [P1] Fix System Data Conversion Failure 3 * Use `isNullOrUndefined`, Clean Up Imports * fix pre candy update save data migration for candy * fix pre candy update save data migration for candy part 2... --------- Co-authored-by: Moka --- src/system/game-data.ts | 6 +++--- .../version_migration/versions/v1_0_4.ts | 21 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 4d01ae9998a..70b0fadf0b0 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -506,9 +506,9 @@ export class GameData { const starterIds = Object.keys(this.starterData).map(s => parseInt(s) as Species); for (const s of starterIds) { - this.starterData[s].candyCount += this.dexData[s].caughtCount; - this.starterData[s].candyCount += this.dexData[s].hatchedCount * 2; - if (this.dexData[s].caughtAttr & DexAttr.SHINY) { + this.starterData[s].candyCount += systemData.dexData[s].caughtCount; + this.starterData[s].candyCount += systemData.dexData[s].hatchedCount * 2; + if (systemData.dexData[s].caughtAttr & DexAttr.SHINY) { this.starterData[s].candyCount += 4; } } diff --git a/src/system/version_migration/versions/v1_0_4.ts b/src/system/version_migration/versions/v1_0_4.ts index f16b6bcb6bb..d6e5baf0c84 100644 --- a/src/system/version_migration/versions/v1_0_4.ts +++ b/src/system/version_migration/versions/v1_0_4.ts @@ -1,6 +1,7 @@ -import { SettingKeys } from "../../settings/settings"; -import { AbilityAttr, defaultStarterSpecies, DexAttr, SystemSaveData, SessionSaveData } from "../../game-data"; -import { allSpecies } from "../../../data/pokemon-species"; +import { SettingKeys } from "#app/system/settings/settings"; +import { AbilityAttr, defaultStarterSpecies, DexAttr, SystemSaveData, SessionSaveData } from "#app/system/game-data"; +import { allSpecies } from "#app/data/pokemon-species"; +import { isNullOrUndefined } from "#app/utils"; export const systemMigrators = [ /** @@ -46,12 +47,14 @@ export const systemMigrators = [ * @param data {@linkcode SystemSaveData} */ function fixStarterData(data: SystemSaveData) { - for (const starterId of defaultStarterSpecies) { - if (data.starterData[starterId]?.abilityAttr) { - data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1; - } - if (data.dexData[starterId]?.caughtAttr) { - data.dexData[starterId].caughtAttr |= DexAttr.FEMALE; + if (!isNullOrUndefined(data.starterData)) { + for (const starterId of defaultStarterSpecies) { + if (data.starterData[starterId]?.abilityAttr) { + data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1; + } + if (data.dexData[starterId]?.caughtAttr) { + data.dexData[starterId].caughtAttr |= DexAttr.FEMALE; + } } } } From c2afac8b02a6ca793263c68dc5a93bf10386b7bc Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sat, 16 Nov 2024 15:32:05 -0800 Subject: [PATCH 89/98] [Bug] Moved code preventing MBH's transfer to post modifier generation (#4858) * Moved code preventing MBH's transfer to after enemy modifiers were generated. * Removed unnecessary ! * Removed unnecessary ? * Created a new enum and functions for retrieving final bosses. * Moved isBattleClassicFinalBoss to game-mode.ts and reverted battle.ts * Preventing item transfer with tryTransferModifier instead * Added filtering to modifier-retrieve methods. * Revised logic in tryTransferHeldItemModifier * Adding what works to the game even though it's not the best. * Added comments * Removing past changes to files. * Added check for Classic Final Boss. --------- Co-authored-by: frutescens --- src/game-mode.ts | 7 +++++++ src/phases/encounter-phase.ts | 13 +++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/game-mode.ts b/src/game-mode.ts index 0f997bf651e..3f12c5b056e 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -230,6 +230,13 @@ export class GameMode implements GameModeConfig { return waveIndex % 10 === 0; } + /** + * @returns `true` if the current battle is against classic mode's final boss + */ + isBattleClassicFinalBoss(waveIndex: number): boolean { + return (this.modeId === GameModes.CLASSIC || this.modeId === GameModes.CHALLENGE) && this.isWaveFinal(waveIndex); + } + /** * Every 50 waves of an Endless mode is a boss * At this time it is paradox pokemon diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index e58500c934e..03126ba81bb 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -141,10 +141,6 @@ export class EncounterPhase extends BattlePhase { } else if (!(battle.waveIndex % 1000)) { enemyPokemon.formIndex = 1; enemyPokemon.updateScale(); - const bossMBH = this.scene.findModifier(m => m instanceof TurnHeldItemTransferModifier && m.pokemonId === enemyPokemon.id, false) as TurnHeldItemTransferModifier; - this.scene.removeModifier(bossMBH!); - bossMBH?.setTransferrableFalse(); - this.scene.addEnemyModifier(bossMBH!); } } @@ -443,6 +439,15 @@ export class EncounterPhase extends BattlePhase { if (enemyPokemon.isShiny()) { this.scene.unshiftPhase(new ShinySparklePhase(this.scene, BattlerIndex.ENEMY + e)); } + /** This sets Eternatus' held item to be untransferrable, preventing it from being stolen */ + if (enemyPokemon.species.speciesId === Species.ETERNATUS && (this.scene.gameMode.isBattleClassicFinalBoss(this.scene.currentBattle.waveIndex) || this.scene.gameMode.isEndlessMajorBoss(this.scene.currentBattle.waveIndex))) { + const enemyMBH = this.scene.findModifier(m => m instanceof TurnHeldItemTransferModifier, false) as TurnHeldItemTransferModifier; + if (enemyMBH) { + this.scene.removeModifier(enemyMBH, true); + enemyMBH.setTransferrableFalse(); + this.scene.addEnemyModifier(enemyMBH); + } + } }); if (![ BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER ].includes(this.scene.currentBattle.battleType)) { From 7dc4210f711e0cb92ac668b9a217561731b2e83c Mon Sep 17 00:00:00 2001 From: Daniel Pochert Date: Sun, 17 Nov 2024 00:33:14 +0100 Subject: [PATCH 90/98] [Bug] fix training session encounter not applying nature correctly (#4801) * fix training session encounter not applying nature correctly * implement pokemon.setCustomNature + nature unlock utility method(s) * update doc --------- Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- .../shady-vitamin-dealer-encounter.ts | 2 +- .../encounters/training-session-encounter.ts | 4 +-- src/field/pokemon.ts | 5 ++++ src/modifier/modifier.ts | 12 +++------ src/system/game-data.ts | 26 +++++++++++++++++++ 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 798e85868f6..3f1ace47b0f 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -138,7 +138,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = newNature = randSeedInt(25) as Nature; } - chosenPokemon.customPokemonData.nature = newNature; + chosenPokemon.setCustomNature(newNature); encounter.setDialogueToken("newNature", getNatureName(newNature)); queueEncounterMessage(scene, `${namespace}:cheap_side_effects`); setEncounterExp(scene, [ chosenPokemon.id ], 100); diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 4a0a91bf751..725c4ba79eb 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -226,8 +226,8 @@ export const TrainingSessionEncounter: MysteryEncounter = const onBeforeRewardsPhase = () => { queueEncounterMessage(scene, `${namespace}:option.2.finished`); // Add the pokemon back to party with Nature change - playerPokemon.setNature(encounter.misc.chosenNature); - scene.gameData.setPokemonCaught(playerPokemon, false); + playerPokemon.setCustomNature(encounter.misc.chosenNature); + scene.gameData.unlockSpeciesNature(playerPokemon.species, encounter.misc.chosenNature); // Add pokemon and modifiers back scene.getPlayerParty().push(playerPokemon); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 07a958fe27b..f9e7d7d1cad 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1072,6 +1072,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.calculateStats(); } + setCustomNature(nature: Nature): void { + this.customPokemonData.nature = nature; + this.calculateStats(); + } + generateNature(naturePool?: Nature[]): void { if (naturePool === undefined) { naturePool = Utils.getEnumValues(Nature); diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 4e59d6c01f0..bbd1763cd84 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1,5 +1,5 @@ import type BattleScene from "#app/battle-scene"; -import { FusionSpeciesFormEvolution, pokemonEvolutions, pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; +import { FusionSpeciesFormEvolution, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { getBerryEffectFunc, getBerryPredicate } from "#app/data/berry"; import { getLevelTotalExp } from "#app/data/exp"; import { allMoves } from "#app/data/move"; @@ -2197,14 +2197,8 @@ export class PokemonNatureChangeModifier extends ConsumablePokemonModifier { * @returns */ override apply(playerPokemon: PlayerPokemon): boolean { - playerPokemon.customPokemonData.nature = this.nature; - let speciesId = playerPokemon.species.speciesId; - playerPokemon.scene.gameData.dexData[speciesId].natureAttr |= 1 << (this.nature + 1); - - while (pokemonPrevolutions.hasOwnProperty(speciesId)) { - speciesId = pokemonPrevolutions[speciesId]; - playerPokemon.scene.gameData.dexData[speciesId].natureAttr |= 1 << (this.nature + 1); - } + playerPokemon.setCustomNature(this.nature); + playerPokemon.scene.gameData.unlockSpeciesNature(playerPokemon.species, this.nature); return true; } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 70b0fadf0b0..3e3a6ce8f8b 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1791,6 +1791,32 @@ export class GameData { }); } + /** + * Checks whether the root species of a given {@PokemonSpecies} has been unlocked in the dex + */ + isRootSpeciesUnlocked(species: PokemonSpecies): boolean { + return !!this.dexData[species.getRootSpeciesId()]?.caughtAttr; + } + + /** + * Unlocks the given {@linkcode Nature} for a {@linkcode PokemonSpecies} and its prevolutions. + * Will fail silently if root species has not been unlocked + */ + unlockSpeciesNature(species: PokemonSpecies, nature: Nature): void { + if (!this.isRootSpeciesUnlocked(species)) { + return; + } + + //recursively unlock nature for species and prevolutions + const _unlockSpeciesNature = (speciesId: Species) => { + this.dexData[speciesId].natureAttr |= 1 << (nature + 1); + if (pokemonPrevolutions.hasOwnProperty(speciesId)) { + _unlockSpeciesNature(pokemonPrevolutions[speciesId]); + } + }; + _unlockSpeciesNature(species.speciesId); + } + updateSpeciesDexIvs(speciesId: Species, ivs: integer[]): void { let dexEntry: DexEntry; do { From ae8efeedf8ca520f2beb17ca8093586de5775ca0 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Sun, 17 Nov 2024 00:34:14 +0100 Subject: [PATCH 91/98] [UI] Make ME text legible when using the legacy UI theme (#4847) --- .../utils/encounter-dialogue-utils.ts | 8 ++-- src/ui/mystery-encounter-ui-handler.ts | 4 +- src/ui/run-info-ui-handler.ts | 3 +- src/ui/text.ts | 37 ++++++++++++++++--- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts index c4d5e47cb05..acaa7c6244f 100644 --- a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts @@ -7,22 +7,22 @@ import i18next from "i18next"; /** * Will inject all relevant dialogue tokens that exist in the {@linkcode BattleScene.currentBattle.mysteryEncounter.dialogueTokens}, into i18n text. * Also adds BBCodeText fragments for colored text, if applicable - * @param scene * @param keyOrString * @param primaryStyle Can define a text style to be applied to the entire string. Must be defined for BBCodeText styles to be applied correctly - * @param uiTheme */ -export function getEncounterText(scene: BattleScene, keyOrString?: string, primaryStyle?: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string | null { +export function getEncounterText(scene: BattleScene, keyOrString?: string, primaryStyle?: TextStyle): string | null { if (isNullOrUndefined(keyOrString)) { return null; } + const uiTheme = scene.uiTheme ?? UiTheme.DEFAULT; + let textString: string | null = getTextWithDialogueTokens(scene, keyOrString); // Can only color the text if a Primary Style is defined // primaryStyle is applied to all text that does not have its own specified style if (primaryStyle && textString) { - textString = getTextWithColors(textString, primaryStyle, uiTheme); + textString = getTextWithColors(textString, primaryStyle, uiTheme, true); } return textString; diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index b568f8b71de..cdb1c9024c5 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -369,9 +369,9 @@ export default class MysteryEncounterUiHandler extends UiHandler { let text: string | null; if (option.hasRequirements() && this.optionsMeetsReqs[i] && (option.optionMode === MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL || option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) { // Options with special requirements that are met are automatically colored green - text = getEncounterText(this.scene, label, TextStyle.SUMMARY_GREEN); + text = getEncounterText(this.scene, label, TextStyle.ME_OPTION_SPECIAL); } else { - text = getEncounterText(this.scene, label, optionDialogue.style ? optionDialogue.style : TextStyle.WINDOW); + text = getEncounterText(this.scene, label, optionDialogue.style ? optionDialogue.style : TextStyle.ME_OPTION_DEFAULT); } if (text) { diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 82d390016f7..ace9d956dd9 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -518,7 +518,8 @@ export default class RunInfoUiHandler extends UiHandler { const runTime = Utils.getPlayTimeString(this.runInfo.playTime); runInfoText.appendText(`${i18next.t("runHistory:runLength")}: ${runTime}`, false); const runMoney = Utils.formatMoney(this.scene.moneyFormat, this.runInfo.money); - runInfoText.appendText(`[color=${getTextColor(TextStyle.MONEY)}]${i18next.t("battleScene:moneyOwned", { formattedMoney : runMoney })}[/color]`); + const moneyTextColor = getTextColor(TextStyle.MONEY_WINDOW, false, this.scene.uiTheme); + runInfoText.appendText(`[color=${moneyTextColor}]${i18next.t("battleScene:moneyOwned", { formattedMoney : runMoney })}[/color]`); runInfoText.setPosition(7, 70); runInfoTextContainer.add(runInfoText); // Luck diff --git a/src/ui/text.ts b/src/ui/text.ts index 069aa8680fc..17ae02be9ef 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -22,7 +22,8 @@ export enum TextStyle { SUMMARY_GOLD, SUMMARY_GRAY, SUMMARY_GREEN, - MONEY, + MONEY, // Money default styling (pale yellow) + MONEY_WINDOW, // Money displayed in Windows (needs different colors based on theme) STATS_LABEL, STATS_VALUE, SETTINGS_VALUE, @@ -38,7 +39,9 @@ export enum TextStyle { MOVE_PP_EMPTY, SMALLER_WINDOW_ALT, BGM_BAR, - PERFECT_IV + PERFECT_IV, + ME_OPTION_DEFAULT, // Default style for choices in ME + ME_OPTION_SPECIAL, // Style for choices with special requirements in ME } export interface TextStyleOptions { @@ -139,6 +142,8 @@ export function getTextStyleOptions(style: TextStyle, uiTheme: UiTheme, extraSty case TextStyle.SUMMARY_GREEN: case TextStyle.WINDOW: case TextStyle.WINDOW_ALT: + case TextStyle.ME_OPTION_DEFAULT: + case TextStyle.ME_OPTION_SPECIAL: shadowXpos = 3; shadowYpos = 3; break; @@ -177,6 +182,7 @@ export function getTextStyleOptions(style: TextStyle, uiTheme: UiTheme, extraSty break; case TextStyle.BATTLE_INFO: case TextStyle.MONEY: + case TextStyle.MONEY_WINDOW: case TextStyle.TOOLTIP_TITLE: styleOptions.fontSize = defaultFontSize - 24; shadowXpos = 3.5; @@ -238,13 +244,22 @@ export function getBBCodeFrag(content: string, textStyle: TextStyle, uiTheme: Ui * - "red text" with TextStyle.SUMMARY_RED applied * @param content string with styling that need to be applied for BBCodeTextObject * @param primaryStyle Primary style is required in order to escape BBCode styling properly. - * @param uiTheme + * @param uiTheme the {@linkcode UiTheme} to get TextStyle for + * @param forWindow set to `true` if the text is to be displayed in a window ({@linkcode BattleScene.addWindow}) + * it will replace all instances of the default MONEY TextStyle by {@linkcode TextStyle.MONEY_WINDOW} */ -export function getTextWithColors(content: string, primaryStyle: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string { +export function getTextWithColors(content: string, primaryStyle: TextStyle, uiTheme: UiTheme, forWindow?: boolean): string { // Apply primary styling before anything else let text = getBBCodeFrag(content, primaryStyle, uiTheme) + "[/color][/shadow]"; const primaryStyleString = [ ...text.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))! ][0]; + /* For money text displayed in game windows, we can't use the default {@linkcode TextStyle.MONEY} + * or it will look wrong in legacy mode because of the different window background color + * So, for text to be displayed in windows replace all "@[MONEY]" with "@[MONEY_WINDOW]" */ + if (forWindow) { + text = text.replace(/@\[MONEY\]/g, (_substring: string) => "@[MONEY_WINDOW]"); + } + // Set custom colors text = text.replace(/@\[([^{]*)\]{([^}]*)}/gi, (substring, textStyle: string, textToColor: string) => { return "[/color][/shadow]" + getBBCodeFrag(textToColor, TextStyle[textStyle], uiTheme) + "[/color][/shadow]" + primaryStyleString; @@ -310,7 +325,12 @@ export function getTextColor(textStyle: TextStyle, shadow?: boolean, uiTheme: Ui return !shadow ? "#f89890" : "#984038"; case TextStyle.SUMMARY_GOLD: case TextStyle.MONEY: - return !shadow ? "#e8e8a8" : "#a0a060"; + return !shadow ? "#e8e8a8" : "#a0a060"; // Pale Yellow/Gold + case TextStyle.MONEY_WINDOW: + if (isLegacyTheme) { + return !shadow ? "#f8b050" : "#c07800"; // Gold + } + return !shadow ? "#e8e8a8" : "#a0a060"; // Pale Yellow/Gold case TextStyle.SETTINGS_LOCKED: case TextStyle.SUMMARY_GRAY: return !shadow ? "#a0a0a0" : "#636363"; @@ -332,6 +352,13 @@ export function getTextColor(textStyle: TextStyle, shadow?: boolean, uiTheme: Ui return !shadow ? "#484848" : "#d0d0c8"; case TextStyle.BGM_BAR: return !shadow ? "#f8f8f8" : "#6b5a73"; + case TextStyle.ME_OPTION_DEFAULT: + return !shadow ? "#f8f8f8" : "#6b5a73"; // White + case TextStyle.ME_OPTION_SPECIAL: + if (isLegacyTheme) { + return !shadow ? "#f8b050" : "#c07800"; // Gold + } + return !shadow ? "#78c850" : "#306850"; // Green } } From 07f08877c18a027b9ff6c473476796b3d57b992b Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Sun, 17 Nov 2024 12:16:54 -0500 Subject: [PATCH 92/98] [Balance] Multi Lens damage reduction on fixed-damage moves (#4896) --- src/field/pokemon.ts | 4 ++++ src/modifier/modifier.ts | 16 +++++++++++---- src/test/abilities/parental_bond.test.ts | 2 +- src/test/items/multi_lens.test.ts | 25 ++++++++++++++++++++++-- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f9e7d7d1cad..f14fc954c84 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2622,6 +2622,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const fixedDamage = new Utils.IntegerHolder(0); applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage); if (fixedDamage.value) { + const multiLensMultiplier = new Utils.NumberHolder(1); + source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, move.id, null, multiLensMultiplier); + fixedDamage.value = Utils.toDmgValue(fixedDamage.value * multiLensMultiplier.value); + return { cancelled: false, result: HitResult.EFFECTIVE, diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index bbd1763cd84..5e5246269a3 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -2727,10 +2727,18 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { * Additional strikes beyond that are given a 0.25x damage multiplier */ private applyDamageModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean { - damageMultiplier.value = (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount) - ? (1 - (0.25 * this.getStackCount())) - : 0.25; - return true; + if (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount) { + // Reduce first hit by 25% for each stack count + damageMultiplier.value *= 1 - 0.25 * this.getStackCount(); + return true; + } else if (pokemon.turnData.hitCount - pokemon.turnData.hitsLeft !== this.getStackCount() + 1) { + // Deal 25% damage for each remaining Multi Lens hit + damageMultiplier.value *= 0.25; + return true; + } else { + // An extra hit not caused by Multi Lens -- assume it is Parental Bond + return false; + } } getMaxHeldItemCount(pokemon: Pokemon): number { diff --git a/src/test/abilities/parental_bond.test.ts b/src/test/abilities/parental_bond.test.ts index 4189941a51e..10048a774cd 100644 --- a/src/test/abilities/parental_bond.test.ts +++ b/src/test/abilities/parental_bond.test.ts @@ -313,7 +313,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("MoveEndPhase", false); - expect(enemyPokemon.hp).toBe(enemyStartingHp - 300); + expect(enemyPokemon.hp).toBe(enemyStartingHp - 200); } ); diff --git a/src/test/items/multi_lens.test.ts b/src/test/items/multi_lens.test.ts index d389ca70555..c5e60c9f9e5 100644 --- a/src/test/items/multi_lens.test.ts +++ b/src/test/items/multi_lens.test.ts @@ -32,8 +32,8 @@ describe("Items - Multi Lens", () => { .enemySpecies(Species.SNORLAX) .enemyAbility(Abilities.BALL_FETCH) .enemyMoveset(Moves.SPLASH) - .startingLevel(100) - .enemyLevel(100); + .startingLevel(99) // Check for proper rounding on Seismic Toss damage reduction + .enemyLevel(99); }); it.each([ @@ -114,4 +114,25 @@ describe("Items - Multi Lens", () => { expect(magikarp.turnData.hitCount).toBe(2); }); + + it("should enhance fixed-damage moves while also applying damage reduction", async () => { + game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]) + .moveset(Moves.SEISMIC_TOSS); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + const spy = vi.spyOn(enemyPokemon, "getAttackDamage"); + + game.move.select(Moves.SEISMIC_TOSS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to("MoveEndPhase"); + const damageResults = spy.mock.results.map(result => result.value?.damage); + + expect(damageResults).toHaveLength(2); + expect(damageResults[0]).toBe(Math.floor(playerPokemon.level * 0.75)); + expect(damageResults[1]).toBe(Math.floor(playerPokemon.level * 0.25)); + }); }); From 1867ac4a9e37a49389dd3db6da6c33806258eb77 Mon Sep 17 00:00:00 2001 From: "Amani H." <109637146+xsn34kzx@users.noreply.github.com> Date: Sun, 17 Nov 2024 12:34:41 -0500 Subject: [PATCH 93/98] [Dev] Minor Version Migration Tweaks (#4727) * [P3] Prevent Unintended ME Migration Application * Change Patch Value from 4 to 5 * Bump Game Version * Fix Imports, Use `isNullOrUndefined` --- .../version_migration/version_converter.ts | 2 +- .../version_migration/versions/v1_0_4.ts | 24 +++++++++++++++ .../version_migration/versions/v1_1_0.ts | 29 +------------------ 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/system/version_migration/version_converter.ts b/src/system/version_migration/version_converter.ts index e96afb5cbd4..d0c69dc3213 100644 --- a/src/system/version_migration/version_converter.ts +++ b/src/system/version_migration/version_converter.ts @@ -129,7 +129,7 @@ class SessionVersionConverter extends VersionConverter { if (curMajor === 1) { if (curMinor === 0) { - if (curPatch <= 4) { + if (curPatch <= 5) { console.log("Applying v1.0.4 session data migration!"); this.callMigrators(data, v1_0_4.sessionMigrators); } diff --git a/src/system/version_migration/versions/v1_0_4.ts b/src/system/version_migration/versions/v1_0_4.ts index d6e5baf0c84..95f0337ecdd 100644 --- a/src/system/version_migration/versions/v1_0_4.ts +++ b/src/system/version_migration/versions/v1_0_4.ts @@ -1,6 +1,7 @@ import { SettingKeys } from "#app/system/settings/settings"; import { AbilityAttr, defaultStarterSpecies, DexAttr, SystemSaveData, SessionSaveData } from "#app/system/game-data"; import { allSpecies } from "#app/data/pokemon-species"; +import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { isNullOrUndefined } from "#app/utils"; export const systemMigrators = [ @@ -134,5 +135,28 @@ export const sessionMigrators = [ m.className = "ResetNegativeStatStageModifier"; } }); + }, + /** + * Converts old Pokemon natureOverride and mysteryEncounterData + * to use the new conjoined {@linkcode Pokemon.customPokemonData} structure instead. + * @param data {@linkcode SessionSaveData} + */ + function migrateCustomPokemonDataAndNatureOverrides(data: SessionSaveData) { + // Fix Pokemon nature overrides and custom data migration + data.party.forEach(pokemon => { + if (pokemon["mysteryEncounterPokemonData"]) { + pokemon.customPokemonData = new CustomPokemonData(pokemon["mysteryEncounterPokemonData"]); + pokemon["mysteryEncounterPokemonData"] = null; + } + if (pokemon["fusionMysteryEncounterPokemonData"]) { + pokemon.fusionCustomPokemonData = new CustomPokemonData(pokemon["fusionMysteryEncounterPokemonData"]); + pokemon["fusionMysteryEncounterPokemonData"] = null; + } + pokemon.customPokemonData = pokemon.customPokemonData ?? new CustomPokemonData(); + if (!isNullOrUndefined(pokemon["natureOverride"]) && pokemon["natureOverride"] >= 0) { + pokemon.customPokemonData.nature = pokemon["natureOverride"]; + pokemon["natureOverride"] = -1; + } + }); } ] as const; diff --git a/src/system/version_migration/versions/v1_1_0.ts b/src/system/version_migration/versions/v1_1_0.ts index aac554c4531..5d6247aeaa2 100644 --- a/src/system/version_migration/versions/v1_1_0.ts +++ b/src/system/version_migration/versions/v1_1_0.ts @@ -1,32 +1,5 @@ -import { SessionSaveData } from "../../game-data"; -import { CustomPokemonData } from "#app/data/custom-pokemon-data"; - export const systemMigrators = [] as const; export const settingsMigrators = [] as const; -export const sessionMigrators = [ - /** - * Converts old Pokemon natureOverride and mysteryEncounterData - * to use the new conjoined {@linkcode Pokemon.customPokemonData} structure instead. - * @param data {@linkcode SessionSaveData} - */ - function migrateCustomPokemonDataAndNatureOverrides(data: SessionSaveData) { - // Fix Pokemon nature overrides and custom data migration - data.party.forEach(pokemon => { - if (pokemon["mysteryEncounterPokemonData"]) { - pokemon.customPokemonData = new CustomPokemonData(pokemon["mysteryEncounterPokemonData"]); - pokemon["mysteryEncounterPokemonData"] = null; - } - if (pokemon["fusionMysteryEncounterPokemonData"]) { - pokemon.fusionCustomPokemonData = new CustomPokemonData(pokemon["fusionMysteryEncounterPokemonData"]); - pokemon["fusionMysteryEncounterPokemonData"] = null; - } - pokemon.customPokemonData = pokemon.customPokemonData ?? new CustomPokemonData(); - if (pokemon["natureOverride"] && pokemon["natureOverride"] >= 0) { - pokemon.customPokemonData.nature = pokemon["natureOverride"]; - pokemon["natureOverride"] = -1; - } - }); - } -] as const; +export const sessionMigrators = [] as const; From 6b58d51ea2d75b29341526a3122659b5415f86ab Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sun, 17 Nov 2024 12:19:23 -0800 Subject: [PATCH 94/98] Changed conditional to actually consider the wave index. (#4899) Co-authored-by: frutescens --- src/data/ability.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index b77b18947be..0572b041c95 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4953,7 +4953,7 @@ class ForceSwitchOutHelper { * For wild Pokémon battles, the Pokémon will flee if the conditions are met (waveIndex and double battles). */ } else { - if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) { + if (!pokemon.scene.currentBattle.waveIndex || pokemon.scene.currentBattle.waveIndex % 10 === 0) { return false; } From e825e308f9d24eafa0c6b7a0237b01b62863c631 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sun, 17 Nov 2024 13:22:11 -0800 Subject: [PATCH 95/98] [Test] Update wimp out test and comment (#4900) * Changed conditional to actually consider the wave index. * Added PigeonBar's test * Added check for MEs + Documentation * Apply suggestions from code review Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> --------- Co-authored-by: frutescens Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> --- src/data/ability.ts | 1 + src/test/abilities/wimp_out.test.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/data/ability.ts b/src/data/ability.ts index 0572b041c95..1697c816902 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4951,6 +4951,7 @@ class ForceSwitchOutHelper { } /** * For wild Pokémon battles, the Pokémon will flee if the conditions are met (waveIndex and double battles). + * It will not flee if it is a Mystery Encounter with fleeing disabled (checked in `getSwitchOutCondition()`) or if it is a wave 10x wild boss */ } else { if (!pokemon.scene.currentBattle.waveIndex || pokemon.scene.currentBattle.waveIndex % 10 === 0) { diff --git a/src/test/abilities/wimp_out.test.ts b/src/test/abilities/wimp_out.test.ts index df965fc340d..4283386b248 100644 --- a/src/test/abilities/wimp_out.test.ts +++ b/src/test/abilities/wimp_out.test.ts @@ -613,4 +613,23 @@ describe("Abilities - Wimp Out", () => { confirmNoSwitch(); }); + + it("should not activate on wave X0 bosses", async () => { + game.override.enemyAbility(Abilities.WIMP_OUT) + .startingLevel(5850) + .startingWave(10); + await game.classicMode.startBattle([ Species.GOLISOPOD ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // Use 2 turns of False Swipe due to opponent's health bar shield + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); + + const isVisible = enemyPokemon.visible; + const hasFled = enemyPokemon.switchOutStatus; + expect(isVisible && !hasFled).toBe(true); + }); }); From 33d8db73efa74dbb7fe79468eea6aefe20cce3d4 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Sun, 17 Nov 2024 22:35:14 +0100 Subject: [PATCH 96/98] [P1] Fix crash caused by removing arena tags on a new catch (#4888) --- src/field/pokemon.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f14fc954c84..1dc4972af79 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3101,10 +3101,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } lapseTag(tagType: BattlerTagType): boolean { - const tags = this.summonData?.tags; - if (isNullOrUndefined(tags)) { + if (!this.summonData) { return false; } + const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (tag && !(tag.lapse(this, BattlerTagLapseType.CUSTOM))) { tag.onRemove(this); @@ -3114,6 +3114,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } lapseTags(lapseType: BattlerTagLapseType): void { + if (!this.summonData) { + return; + } const tags = this.summonData.tags; tags.filter(t => lapseType === BattlerTagLapseType.FAINT || ((t.lapseTypes.some(lType => lType === lapseType)) && !(t.lapse(this, lapseType)))).forEach(t => { t.onRemove(this); @@ -3122,6 +3125,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } removeTag(tagType: BattlerTagType): boolean { + if (!this.summonData) { + return false; + } const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (tag) { From dd904fe7827eb2695c5362f0864e32d58df10c6c Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 17 Nov 2024 18:08:00 -0800 Subject: [PATCH 97/98] [Hotfix] Prevent crash if enemy Future Sight user is caught (#4905) --- package-lock.json | 4 ++-- package.json | 2 +- src/field/pokemon.ts | 7 +++++-- src/phases/move-effect-phase.ts | 10 +++++++--- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 792720200a9..22a55cdfa28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pokemon-rogue-battle", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pokemon-rogue-battle", - "version": "1.2.0", + "version": "1.2.1", "hasInstallScript": true, "dependencies": { "@material/material-color-utilities": "^0.2.7", diff --git a/package.json b/package.json index 3c65fe1a8b9..5aaffc59de1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pokemon-rogue-battle", "private": true, - "version": "1.2.0", + "version": "1.2.1", "type": "module", "scripts": { "start": "vite", diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 1dc4972af79..217d5fa699b 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3243,11 +3243,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return null; } - getMoveHistory(): TurnMove[] { + public getMoveHistory(): TurnMove[] { return this.battleSummonData.moveHistory; } - pushMoveHistory(turnMove: TurnMove) { + public pushMoveHistory(turnMove: TurnMove): void { + if (!this.isOnField()) { + return; + } turnMove.turn = this.scene.currentBattle?.turn; this.getMoveHistory().push(turnMove); } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index afc8dd0475d..3a609f850c6 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -92,8 +92,12 @@ export class MoveEffectPhase extends PokemonPhase { 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() && !isDelayedAttack) { - return super.end(); + if (!user.isOnField()) { + if (!isDelayedAttack) { + return super.end(); + } else { + user.resetTurnData(); + } } /** @@ -174,7 +178,7 @@ export class MoveEffectPhase extends PokemonPhase { const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; // Move animation only needs one target - new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), () => { + new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), () => { /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; for (const target of targets) { From 0c920ba195c8fa8470694f5a1f4b3157438aa256 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Mon, 18 Nov 2024 19:41:24 +0100 Subject: [PATCH 98/98] [Hotfix] Prevent more Future Sight crash with new catches (#4910) --- package-lock.json | 4 ++-- package.json | 2 +- src/phases/move-effect-phase.ts | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 22a55cdfa28..9d6e2440f44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pokemon-rogue-battle", - "version": "1.2.1", + "version": "1.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pokemon-rogue-battle", - "version": "1.2.1", + "version": "1.2.2", "hasInstallScript": true, "dependencies": { "@material/material-color-utilities": "^0.2.7", diff --git a/package.json b/package.json index 5aaffc59de1..ef2d4938087 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pokemon-rogue-battle", "private": true, - "version": "1.2.1", + "version": "1.2.2", "type": "module", "scripts": { "start": "vite", diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 3a609f850c6..f7a9b22c396 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -96,6 +96,14 @@ export class MoveEffectPhase extends PokemonPhase { if (!isDelayedAttack) { return super.end(); } else { + if (!user.scene) { + /** + * This happens if the Pokemon that used the delayed attack gets caught and released + * on the turn the attack would have triggered. Having access to the global scene + * in the future may solve this entirely, so for now we just cancel the hit + */ + return super.end(); + } user.resetTurnData(); } }