From 467841d167de63a2adb2c9169503fc1dcabadfbd Mon Sep 17 00:00:00 2001 From: PrabbyDD <147005742+PrabbyDD@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:33:41 -0700 Subject: [PATCH 01/17] [P2] Nightmare triggers at turn end instead of after move (#4702) --- src/data/battler-tags.ts | 2 +- src/test/moves/nightmare.test.ts | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/test/moves/nightmare.test.ts diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 4977a8da5a9..e0616c341be 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -838,7 +838,7 @@ export class SeedTag extends BattlerTag { export class NightmareTag extends BattlerTag { constructor() { - super(BattlerTagType.NIGHTMARE, BattlerTagLapseType.AFTER_MOVE, 1, Moves.NIGHTMARE); + super(BattlerTagType.NIGHTMARE, BattlerTagLapseType.TURN_END, 1, Moves.NIGHTMARE); } onAdd(pokemon: Pokemon): void { diff --git a/src/test/moves/nightmare.test.ts b/src/test/moves/nightmare.test.ts new file mode 100644 index 00000000000..61b133a3280 --- /dev/null +++ b/src/test/moves/nightmare.test.ts @@ -0,0 +1,54 @@ +import { CommandPhase } from "#app/phases/command-phase"; +import { TurnInitPhase } from "#app/phases/turn-init-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 } from "vitest"; +import { StatusEffect } from "#app/data/status-effect"; + +describe("Moves - Nightmare", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override.battleType("single") + .enemySpecies(Species.RATTATA) + .enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.BALL_FETCH) + .enemyStatusEffect(StatusEffect.SLEEP) + .startingLevel(5) + .moveset([ Moves.NIGHTMARE, Moves.SPLASH ]); + }); + + it("lowers enemy hp by 1/4 each turn while asleep", async () => { + await game.classicMode.startBattle([ Species.HYPNO ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyMaxHP = enemyPokemon.hp; + game.move.select(Moves.NIGHTMARE); + await game.phaseInterceptor.to(TurnInitPhase); + + expect(enemyPokemon.hp).toBe(enemyMaxHP - Math.floor(enemyMaxHP / 4)); + + // take a second turn to make sure damage occurs again + await game.phaseInterceptor.to(CommandPhase); + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to(TurnInitPhase); + expect(enemyPokemon.hp).toBe(enemyMaxHP - Math.floor(enemyMaxHP / 4) - Math.floor(enemyMaxHP / 4)); + }); +}); From c2eb9de9df6cd7ba625dd73c3812274f47994731 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:41:25 -0700 Subject: [PATCH 02/17] [Ability] Partially Implement Infiltrator (does not work with mist) (#4636) * Implement Infiltrator * Integration tests + Infiltrator is (P) again * Fix screen tests * Fix `hitsSubstitute` * docs for Infiltrator attr --- src/data/ability.ts | 27 ++++++- src/data/arena-tag.ts | 23 +++++- src/data/move.ts | 15 ++-- src/field/pokemon.ts | 28 +++++-- src/phases/stat-stage-change-phase.ts | 3 +- src/test/abilities/infiltrator.test.ts | 107 +++++++++++++++++++++++++ src/test/moves/aurora_veil.test.ts | 2 +- src/test/moves/light_screen.test.ts | 2 +- src/test/moves/reflect.test.ts | 2 +- 9 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 src/test/abilities/infiltrator.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 33f6e0522f7..0d5cf2751ce 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4342,6 +4342,30 @@ export class AlwaysHitAbAttr extends AbAttr { } /** Attribute for abilities that allow moves that make contact to ignore protection (i.e. Unseen Fist) */ export class IgnoreProtectOnContactAbAttr extends AbAttr { } +/** + * Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Infiltrator_(Ability) | Infiltrator}. + * Allows the source's moves to bypass the effects of opposing Light Screen, Reflect, Aurora Veil, Safeguard, Mist, and Substitute. + */ +export class InfiltratorAbAttr extends AbAttr { + /** + * Sets a flag to bypass screens, Substitute, Safeguard, and Mist + * @param pokemon n/a + * @param passive n/a + * @param simulated n/a + * @param cancelled n/a + * @param args `[0]` a {@linkcode Utils.BooleanHolder | BooleanHolder} containing the flag + * @returns `true` if the bypass flag was successfully set; `false` otherwise. + */ + override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: null, args: any[]): boolean { + const bypassed = args[0]; + if (args[0] instanceof Utils.BooleanHolder) { + bypassed.value = true; + return true; + } + return false; + } +} + export class UncopiableAbilityAbAttr extends AbAttr { constructor() { super(false); @@ -5321,7 +5345,8 @@ export function initAbilities() { .attr(PostSummonTransformAbAttr) .attr(UncopiableAbilityAbAttr), new Ability(Abilities.INFILTRATOR, 5) - .unimplemented(), + .attr(InfiltratorAbAttr) + .partial(), // does not bypass Mist new Ability(Abilities.MUMMY, 5) .attr(PostDefendAbilityGiveAbAttr, Abilities.MUMMY) .bypassFaint(), diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index ed4c2789165..aa6aec6f73a 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -7,7 +7,7 @@ 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, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; +import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, InfiltratorAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; import { Stat } from "#enums/stat"; import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims"; import i18next from "i18next"; @@ -130,7 +130,18 @@ export class MistTag extends ArenaTag { * to flag the stat reduction as cancelled * @returns `true` if a stat reduction was cancelled; `false` otherwise */ - override apply(arena: Arena, simulated: boolean, cancelled: BooleanHolder): boolean { + override apply(arena: Arena, simulated: boolean, attacker: Pokemon, cancelled: BooleanHolder): boolean { + // `StatStageChangePhase` currently doesn't have a reference to the source of stat drops, + // so this code currently has no effect on gameplay. + if (attacker) { + const bypassed = new BooleanHolder(false); + // TODO: Allow this to be simulated + applyAbAttrs(InfiltratorAbAttr, attacker, null, false, bypassed); + if (bypassed.value) { + return false; + } + } + cancelled.value = true; if (!simulated) { @@ -169,12 +180,18 @@ export class WeakenMoveScreenTag extends ArenaTag { * * @param arena the {@linkcode Arena} where the move is applied. * @param simulated n/a + * @param attacker the attacking {@linkcode Pokemon} * @param moveCategory the attacking move's {@linkcode MoveCategory}. * @param damageMultiplier A {@linkcode NumberHolder} containing the damage multiplier * @returns `true` if the attacking move was weakened; `false` otherwise. */ - override apply(arena: Arena, simulated: boolean, moveCategory: MoveCategory, damageMultiplier: NumberHolder): boolean { + override apply(arena: Arena, simulated: boolean, attacker: Pokemon, moveCategory: MoveCategory, damageMultiplier: NumberHolder): boolean { if (this.weakenedCategories.includes(moveCategory)) { + const bypassed = new BooleanHolder(false); + applyAbAttrs(InfiltratorAbAttr, attacker, null, false, bypassed); + if (bypassed.value) { + return false; + } damageMultiplier.value = arena.scene.currentBattle.double ? 2732 / 4096 : 0.5; return true; } diff --git a/src/data/move.ts b/src/data/move.ts index 309a2d3a7eb..0d9c57bf094 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, 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, 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"; @@ -346,7 +346,11 @@ export default class Move implements Localizable { return false; } - return !user.hasAbility(Abilities.INFILTRATOR) + const bypassed = new Utils.BooleanHolder(false); + // TODO: Allow this to be simulated + applyAbAttrs(InfiltratorAbAttr, user, null, false, bypassed); + + return !bypassed.value && !this.hasFlag(MoveFlags.SOUND_BASED) && !this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE); } @@ -2074,7 +2078,7 @@ export class StatusEffectAttr extends MoveEffectAttr { } } - if (user !== target && target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)) { + if (user !== target && target.isSafeguarded(user)) { if (move.category === MoveCategory.STATUS) { user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) })); } @@ -5161,7 +5165,7 @@ export class ConfuseAttr extends AddBattlerTagAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!this.selfTarget && target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)) { + if (!this.selfTarget && target.isSafeguarded(user)) { if (move.category === MoveCategory.STATUS) { user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) })); } @@ -7598,6 +7602,7 @@ 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)) .ignoresProtect(), new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1) @@ -8028,7 +8033,7 @@ export function initMoves() { .attr(RemoveScreensAttr), new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3) .attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true) - .condition((user, target, move) => !target.status && !target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)), + .condition((user, target, move) => !target.status && !target.isSafeguarded(user)), new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1) .attr(RemoveHeldItemAttr, false), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 0ee879ebf97..94b9fd12540 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -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, ReduceStatusEffectDurationAbAttr, 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 } from "#app/data/ability"; +import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, 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 } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -2610,7 +2610,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** Reduces damage if this Pokemon has a relevant screen (e.g. Light Screen for special attacks) */ const screenMultiplier = new Utils.NumberHolder(1); - this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, simulated, moveCategory, screenMultiplier); + this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, simulated, source, moveCategory, screenMultiplier); /** * For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if: @@ -3352,13 +3352,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - const types = this.getTypes(true, true); - - const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; - if (sourcePokemon && sourcePokemon !== this && this.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) { + if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) { return false; } + const types = this.getTypes(true, true); + switch (effect) { case StatusEffect.POISON: case StatusEffect.TOXIC: @@ -3504,6 +3503,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } + /** + * Checks if this Pokemon is protected by Safeguard + * @param attacker the {@linkcode Pokemon} inflicting status on this Pokemon + * @returns `true` if this Pokemon is protected by Safeguard; `false` otherwise. + */ + isSafeguarded(attacker: Pokemon): boolean { + const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + if (this.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) { + const bypassed = new Utils.BooleanHolder(false); + if (attacker) { + applyAbAttrs(InfiltratorAbAttr, attacker, null, false, bypassed); + } + return !bypassed.value; + } + return false; + } + primeSummonData(summonDataPrimer: PokemonSummonData): void { this.summonDataPrimer = summonDataPrimer; } diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 4c13b883445..2d4b3ce6c6f 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -64,7 +64,8 @@ export class StatStageChangePhase extends PokemonPhase { const cancelled = new BooleanHolder(false); if (!this.selfTarget && stages.value < 0) { - this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, cancelled); + // TODO: add a reference to the source of the stat change to fix Infiltrator interaction + this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, null, false, cancelled); } if (!cancelled.value && !this.selfTarget && stages.value < 0) { diff --git a/src/test/abilities/infiltrator.test.ts b/src/test/abilities/infiltrator.test.ts new file mode 100644 index 00000000000..01c5cef7796 --- /dev/null +++ b/src/test/abilities/infiltrator.test.ts @@ -0,0 +1,107 @@ +import { ArenaTagSide } from "#app/data/arena-tag"; +import { allMoves } from "#app/data/move"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#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("Abilities - Infiltrator", () => { + 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.WATER_GUN, Moves.SPORE, Moves.BABY_DOLL_EYES ]) + .ability(Abilities.INFILTRATOR) + .battleType("single") + .disableCrits() + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingLevel(100) + .enemyLevel(100); + }); + + it.each([ + { effectName: "Light Screen", tagType: ArenaTagType.LIGHT_SCREEN, move: Moves.WATER_GUN }, + { effectName: "Reflect", tagType: ArenaTagType.REFLECT, move: Moves.TACKLE }, + { effectName: "Aurora Veil", tagType: ArenaTagType.AURORA_VEIL, move: Moves.TACKLE } + ])("should bypass the target's $effectName", async ({ tagType, move }) => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + const preScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage; + + game.scene.arena.addTag(tagType, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true); + + const postScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage; + + expect(postScreenDmg).toBe(preScreenDmg); + expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR); + }); + + it("should bypass the target's Safeguard", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true); + + game.move.select(Moves.SPORE); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(enemy.status?.effect).toBe(StatusEffect.SLEEP); + expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR); + }); + + // TODO: fix this interaction to pass this test + it.skip("should bypass the target's Mist", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + game.scene.arena.addTag(ArenaTagType.MIST, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true); + + game.move.select(Moves.BABY_DOLL_EYES); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(enemy.getStatStage(Stat.ATK)).toBe(-1); + expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR); + }); + + it("should bypass the target's Substitute", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.addTag(BattlerTagType.SUBSTITUTE, 1, Moves.NONE, enemy.id); + + game.move.select(Moves.BABY_DOLL_EYES); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(enemy.getStatStage(Stat.ATK)).toBe(-1); + expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR); + }); +}); diff --git a/src/test/moves/aurora_veil.test.ts b/src/test/moves/aurora_veil.test.ts index 243ba3a3269..e68117a2f59 100644 --- a/src/test/moves/aurora_veil.test.ts +++ b/src/test/moves/aurora_veil.test.ts @@ -111,7 +111,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) = const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (defender.scene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side)) { - defender.scene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, false, move.category, multiplierHolder); + defender.scene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, false, attacker, move.category, multiplierHolder); } return move.power * multiplierHolder.value; diff --git a/src/test/moves/light_screen.test.ts b/src/test/moves/light_screen.test.ts index 11b8144bb4e..af14d9273e6 100644 --- a/src/test/moves/light_screen.test.ts +++ b/src/test/moves/light_screen.test.ts @@ -94,7 +94,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) = const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (defender.scene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side)) { - defender.scene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, false, move.category, multiplierHolder); + defender.scene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, false, attacker, move.category, multiplierHolder); } return move.power * multiplierHolder.value; diff --git a/src/test/moves/reflect.test.ts b/src/test/moves/reflect.test.ts index b18b2423895..3bf415ea75c 100644 --- a/src/test/moves/reflect.test.ts +++ b/src/test/moves/reflect.test.ts @@ -94,7 +94,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) = const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (defender.scene.arena.getTagOnSide(ArenaTagType.REFLECT, side)) { - defender.scene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, move.category, multiplierHolder); + defender.scene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, attacker, move.category, multiplierHolder); } return move.power * multiplierHolder.value; From 7066a15ceb2eee2df7ba13778294f198bbee9c53 Mon Sep 17 00:00:00 2001 From: Mason S <132116525+ElizaAlex@users.noreply.github.com> Date: Tue, 22 Oct 2024 02:53:00 -0400 Subject: [PATCH 03/17] [Refactor] Added `BattlerTagLapseType.AFTER_HIT` (#3655) * [Refactor] Added ON_GET_HIT BattlerTagLapseType Adjusted BeakBlastChargingTag and ShellTrapTag to use new lapse type Adjusted MoveEffectPhase to now lapse all tags with the ON_GET_HIT lapse type * [Refactor] Added ON_GET_HIT BattlerTagLapseType Adjusted BeakBlastChargingTag and ShellTrapTag to use new lapse type Adjusted MoveEffectPhase to now lapse all tags with the ON_GET_HIT lapse type * Fix nits * Rename `ON_GET_HIT` to `AFTER_HIT` Change `isOpponentTo` to `isOpponent` * Fix a couple minor nits * Remove single-use function --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/battler-tags.ts | 165 ++++++++++++++++++++------------ src/field/pokemon.ts | 9 ++ src/phases/move-effect-phase.ts | 6 +- 3 files changed, 113 insertions(+), 67 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index e0616c341be..c3b7765d062 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1,29 +1,44 @@ -import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims"; -import { getPokemonNameWithAffix } from "../messages"; -import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; -import { StatusEffect } from "./status-effect"; -import * as Utils from "../utils"; -import { ChargeAttr, MoveFlags, allMoves, MoveCategory, applyMoveAttrs, StatusCategoryOnAllyAttr, HealOnAllyAttr, ConsecutiveUseDoublePowerAttr } from "./move"; -import { Type } from "./type"; -import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs, ProtectStatAbAttr } from "./ability"; -import { TerrainType } from "./terrain"; -import { WeatherType } from "./weather"; -import { allAbilities } from "./ability"; -import { SpeciesFormChangeManualTrigger } from "./pokemon-forms"; -import { Abilities } from "#enums/abilities"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { Moves } from "#enums/moves"; -import { Species } from "#enums/species"; -import i18next from "#app/plugins/i18n"; -import { Stat, type BattleStat, type EffectiveStat, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat"; +import BattleScene from "#app/battle-scene"; +import { + allAbilities, + applyAbAttrs, + BlockNonDirectDamageAbAttr, + FlinchEffectAbAttr, + ProtectStatAbAttr, + ReverseDrainAbAttr +} from "#app/data/ability"; +import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "#app/data/battle-anims"; +import Move, { + allMoves, + applyMoveAttrs, + ChargeAttr, + ConsecutiveUseDoublePowerAttr, + HealOnAllyAttr, + MoveCategory, + MoveFlags, + StatusCategoryOnAllyAttr +} from "#app/data/move"; +import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms"; +import { StatusEffect } from "#app/data/status-effect"; +import { TerrainType } from "#app/data/terrain"; +import { Type } from "#app/data/type"; +import { WeatherType } from "#app/data/weather"; +import Pokemon, { HitResult, MoveResult } from "#app/field/pokemon"; +import { getPokemonNameWithAffix } from "#app/messages"; import { CommonAnimPhase } from "#app/phases/common-anim-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MovePhase } from "#app/phases/move-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; -import { StatStageChangePhase, StatStageChangeCallback } from "#app/phases/stat-stage-change-phase"; -import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; -import BattleScene from "#app/battle-scene"; +import { StatStageChangeCallback, StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import i18next from "#app/plugins/i18n"; +import { BooleanHolder, getFrameMs, NumberHolder, toDmgValue } from "#app/utils"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +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"; export enum BattlerTagLapseType { FAINT, @@ -33,6 +48,7 @@ export enum BattlerTagLapseType { MOVE_EFFECT, TURN_END, HIT, + AFTER_HIT, CUSTOM } @@ -405,7 +421,7 @@ export class RechargingTag extends BattlerTag { */ export class BeakBlastChargingTag extends BattlerTag { constructor() { - super(BattlerTagType.BEAK_BLAST_CHARGING, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], 1, Moves.BEAK_BLAST); + super(BattlerTagType.BEAK_BLAST_CHARGING, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END, BattlerTagLapseType.AFTER_HIT ], 1, Moves.BEAK_BLAST); } onAdd(pokemon: Pokemon): void { @@ -421,16 +437,13 @@ export class BeakBlastChargingTag extends BattlerTag { * to be removed after the source makes a move (or the turn ends, whichever comes first) * @param pokemon {@linkcode Pokemon} the owner of this tag * @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle - * @returns `true` if invoked with the CUSTOM lapse type; `false` otherwise + * @returns `true` if invoked with the `AFTER_HIT` lapse type */ lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { - if (lapseType === BattlerTagLapseType.CUSTOM) { - const effectPhase = pokemon.scene.getCurrentPhase(); - if (effectPhase instanceof MoveEffectPhase) { - const attacker = effectPhase.getPokemon(); - if (effectPhase.move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) { - attacker.trySetStatus(StatusEffect.BURN, true, pokemon); - } + if (lapseType === BattlerTagLapseType.AFTER_HIT) { + const phaseData = getMoveEffectPhaseData(pokemon); + if (phaseData?.move.hasFlag(MoveFlags.MAKES_CONTACT)) { + phaseData.attacker.trySetStatus(StatusEffect.BURN, true, pokemon); } return true; } @@ -444,11 +457,10 @@ export class BeakBlastChargingTag extends BattlerTag { * @see {@link https://bulbapedia.bulbagarden.net/wiki/Shell_Trap_(move) | Shell Trap} */ export class ShellTrapTag extends BattlerTag { - public activated: boolean; + public activated: boolean = false; constructor() { - super(BattlerTagType.SHELL_TRAP, BattlerTagLapseType.TURN_END, 1); - this.activated = false; + super(BattlerTagType.SHELL_TRAP, [ BattlerTagLapseType.TURN_END, BattlerTagLapseType.AFTER_HIT ], 1); } onAdd(pokemon: Pokemon): void { @@ -459,25 +471,33 @@ export class ShellTrapTag extends BattlerTag { * "Activates" the shell trap, causing the tag owner to move next. * @param pokemon {@linkcode Pokemon} the owner of this tag * @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle - * @returns `true` if invoked with the `CUSTOM` lapse type; `false` otherwise + * @returns `true` if invoked with the `AFTER_HIT` lapse type */ lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { - if (lapseType === BattlerTagLapseType.CUSTOM) { - const shellTrapPhaseIndex = pokemon.scene.phaseQueue.findIndex( - phase => phase instanceof MovePhase && phase.pokemon === pokemon - ); - const firstMovePhaseIndex = pokemon.scene.phaseQueue.findIndex( - phase => phase instanceof MovePhase - ); + if (lapseType === BattlerTagLapseType.AFTER_HIT) { + const phaseData = getMoveEffectPhaseData(pokemon); - if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) { - const shellTrapMovePhase = pokemon.scene.phaseQueue.splice(shellTrapPhaseIndex, 1)[0]; - pokemon.scene.prependToPhase(shellTrapMovePhase, MovePhase); + // Trap should only be triggered by opponent's Physical moves + if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) { + const shellTrapPhaseIndex = pokemon.scene.phaseQueue.findIndex( + phase => phase instanceof MovePhase && phase.pokemon === pokemon + ); + const firstMovePhaseIndex = pokemon.scene.phaseQueue.findIndex( + phase => phase instanceof MovePhase + ); + + // Only shift MovePhase timing if it's not already next up + if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) { + const shellTrapMovePhase = pokemon.scene.phaseQueue.splice(shellTrapPhaseIndex, 1)[0]; + pokemon.scene.prependToPhase(shellTrapMovePhase, MovePhase); + } + + this.activated = true; } - this.activated = true; return true; } + return super.lapse(pokemon, lapseType); } } @@ -641,7 +661,7 @@ export class ConfusedTag extends BattlerTag { if (pokemon.randSeedInt(3) === 0) { const atk = pokemon.getEffectiveStat(Stat.ATK); const def = pokemon.getEffectiveStat(Stat.DEF); - const damage = Utils.toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedIntRange(85, 100) / 100)); + const damage = toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedIntRange(85, 100) / 100)); pokemon.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself")); pokemon.damageAndUpdate(damage); pokemon.battleData.hitCount++; @@ -812,13 +832,13 @@ export class SeedTag extends BattlerTag { if (ret) { const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex); if (source) { - const cancelled = new Utils.BooleanHolder(false); + const cancelled = new BooleanHolder(false); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); if (!cancelled.value) { pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, source.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.LEECH_SEED)); - const damage = pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 8)); + const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8)); const reverseDrain = pokemon.hasAbilityWithAttr(ReverseDrainAbAttr, false); pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, source.getBattlerIndex(), !reverseDrain ? damage : damage * -1, @@ -860,11 +880,11 @@ export class NightmareTag extends BattlerTag { pokemon.scene.queueMessage(i18next.t("battlerTags:nightmareLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.CURSE)); // TODO: Update animation type - const cancelled = new Utils.BooleanHolder(false); + const cancelled = new BooleanHolder(false); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); if (!cancelled.value) { - pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 4)); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4)); } } @@ -1004,7 +1024,7 @@ export class IngrainTag extends TrappedTag { new PokemonHealPhase( pokemon.scene, pokemon.getBattlerIndex(), - Utils.toDmgValue(pokemon.getMaxHp() / 16), + toDmgValue(pokemon.getMaxHp() / 16), i18next.t("battlerTags:ingrainLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), true ) @@ -1067,7 +1087,7 @@ export class AquaRingTag extends BattlerTag { new PokemonHealPhase( pokemon.scene, pokemon.getBattlerIndex(), - Utils.toDmgValue(pokemon.getMaxHp() / 16), + toDmgValue(pokemon.getMaxHp() / 16), i18next.t("battlerTags:aquaRingLapse", { moveName: this.getMoveName(), pokemonName: getPokemonNameWithAffix(pokemon) @@ -1161,11 +1181,11 @@ export abstract class DamagingTrapTag extends TrappedTag { ); pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, this.commonAnim)); - const cancelled = new Utils.BooleanHolder(false); + const cancelled = new BooleanHolder(false); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); if (!cancelled.value) { - pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 8)); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8)); } } @@ -1356,7 +1376,7 @@ export class ContactDamageProtectedTag extends ProtectedTag { if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) { const attacker = effectPhase.getPokemon(); if (!attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { - attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); + attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); } } } @@ -1709,7 +1729,7 @@ export class SemiInvulnerableTag extends BattlerTag { onRemove(pokemon: Pokemon): void { // Wait 2 frames before setting visible for battle animations that don't immediately show the sprite invisible pokemon.scene.tweens.addCounter({ - duration: Utils.getFrameMs(2), + duration: getFrameMs(2), onComplete: () => pokemon.setVisible(true) }); } @@ -1860,12 +1880,12 @@ export class SaltCuredTag extends BattlerTag { if (ret) { pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.SALT_CURE)); - const cancelled = new Utils.BooleanHolder(false); + const cancelled = new BooleanHolder(false); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); if (!cancelled.value) { const pokemonSteelOrWater = pokemon.isOfType(Type.STEEL) || pokemon.isOfType(Type.WATER); - pokemon.damageAndUpdate(Utils.toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8)); + pokemon.damageAndUpdate(toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8)); pokemon.scene.queueMessage( i18next.t("battlerTags:saltCuredLapse", { @@ -1907,11 +1927,11 @@ export class CursedTag extends BattlerTag { if (ret) { pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.SALT_CURE)); - const cancelled = new Utils.BooleanHolder(false); + const cancelled = new BooleanHolder(false); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); if (!cancelled.value) { - pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 4)); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4)); pokemon.scene.queueMessage(i18next.t("battlerTags:cursedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); } } @@ -2173,7 +2193,7 @@ export class GulpMissileTag extends BattlerTag { return true; } - const cancelled = new Utils.BooleanHolder(false); + const cancelled = new BooleanHolder(false); applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled); if (!cancelled.value) { @@ -2289,7 +2309,7 @@ export class HealBlockTag extends MoveRestrictionBattlerTag { * @returns `true` if the move cannot be used because the target is an ally */ override isMoveTargetRestricted(move: Moves, user: Pokemon, target: Pokemon) { - const moveCategory = new Utils.IntegerHolder(allMoves[move].category); + const moveCategory = new NumberHolder(allMoves[move].category); applyMoveAttrs(StatusCategoryOnAllyAttr, user, target, allMoves[move], moveCategory); if (allMoves[move].hasAttr(HealOnAllyAttr) && moveCategory.value === MoveCategory.STATUS ) { return true; @@ -2506,7 +2526,7 @@ export class MysteryEncounterPostSummonTag extends BattlerTag { const ret = super.lapse(pokemon, lapseType); if (lapseType === BattlerTagLapseType.CUSTOM) { - const cancelled = new Utils.BooleanHolder(false); + const cancelled = new BooleanHolder(false); applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); if (!cancelled.value) { if (pokemon.mysteryEncounterBattleEffects) { @@ -2955,3 +2975,22 @@ export function loadBattlerTag(source: BattlerTag | any): BattlerTag { tag.loadTag(source); return tag; } + +/** + * Helper function to verify that the current phase is a MoveEffectPhase and provide quick access to commonly used fields + * + * @param pokemon {@linkcode Pokemon} The Pokémon used to access the current phase + * @returns null if current phase is not MoveEffectPhase, otherwise Object containing the {@linkcode MoveEffectPhase}, and its + * corresponding {@linkcode Move} and user {@linkcode Pokemon} + */ +function getMoveEffectPhaseData(pokemon: Pokemon): {phase: MoveEffectPhase, attacker: Pokemon, move: Move} | null { + const phase = pokemon.scene.getCurrentPhase(); + if (phase instanceof MoveEffectPhase) { + return { + phase : phase, + attacker : phase.getPokemon(), + move : phase.move.getMove() + }; + } + return null; +} diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 94b9fd12540..d320880c52a 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2290,6 +2290,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.levelExp = this.exp - getLevelTotalExp(this.level, this.species.growthRate); } + /** + * Compares if `this` and {@linkcode target} are on the same team. + * @param target the {@linkcode Pokemon} to compare against. + * @returns `true` if the two pokemon are allies, `false` otherwise + */ + public isOpponent(target: Pokemon): boolean { + return this.isPlayer() !== target.isPlayer(); + } + getOpponent(targetIndex: integer): Pokemon | null { const ret = this.getOpponents()[targetIndex]; if (ret.summonData) { diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index dc880f85e23..8d1a255d268 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -280,10 +280,8 @@ export class MoveEffectPhase extends PokemonPhase { if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); } - target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING); - if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) { - target.lapseTag(BattlerTagType.SHELL_TRAP); - } + target.lapseTags(BattlerTagLapseType.AFTER_HIT); + })).then(() => { // Apply the user's post-attack ability effects applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { From 181f59882a02fcab129c45672cc2d397aaff3049 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:37:13 -0700 Subject: [PATCH 04/17] [P2] Fix Early Bird (#4632) * Fix Early Bird, add tests * Update tsdocs for Early Bird's `AbAttr` Rename `turnCount` to `toxicTurnCount` and `turnsRemaining` to `sleepTurnsRemaining` in `status-effect.ts` * Fix Toxic Orb test * Redundant code :despair: * Fix status override to set the number of sleep turns --- src/data/ability.ts | 19 ++++- src/data/move.ts | 16 ++-- src/data/status-effect.ts | 24 +++--- src/field/pokemon.ts | 25 +++--- src/phases/move-phase.ts | 7 +- src/phases/obtain-status-effect-phase.ts | 14 ++-- src/phases/post-summon-phase.ts | 2 +- src/phases/post-turn-status-effect-phase.ts | 2 +- src/system/pokemon-data.ts | 2 +- src/test/abilities/early_bird.test.ts | 93 +++++++++++++++++++++ src/test/abilities/magic_guard.test.ts | 4 +- src/test/data/status-effect.test.ts | 66 ++++++++++++++- src/test/items/toxic_orb.test.ts | 23 ++--- src/test/moves/nightmare.test.ts | 10 +-- src/test/moves/will_o_wisp.test.ts | 53 ++++++++++++ 15 files changed, 292 insertions(+), 68 deletions(-) create mode 100644 src/test/abilities/early_bird.test.ts create mode 100644 src/test/moves/will_o_wisp.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 0d5cf2751ce..ebdd5105bb4 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4200,6 +4200,11 @@ export class RedirectTypeMoveAbAttr extends RedirectMoveAbAttr { export class BlockRedirectAbAttr extends AbAttr { } +/** + * Used by Early Bird, makes the pokemon wake up faster + * @param statusEffect - The {@linkcode StatusEffect} to check for + * @see {@linkcode apply} + */ export class ReduceStatusEffectDurationAbAttr extends AbAttr { private statusEffect: StatusEffect; @@ -4209,9 +4214,19 @@ export class ReduceStatusEffectDurationAbAttr extends AbAttr { this.statusEffect = statusEffect; } - apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + /** + * Reduces the number of sleep turns remaining by an extra 1 when applied + * @param args - The args passed to the `AbAttr`: + * - `[0]` - The {@linkcode StatusEffect} of the Pokemon + * - `[1]` - The number of turns remaining until the status is healed + * @returns `true` if the ability was applied + */ + apply(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: Utils.BooleanHolder, args: any[]): boolean { + if (!(args[1] instanceof Utils.NumberHolder)) { + return false; + } if (args[0] === this.statusEffect) { - (args[1] as Utils.IntegerHolder).value = Utils.toDmgValue((args[1] as Utils.IntegerHolder).value / 2); + args[1].value -= 1; return true; } diff --git a/src/data/move.ts b/src/data/move.ts index 0d9c57bf094..ec25844909e 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2050,15 +2050,15 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr { export class StatusEffectAttr extends MoveEffectAttr { public effect: StatusEffect; - public cureTurn: integer | null; - public overrideStatus: boolean; + public turnsRemaining?: number; + public overrideStatus: boolean = false; - constructor(effect: StatusEffect, selfTarget?: boolean, cureTurn?: integer, overrideStatus?: boolean) { + constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) { super(selfTarget, MoveEffectTrigger.HIT); this.effect = effect; - this.cureTurn = cureTurn!; // TODO: is this bang correct? - this.overrideStatus = !!overrideStatus; + this.turnsRemaining = turnsRemaining; + this.overrideStatus = overrideStatus; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -2085,7 +2085,7 @@ export class StatusEffectAttr extends MoveEffectAttr { return false; } if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0)) - && pokemon.trySetStatus(this.effect, true, user, this.cureTurn)) { + && pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining)) { applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect); return true; } @@ -2102,8 +2102,8 @@ export class StatusEffectAttr extends MoveEffectAttr { export class MultiStatusEffectAttr extends StatusEffectAttr { public effects: StatusEffect[]; - constructor(effects: StatusEffect[], selfTarget?: boolean, cureTurn?: integer, overrideStatus?: boolean) { - super(effects[0], selfTarget, cureTurn, overrideStatus); + constructor(effects: StatusEffect[], selfTarget?: boolean, turnsRemaining?: number, overrideStatus?: boolean) { + super(effects[0], selfTarget, turnsRemaining, overrideStatus); this.effects = effects; } diff --git a/src/data/status-effect.ts b/src/data/status-effect.ts index 4319985f43a..56e754ac407 100644 --- a/src/data/status-effect.ts +++ b/src/data/status-effect.ts @@ -1,4 +1,4 @@ -import * as Utils from "../utils"; +import { randIntRange } from "#app/utils"; import { StatusEffect } from "#enums/status-effect"; import i18next, { ParseKeys } from "i18next"; @@ -6,17 +6,21 @@ export { StatusEffect }; export class Status { public effect: StatusEffect; - public turnCount: integer; - public cureTurn: integer | null; + /** Toxic damage is `1/16 max HP * toxicTurnCount` */ + public toxicTurnCount: number = 0; + public sleepTurnsRemaining?: number; - constructor(effect: StatusEffect, turnCount: integer = 0, cureTurn?: integer) { + constructor(effect: StatusEffect, toxicTurnCount: number = 0, sleepTurnsRemaining?: number) { this.effect = effect; - this.turnCount = turnCount === undefined ? 0 : turnCount; - this.cureTurn = cureTurn!; // TODO: is this bang correct? + this.toxicTurnCount = toxicTurnCount; + this.sleepTurnsRemaining = sleepTurnsRemaining; } incrementTurn(): void { - this.turnCount++; + this.toxicTurnCount++; + if (this.sleepTurnsRemaining) { + this.sleepTurnsRemaining--; + } } isPostTurn(): boolean { @@ -107,7 +111,7 @@ export function getStatusEffectCatchRateMultiplier(statusEffect: StatusEffect): * Returns a random non-volatile StatusEffect */ export function generateRandomStatusEffect(): StatusEffect { - return Utils.randIntRange(1, 6); + return randIntRange(1, 6); } /** @@ -123,7 +127,7 @@ export function getRandomStatusEffect(statusEffectA: StatusEffect, statusEffectB return statusEffectA; } - return Utils.randIntRange(0, 2) ? statusEffectA : statusEffectB; + return randIntRange(0, 2) ? statusEffectA : statusEffectB; } /** @@ -140,7 +144,7 @@ export function getRandomStatus(statusA: Status | null, statusB: Status | null): } - return Utils.randIntRange(0, 2) ? statusA : statusB; + return randIntRange(0, 2) ? statusA : statusB; } /** diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d320880c52a..a3d7429ed9b 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -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, ReduceStatusEffectDurationAbAttr, 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 } 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 } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -3430,7 +3430,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return true; } - trySetStatus(effect: StatusEffect | undefined, asPhase: boolean = false, sourcePokemon: Pokemon | null = null, cureTurn: integer | null = 0, sourceText: string | null = null): boolean { + trySetStatus(effect?: StatusEffect, asPhase: boolean = false, sourcePokemon: Pokemon | null = null, turnsRemaining: number = 0, sourceText: string | null = null): boolean { if (!this.canSetStatus(effect, asPhase, false, sourcePokemon)) { return false; } @@ -3444,15 +3444,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } if (asPhase) { - this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText, sourcePokemon)); + this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, turnsRemaining, sourceText, sourcePokemon)); return true; } - let statusCureTurn: Utils.IntegerHolder; + let sleepTurnsRemaining: Utils.NumberHolder; if (effect === StatusEffect.SLEEP) { - statusCureTurn = new Utils.IntegerHolder(this.randSeedIntRange(2, 4)); - applyAbAttrs(ReduceStatusEffectDurationAbAttr, this, null, false, effect, statusCureTurn); + sleepTurnsRemaining = new Utils.NumberHolder(this.randSeedIntRange(2, 4)); this.setFrameRate(4); @@ -3472,9 +3471,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - statusCureTurn = statusCureTurn!; // tell TS compiler it's defined + sleepTurnsRemaining = sleepTurnsRemaining!; // tell TS compiler it's defined effect = effect!; // If `effect` is undefined then `trySetStatus()` will have already returned early via the `canSetStatus()` call - this.status = new Status(effect, 0, statusCureTurn?.value); + this.status = new Status(effect, 0, sleepTurnsRemaining?.value); if (effect !== StatusEffect.FAINT) { this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true); @@ -4001,7 +4000,7 @@ export class PlayerPokemon extends Pokemon { super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource); if (Overrides.STATUS_OVERRIDE) { - this.status = new Status(Overrides.STATUS_OVERRIDE); + this.status = new Status(Overrides.STATUS_OVERRIDE, 0, 4); } if (Overrides.SHINY_OVERRIDE) { @@ -4481,7 +4480,7 @@ export class EnemyPokemon extends Pokemon { } if (Overrides.OPP_STATUS_OVERRIDE) { - this.status = new Status(Overrides.OPP_STATUS_OVERRIDE); + this.status = new Status(Overrides.OPP_STATUS_OVERRIDE, 0, 4); } if (Overrides.OPP_GENDER_OVERRIDE) { @@ -4490,9 +4489,11 @@ export class EnemyPokemon extends Pokemon { const speciesId = this.species.speciesId; - if (speciesId in Overrides.OPP_FORM_OVERRIDES + if ( + speciesId in Overrides.OPP_FORM_OVERRIDES && Overrides.OPP_FORM_OVERRIDES[speciesId] - && this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]]) { + && this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]] + ) { this.formIndex = Overrides.OPP_FORM_OVERRIDES[speciesId] ?? 0; } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 0af61918636..e9d8887e9cb 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -1,6 +1,6 @@ import { BattlerIndex } from "#app/battle"; import BattleScene from "#app/battle-scene"; -import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr } from "#app/data/ability"; +import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability"; import { CommonAnim } from "#app/data/battle-anims"; import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags"; import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, ChargeAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move"; @@ -175,7 +175,10 @@ export class MovePhase extends BattlePhase { break; case StatusEffect.SLEEP: applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove()); - healed = this.pokemon.status.turnCount === this.pokemon.status.cureTurn; + const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0); + applyAbAttrs(ReduceStatusEffectDurationAbAttr, this.pokemon, null, false, this.pokemon.status.effect, turnsRemaining); + this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value; + healed = this.pokemon.status.sleepTurnsRemaining <= 0; activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP); this.cancelled = activated; break; diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index c396fa7ba59..01384b932cb 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -8,26 +8,26 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonPhase } from "./pokemon-phase"; export class ObtainStatusEffectPhase extends PokemonPhase { - private statusEffect?: StatusEffect | undefined; - private cureTurn?: integer | null; + private statusEffect?: StatusEffect; + private turnsRemaining?: number; private sourceText?: string | null; private sourcePokemon?: Pokemon | null; - constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string | null, sourcePokemon?: Pokemon | null) { + constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, turnsRemaining?: number, sourceText?: string | null, sourcePokemon?: Pokemon | null) { super(scene, battlerIndex); this.statusEffect = statusEffect; - this.cureTurn = cureTurn; + this.turnsRemaining = turnsRemaining; this.sourceText = sourceText; - this.sourcePokemon = sourcePokemon; // For tracking which Pokemon caused the status effect + this.sourcePokemon = sourcePokemon; } start() { const pokemon = this.getPokemon(); if (pokemon && !pokemon.status) { if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { - if (this.cureTurn) { - pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct? + if (this.turnsRemaining) { + pokemon.status!.sleepTurnsRemaining = this.turnsRemaining; } pokemon.updateInfo(true); new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => { diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 617bb8b1cfe..3db98d9926c 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -18,7 +18,7 @@ export class PostSummonPhase extends PokemonPhase { const pokemon = this.getPokemon(); if (pokemon.status?.effect === StatusEffect.TOXIC) { - pokemon.status.turnCount = 0; + pokemon.status.toxicTurnCount = 0; } this.scene.arena.applyTags(ArenaTrapTag, false, pokemon); diff --git a/src/phases/post-turn-status-effect-phase.ts b/src/phases/post-turn-status-effect-phase.ts index 06681b733f0..2efd992a2b5 100644 --- a/src/phases/post-turn-status-effect-phase.ts +++ b/src/phases/post-turn-status-effect-phase.ts @@ -30,7 +30,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { damage.value = Math.max(pokemon.getMaxHp() >> 3, 1); break; case StatusEffect.TOXIC: - damage.value = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.turnCount), 1); + damage.value = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.toxicTurnCount), 1); break; case StatusEffect.BURN: damage.value = Math.max(pokemon.getMaxHp() >> 4, 1); diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index cddc5798872..e681c995b26 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -137,7 +137,7 @@ export default class PokemonData { this.moveset = (source.moveset || [ new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL) ]).filter(m => m).map((m: any) => new PokemonMove(m.moveId, m.ppUsed, m.ppUp)); if (!forHistory) { this.status = source.status - ? new Status(source.status.effect, source.status.turnCount, source.status.cureTurn) + ? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining) : null; } diff --git a/src/test/abilities/early_bird.test.ts b/src/test/abilities/early_bird.test.ts new file mode 100644 index 00000000000..a69290fa1e4 --- /dev/null +++ b/src/test/abilities/early_bird.test.ts @@ -0,0 +1,93 @@ +import { Status } from "#app/data/status-effect"; +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 Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Early Bird", () => { + 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.REST, Moves.BELLY_DRUM, Moves.SPLASH ]) + .ability(Abilities.EARLY_BIRD) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("reduces Rest's sleep time to 1 turn", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.BELLY_DRUM); + await game.toNextTurn(); + game.move.select(Moves.REST); + await game.toNextTurn(); + + expect(player.status?.effect).toBe(StatusEffect.SLEEP); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(player.status?.effect).toBe(StatusEffect.SLEEP); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(player.status?.effect).toBeUndefined(); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + }); + + it("reduces 3-turn sleep to 1 turn", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const player = game.scene.getPlayerPokemon()!; + player.status = new Status(StatusEffect.SLEEP, 0, 4); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(player.status?.effect).toBe(StatusEffect.SLEEP); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(player.status?.effect).toBeUndefined(); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + }); + + it("reduces 1-turn sleep to 0 turns", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const player = game.scene.getPlayerPokemon()!; + player.status = new Status(StatusEffect.SLEEP, 0, 2); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(player.status?.effect).toBeUndefined(); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + }); +}); diff --git a/src/test/abilities/magic_guard.test.ts b/src/test/abilities/magic_guard.test.ts index 614f983e76e..8075eac66f2 100644 --- a/src/test/abilities/magic_guard.test.ts +++ b/src/test/abilities/magic_guard.test.ts @@ -150,7 +150,7 @@ describe("Abilities - Magic Guard", () => { const enemyPokemon = game.scene.getEnemyPokemon()!; - const toxicStartCounter = enemyPokemon.status!.turnCount; + const toxicStartCounter = enemyPokemon.status!.toxicTurnCount; //should be 0 await game.phaseInterceptor.to(TurnEndPhase); @@ -162,7 +162,7 @@ describe("Abilities - Magic Guard", () => { * - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5 */ expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); - expect(enemyPokemon.status!.turnCount).toBeGreaterThan(toxicStartCounter); + expect(enemyPokemon.status!.toxicTurnCount).toBeGreaterThan(toxicStartCounter); expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5); } ); diff --git a/src/test/data/status-effect.test.ts b/src/test/data/status-effect.test.ts index bca3bd21c70..8b37da45d8d 100644 --- a/src/test/data/status-effect.test.ts +++ b/src/test/data/status-effect.test.ts @@ -1,4 +1,5 @@ import { + Status, StatusEffect, getStatusEffectActivationText, getStatusEffectDescriptor, @@ -6,14 +7,19 @@ import { getStatusEffectObtainText, getStatusEffectOverlapText, } from "#app/data/status-effect"; +import { MoveResult } from "#app/field/pokemon"; +import GameManager from "#app/test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; import { mockI18next } from "#test/utils/testUtils"; import i18next from "i18next"; -import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const pokemonName = "PKM"; const sourceText = "SOURCE"; -describe("status-effect", () => { +describe("Status Effect Messages", () => { beforeAll(() => { i18next.init(); }); @@ -299,3 +305,59 @@ describe("status-effect", () => { vi.resetAllMocks(); }); }); + +describe("Status Effects - Sleep", () => { + 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("should last the appropriate number of turns", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const player = game.scene.getPlayerPokemon()!; + player.status = new Status(StatusEffect.SLEEP, 0, 4); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(player.status.effect).toBe(StatusEffect.SLEEP); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(player.status.effect).toBe(StatusEffect.SLEEP); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(player.status.effect).toBe(StatusEffect.SLEEP); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(player.status?.effect).toBeUndefined(); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + }); +}); diff --git a/src/test/items/toxic_orb.test.ts b/src/test/items/toxic_orb.test.ts index 63c7b6245f5..583e302126c 100644 --- a/src/test/items/toxic_orb.test.ts +++ b/src/test/items/toxic_orb.test.ts @@ -7,8 +7,6 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -const TIMEOUT = 20 * 1000; - describe("Items - Toxic orb", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -27,10 +25,10 @@ describe("Items - Toxic orb", () => { game = new GameManager(phaserGame); game.override .battleType("single") - .enemySpecies(Species.RATTATA) + .enemySpecies(Species.MAGIKARP) .ability(Abilities.BALL_FETCH) .enemyAbility(Abilities.BALL_FETCH) - .moveset([ Moves.SPLASH ]) + .moveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH) .startingHeldItems([{ name: "TOXIC_ORB", @@ -39,22 +37,19 @@ describe("Items - Toxic orb", () => { vi.spyOn(i18next, "t"); }); - it("badly poisons the holder", async () => { - await game.classicMode.startBattle([ Species.MIGHTYENA ]); + it("should badly poison the holder", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); - const player = game.scene.getPlayerField()[0]; + const player = game.scene.getPlayerPokemon()!; + expect(player.getHeldItems()[0].type.id).toBe("TOXIC_ORB"); game.move.select(Moves.SPLASH); await game.phaseInterceptor.to("TurnEndPhase"); - // Toxic orb should trigger here - await game.phaseInterceptor.run("MessagePhase"); + await game.phaseInterceptor.to("MessagePhase"); expect(i18next.t).toHaveBeenCalledWith("statusEffect:toxic.obtainSource", expect.anything()); - await game.toNextTurn(); - expect(player.status?.effect).toBe(StatusEffect.TOXIC); - // Damage should not have ticked yet. - expect(player.status?.turnCount).toBe(0); - }, TIMEOUT); + expect(player.status?.toxicTurnCount).toBe(0); + }); }); diff --git a/src/test/moves/nightmare.test.ts b/src/test/moves/nightmare.test.ts index 61b133a3280..f4c485ff1b4 100644 --- a/src/test/moves/nightmare.test.ts +++ b/src/test/moves/nightmare.test.ts @@ -1,12 +1,10 @@ -import { CommandPhase } from "#app/phases/command-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; +import { StatusEffect } from "#app/data/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"; -import { StatusEffect } from "#app/data/status-effect"; describe("Moves - Nightmare", () => { let phaserGame: Phaser.Game; @@ -39,16 +37,16 @@ describe("Moves - Nightmare", () => { const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyMaxHP = enemyPokemon.hp; + game.move.select(Moves.NIGHTMARE); - await game.phaseInterceptor.to(TurnInitPhase); + await game.toNextTurn(); expect(enemyPokemon.hp).toBe(enemyMaxHP - Math.floor(enemyMaxHP / 4)); // take a second turn to make sure damage occurs again - await game.phaseInterceptor.to(CommandPhase); game.move.select(Moves.SPLASH); + await game.toNextTurn(); - await game.phaseInterceptor.to(TurnInitPhase); expect(enemyPokemon.hp).toBe(enemyMaxHP - Math.floor(enemyMaxHP / 4) - Math.floor(enemyMaxHP / 4)); }); }); diff --git a/src/test/moves/will_o_wisp.test.ts b/src/test/moves/will_o_wisp.test.ts new file mode 100644 index 00000000000..39729d331ad --- /dev/null +++ b/src/test/moves/will_o_wisp.test.ts @@ -0,0 +1,53 @@ +import { BattlerIndex } from "#app/battle"; +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"; + +describe("Moves - Will-O-Wisp", () => { + 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.WILL_O_WISP, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should burn the opponent", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.WILL_O_WISP); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.move.forceHit(); + await game.toNextTurn(); + + expect(enemy.status?.effect).toBe(StatusEffect.BURN); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(enemy.status?.effect).toBe(StatusEffect.BURN); + }); +}); From 5e7f2042fcba5ea38de0f6d7298499d93019a9fc Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:05:37 -0700 Subject: [PATCH 05/17] [UI][QoL] Cursor defaults to Fight at the start of each new wave (#4666) * cursors are dumb * update * fixed? * maybe solution * fix in! * Possible cursor fixes --------- Co-authored-by: frutescens Co-authored-by: Opaque02 <66582645+Opaque02@users.noreply.github.com> --- src/phases/command-phase.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index e6f2eb69ff3..6d4d46c51c9 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -30,6 +30,15 @@ export class CommandPhase extends FieldPhase { start() { super.start(); + const commandUiHandler = this.scene.ui.handlers[Mode.COMMAND]; + if (commandUiHandler) { + if (this.scene.currentBattle.turn === 1 || commandUiHandler.getCursor() === Command.POKEMON) { + commandUiHandler.setCursor(Command.FIGHT); + } else { + commandUiHandler.setCursor(commandUiHandler.getCursor()); + } + } + if (this.fieldIndex) { // If we somehow are attempting to check the right pokemon but there's only one pokemon out // Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching From 96e5f2d763d3bf225ae685675f54b1fb8aa5ae37 Mon Sep 17 00:00:00 2001 From: damocleas Date: Tue, 22 Oct 2024 21:07:28 -0400 Subject: [PATCH 06/17] Starmobile Stat Adjustments (#4704) Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> --- src/data/pokemon-species.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 947ac939989..96d1eb430fb 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -2580,11 +2580,11 @@ export function initSpecies() { new PokemonSpecies(Species.VAROOM, 9, false, false, false, "Single-Cyl Pokémon", Type.STEEL, Type.POISON, 1, 35, Abilities.OVERCOAT, Abilities.NONE, Abilities.SLOW_START, 300, 45, 70, 63, 30, 45, 47, 190, 50, 60, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.REVAVROOM, 9, false, false, false, "Multi-Cyl Pokémon", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, GrowthRate.MEDIUM_FAST, 50, false, false, new PokemonForm("Normal", "", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, false, null, true), - new PokemonForm("Segin Starmobile", "segin-starmobile", Type.STEEL, Type.DARK, 1.8, 240, Abilities.INTIMIDATE, Abilities.NONE, Abilities.INTIMIDATE, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), - new PokemonForm("Schedar Starmobile", "schedar-starmobile", Type.STEEL, Type.FIRE, 1.8, 240, Abilities.SPEED_BOOST, Abilities.NONE, Abilities.SPEED_BOOST, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), - new PokemonForm("Navi Starmobile", "navi-starmobile", Type.STEEL, Type.POISON, 1.8, 240, Abilities.TOXIC_DEBRIS, Abilities.NONE, Abilities.TOXIC_DEBRIS, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), - new PokemonForm("Ruchbah Starmobile", "ruchbah-starmobile", Type.STEEL, Type.FAIRY, 1.8, 240, Abilities.MISTY_SURGE, Abilities.NONE, Abilities.MISTY_SURGE, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), - new PokemonForm("Caph Starmobile", "caph-starmobile", Type.STEEL, Type.FIGHTING, 1.8, 240, Abilities.STAMINA, Abilities.NONE, Abilities.STAMINA, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), + new PokemonForm("Segin Starmobile", "segin-starmobile", Type.STEEL, Type.DARK, 1.8, 240, Abilities.INTIMIDATE, Abilities.NONE, Abilities.INTIMIDATE, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175), + new PokemonForm("Schedar Starmobile", "schedar-starmobile", Type.STEEL, Type.FIRE, 1.8, 240, Abilities.SPEED_BOOST, Abilities.NONE, Abilities.SPEED_BOOST, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175), + new PokemonForm("Navi Starmobile", "navi-starmobile", Type.STEEL, Type.POISON, 1.8, 240, Abilities.TOXIC_DEBRIS, Abilities.NONE, Abilities.TOXIC_DEBRIS, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175), + new PokemonForm("Ruchbah Starmobile", "ruchbah-starmobile", Type.STEEL, Type.FAIRY, 1.8, 240, Abilities.MISTY_SURGE, Abilities.NONE, Abilities.MISTY_SURGE, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175), + new PokemonForm("Caph Starmobile", "caph-starmobile", Type.STEEL, Type.FIGHTING, 1.8, 240, Abilities.STAMINA, Abilities.NONE, Abilities.STAMINA, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175), ), new PokemonSpecies(Species.CYCLIZAR, 9, false, false, false, "Mount Pokémon", Type.DRAGON, Type.NORMAL, 1.6, 63, Abilities.SHED_SKIN, Abilities.NONE, Abilities.REGENERATOR, 501, 70, 95, 65, 85, 65, 121, 190, 50, 175, GrowthRate.MEDIUM_SLOW, 50, false), new PokemonSpecies(Species.ORTHWORM, 9, false, false, false, "Earthworm Pokémon", Type.STEEL, null, 2.5, 310, Abilities.EARTH_EATER, Abilities.NONE, Abilities.SAND_VEIL, 480, 70, 85, 145, 60, 55, 65, 25, 50, 240, GrowthRate.SLOW, 50, false), From 0fe57b44b5ca2fcc934759147a29b46e07b0f76b Mon Sep 17 00:00:00 2001 From: Blitzy <118096277+Blitz425@users.noreply.github.com> Date: Tue, 22 Oct 2024 20:13:10 -0500 Subject: [PATCH 07/17] [Balance] Add Exclusive Moves from Prior Evolutions via Memory Mushroom (#4681) --- src/data/balance/pokemon-level-moves.ts | 479 +++++++++++++++++++----- 1 file changed, 389 insertions(+), 90 deletions(-) diff --git a/src/data/balance/pokemon-level-moves.ts b/src/data/balance/pokemon-level-moves.ts index b5608093df2..53f547c4504 100644 --- a/src/data/balance/pokemon-level-moves.ts +++ b/src/data/balance/pokemon-level-moves.ts @@ -93,6 +93,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.GROWL ], [ 1, Moves.EMBER ], [ 1, Moves.SMOKESCREEN ], + [ 1, Moves.FIRE_SPIN ], // Previous Stage Move [ 12, Moves.DRAGON_BREATH ], [ 19, Moves.FIRE_FANG ], [ 24, Moves.SLASH ], @@ -174,6 +175,9 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.METAPOD]: [ [ EVOLVE_MOVE, Moves.HARDEN ], + [ RELEARN_MOVE, Moves.TACKLE ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STRING_SHOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.HARDEN ], ], [Species.BUTTERFREE]: [ @@ -203,10 +207,17 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.KAKUNA]: [ [ EVOLVE_MOVE, Moves.HARDEN ], + [ RELEARN_MOVE, Moves.POISON_STING ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STRING_SHOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.HARDEN ], ], [Species.BEEDRILL]: [ [ EVOLVE_MOVE, Moves.TWINEEDLE ], + [ 1, Moves.POISON_STING ], // Previous Stage Move + [ 1, Moves.STRING_SHOT ], // Previous Stage Move + [ 1, Moves.HARDEN ], // Previous Stage Move + [ 1, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.FURY_ATTACK ], [ 11, Moves.FURY_CUTTER ], [ 14, Moves.RAGE ], @@ -454,6 +465,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.POISON_STING ], [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.CRUSH_CLAW ], + [ 1, Moves.AGILITY ], // Previous Stage Move [ 9, Moves.ROLLOUT ], [ 12, Moves.FURY_CUTTER ], [ 15, Moves.RAPID_SPIN ], @@ -961,6 +973,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], [ 1, Moves.FOCUS_ENERGY ], + [ 1, Moves.COVET ], // Previous Stage Move [ 1, Moves.FLING ], [ 5, Moves.FURY_SWIPES ], [ 8, Moves.LOW_KICK ], @@ -1044,10 +1057,6 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.POLIWRATH]: [ [ EVOLVE_MOVE, Moves.DYNAMIC_PUNCH ], - [ 1, Moves.BUBBLE_BEAM ], - [ 1, Moves.BODY_SLAM ], - [ 1, Moves.HYPNOSIS ], - [ 1, Moves.WATER_SPORT ], [ RELEARN_MOVE, Moves.POUND ], [ RELEARN_MOVE, Moves.DOUBLE_EDGE ], [ RELEARN_MOVE, Moves.WATER_GUN ], @@ -1057,13 +1066,18 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ RELEARN_MOVE, Moves.MUD_SHOT ], [ RELEARN_MOVE, Moves.EARTH_POWER ], [ RELEARN_MOVE, Moves.CIRCLE_THROW ], + [ 1, Moves.BUBBLE_BEAM ], + [ 1, Moves.BODY_SLAM ], + [ 1, Moves.HYPNOSIS ], + [ 1, Moves.WATER_SPORT ], ], [Species.ABRA]: [ [ 1, Moves.TELEPORT ], - [ 1, Moves.CONFUSION ], //Custom + [ 1, Moves.CONFUSION ], // Custom ], [Species.KADABRA]: [ - [ EVOLVE_MOVE, Moves.PSYBEAM ], //LGPE + [ EVOLVE_MOVE, Moves.PSYBEAM ], // LGPE + [ 1, Moves.CONFUSION ], // Previous Stage Move, Custom [ 1, Moves.DISABLE ], [ 1, Moves.TELEPORT ], [ 1, Moves.KINESIS ], @@ -1184,10 +1198,18 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ RELEARN_MOVE, Moves.STOCKPILE ], [ RELEARN_MOVE, Moves.SWALLOW ], [ RELEARN_MOVE, Moves.SPIT_UP ], + [ RELEARN_MOVE, Moves.WRAP ], // Previous Stage Move + [ RELEARN_MOVE, Moves.GROWTH ], // Previous Stage Move + [ RELEARN_MOVE, Moves.ACID ], // Previous Stage Move + [ RELEARN_MOVE, Moves.KNOCK_OFF ], // Previous Stage Move [ RELEARN_MOVE, Moves.GASTRO_ACID ], + [ RELEARN_MOVE, Moves.POISON_JAB ], // Previous Stage Move + [ RELEARN_MOVE, Moves.SLAM ], // Previous Stage Move [ RELEARN_MOVE, Moves.POWER_WHIP ], [ 1, Moves.VINE_WHIP ], [ 1, Moves.SLEEP_POWDER ], + [ 1, Moves.POISON_POWDER ], // Previous Stage Move + [ 1, Moves.STUN_SPORE ], // Previous Stage Move [ 1, Moves.SWEET_SCENT ], [ 1, Moves.RAZOR_LEAF ], [ 44, Moves.LEAF_BLADE ], @@ -1262,6 +1284,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.ROCK_POLISH ], + [ 1, Moves.ROLLOUT ], // Previous Stage Move [ 1, Moves.HEAVY_SLAM ], [ 16, Moves.ROCK_THROW ], [ 18, Moves.SMACK_DOWN ], @@ -1548,7 +1571,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.GASTLY]: [ [ 1, Moves.CONFUSE_RAY ], [ 1, Moves.LICK ], - [ 1, Moves.ACID ], //Custom + [ 1, Moves.ACID ], // Custom [ 4, Moves.HYPNOSIS ], [ 8, Moves.MEAN_LOOK ], [ 12, Moves.PAYBACK ], @@ -1567,6 +1590,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.HYPNOSIS ], [ 1, Moves.CONFUSE_RAY ], [ 1, Moves.LICK ], + [ 1, Moves.ACID ], // Previous Stage Move, Custom [ 1, Moves.MEAN_LOOK ], [ 12, Moves.PAYBACK ], [ 16, Moves.SPITE ], @@ -1583,6 +1607,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.HYPNOSIS ], [ 1, Moves.CONFUSE_RAY ], [ 1, Moves.LICK ], + [ 1, Moves.ACID ], // Previous Stage Move, Custom [ 1, Moves.PERISH_SONG ], [ 1, Moves.MEAN_LOOK ], [ 1, Moves.SHADOW_PUNCH ], @@ -1609,7 +1634,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 12, Moves.DRAGON_BREATH ], [ 16, Moves.CURSE ], [ 20, Moves.ROCK_SLIDE ], - [ 22, Moves.GYRO_BALL ], //Custom, from USUM + [ 22, Moves.GYRO_BALL ], // Custom, from USUM [ 24, Moves.SCREECH ], [ 28, Moves.SAND_TOMB ], [ 32, Moves.STEALTH_ROCK ], @@ -1849,7 +1874,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.LICKITUNG]: [ [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.LICK ], - [ 1, Moves.TACKLE ], //Custom + [ 1, Moves.TACKLE ], // Custom [ 6, Moves.REST ], [ 12, Moves.SUPERSONIC ], [ 18, Moves.WRAP ], @@ -2090,6 +2115,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.MR_MIME]: [ [ 1, Moves.POUND ], + [ 1, Moves.TICKLE ], // Previous Stage Move [ 1, Moves.BATON_PASS ], [ 1, Moves.ENCORE ], [ 1, Moves.COPYCAT ], @@ -2122,7 +2148,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 20, Moves.DOUBLE_HIT ], [ 24, Moves.SLASH ], [ 28, Moves.FOCUS_ENERGY ], - [ 30, Moves.STEEL_WING ], //Custom + [ 30, Moves.STEEL_WING ], // Custom [ 32, Moves.AGILITY ], [ 36, Moves.AIR_SLASH ], [ 40, Moves.X_SCISSOR ], @@ -2279,6 +2305,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.VAPOREON]: [ [ EVOLVE_MOVE, Moves.BOUNCY_BUBBLE ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -2306,6 +2333,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.JOLTEON]: [ [ EVOLVE_MOVE, Moves.BUZZY_BUZZ ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -2333,6 +2361,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.FLAREON]: [ [ EVOLVE_MOVE, Moves.SIZZLY_SLIDE ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -2463,6 +2492,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SNORLAX]: [ [ 1, Moves.TACKLE ], [ 1, Moves.SCREECH ], + [ 1, Moves.ODOR_SLEUTH ], // Previous Stage Move [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.METRONOME ], [ 1, Moves.LICK ], @@ -2632,7 +2662,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.CHIKORITA]: [ [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], - [ 5, Moves.RAZOR_LEAF ], //Custom, moved from 6 to 5 + [ 5, Moves.RAZOR_LEAF ], // Custom, moved from 6 to 5 [ 9, Moves.POISON_POWDER ], [ 12, Moves.SYNTHESIS ], [ 17, Moves.REFLECT ], @@ -2682,8 +2712,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.CYNDAQUIL]: [ [ 1, Moves.TACKLE ], [ 1, Moves.LEER ], - [ 5, Moves.EMBER ], //Custom, moved from 10 to 5 - [ 10, Moves.SMOKESCREEN ], //Custom, moved from 6 to 10 + [ 5, Moves.EMBER ], // Custom, moved from 10 to 5 + [ 10, Moves.SMOKESCREEN ], // Custom, moved from 6 to 10 [ 13, Moves.QUICK_ATTACK ], [ 19, Moves.FLAME_WHEEL ], [ 22, Moves.DEFENSE_CURL ], @@ -2737,7 +2767,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.TOTODILE]: [ [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], - [ 5, Moves.WATER_GUN ], //Custom, moved from 6 to 5 + [ 5, Moves.WATER_GUN ], // Custom, moved from 6 to 5 [ 9, Moves.BITE ], [ 13, Moves.SCARY_FACE ], [ 19, Moves.ICE_FANG ], @@ -3149,6 +3179,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.MOONBLAST ], ], [Species.MARILL]: [ + [ 1, Moves.SPLASH ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAIL_WHIP ], [ 1, Moves.WATER_GUN ], @@ -3168,6 +3199,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 36, Moves.SUPERPOWER ], ], [Species.AZUMARILL]: [ + [ 1, Moves.SPLASH ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAIL_WHIP ], [ 1, Moves.WATER_GUN ], @@ -3189,6 +3221,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SUDOWOODO]: [ [ EVOLVE_MOVE, Moves.SLAM ], [ 1, Moves.ROCK_THROW ], + [ 1, Moves.TACKLE ], // Previous Stage Move, Custom [ 1, Moves.FLAIL ], [ 1, Moves.FAKE_TEARS ], [ 1, Moves.HAMMER_ARM ], @@ -3222,6 +3255,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.HYDRO_PUMP ], [ 1, Moves.BELLY_DRUM ], [ 1, Moves.POUND ], + [ 1, Moves.WATER_SPORT ], // Previous Stage Move ], [Species.HOPPIP]: [ [ 1, Moves.TACKLE ], @@ -3315,9 +3349,12 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 39, Moves.SEED_BOMB ], ], [Species.SUNFLORA]: [ + [ RELEARN_MOVE, Moves.SEED_BOMB ], // Previous Stage Move [ 1, Moves.POUND ], [ 1, Moves.TACKLE ], [ 1, Moves.GROWTH ], + [ 1, Moves.ENDEAVOR ], // Previous Stage Move + [ 1, Moves.SYNTHESIS ], // Previous Stage Move [ 4, Moves.INGRAIN ], [ 7, Moves.ABSORB ], [ 10, Moves.MEGA_DRAIN ], @@ -3382,6 +3419,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.ESPEON]: [ [ EVOLVE_MOVE, Moves.GLITZY_GLOW ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -3408,6 +3446,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.UMBREON]: [ [ EVOLVE_MOVE, Moves.BADDY_BAD ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -3464,6 +3503,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 15, Moves.DISABLE ], [ 18, Moves.WATER_PULSE ], [ 21, Moves.HEADBUTT ], + [ 24, Moves.ZEN_HEADBUTT ], // Previous Stage Move, Galar Slowking Level [ 27, Moves.AMNESIA ], [ 30, Moves.SURF ], [ 33, Moves.SLACK_OFF ], @@ -3562,7 +3602,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.DUNSPARCE]: [ [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.FLAIL ], - [ 1, Moves.TACKLE ], //Custom + [ 1, Moves.TACKLE ], // Custom [ 4, Moves.MUD_SLAP ], [ 8, Moves.ROLLOUT ], [ 12, Moves.GLARE ], @@ -3609,6 +3649,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 12, Moves.DRAGON_BREATH ], [ 16, Moves.CURSE ], [ 20, Moves.ROCK_SLIDE ], + [ 22, Moves.GYRO_BALL ], // Custom from USUM [ 24, Moves.SCREECH ], [ 28, Moves.SAND_TOMB ], [ 32, Moves.STEALTH_ROCK ], @@ -3688,6 +3729,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 20, Moves.DOUBLE_HIT ], [ 24, Moves.SLASH ], [ 28, Moves.FOCUS_ENERGY ], + [ 30, Moves.STEEL_WING ], // Custom [ 32, Moves.IRON_DEFENSE ], [ 36, Moves.IRON_HEAD ], [ 40, Moves.X_SCISSOR ], @@ -3765,8 +3807,11 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], [ 1, Moves.LICK ], - [ 1, Moves.FAKE_TEARS ], [ 1, Moves.COVET ], + [ 1, Moves.FLING ], // Previous Stage Move + [ 1, Moves.BABY_DOLL_EYES ], // Previous Stage Move + [ 1, Moves.FAKE_TEARS ], + [ 1, Moves.CHARM ], // Previous Stage Move [ 8, Moves.FURY_SWIPES ], [ 13, Moves.PAYBACK ], [ 17, Moves.SWEET_SCENT ], @@ -3783,7 +3828,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SLUGMA]: [ [ 1, Moves.SMOG ], [ 1, Moves.YAWN ], - [ 5, Moves.EMBER ], //Custom, Moved from Level 6 to 5 + [ 5, Moves.EMBER ], // Custom, Moved from Level 6 to 5 [ 8, Moves.ROCK_THROW ], [ 13, Moves.HARDEN ], [ 20, Moves.CLEAR_SMOG ], @@ -3898,7 +3943,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 48, Moves.SOAK ], [ 54, Moves.HYPER_BEAM ], ], - [Species.DELIBIRD]: [ //Given a custom level up learnset + [Species.DELIBIRD]: [ // Given a custom level up learnset [ 1, Moves.PRESENT ], [ 1, Moves.METRONOME ], [ 5, Moves.FAKE_OUT ], @@ -3991,6 +4036,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 62, Moves.INFERNO ], ], [Species.KINGDRA]: [ + [ RELEARN_MOVE, Moves.LASER_FOCUS ], // Previous Stage Move [ 1, Moves.LEER ], [ 1, Moves.WATER_GUN ], [ 1, Moves.SMOKESCREEN ], @@ -4025,9 +4071,17 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.DONPHAN]: [ [ EVOLVE_MOVE, Moves.FURY_ATTACK ], - [ 1, Moves.HORN_ATTACK ], + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.GROWL ], + [ 1, Moves.HORN_ATTACK ], [ 1, Moves.DEFENSE_CURL ], + [ 1, Moves.ODOR_SLEUTH ], // Previous Stage Move + [ 1, Moves.FLAIL ], // Previous Stage Move + [ 1, Moves.ENDURE ], // Previous Stage Move + [ 1, Moves.TAKE_DOWN ], // Previous Stage Move + [ 1, Moves.CHARM ], // Previous Stage Move + [ 1, Moves.LAST_RESORT ], // Previous Stage Move + [ 1, Moves.DOUBLE_EDGE ], // Previous Stage Move [ 1, Moves.THUNDER_FANG ], [ 1, Moves.FIRE_FANG ], [ 1, Moves.BULLDOZE ], @@ -4047,6 +4101,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.CONVERSION ], [ 1, Moves.RECYCLE ], [ 1, Moves.MAGNET_RISE ], + [ 1, Moves.MAGIC_COAT ], // Previous Stage Move [ 15, Moves.THUNDER_SHOCK ], [ 20, Moves.PSYBEAM ], [ 25, Moves.CONVERSION_2 ], @@ -4513,6 +4568,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.MARSHTOMP]: [ [ EVOLVE_MOVE, Moves.MUD_SHOT ], + [ RELEARN_MOVE, Moves.SURF ], // Previous Stage Move [ RELEARN_MOVE, Moves.ROCK_SMASH ], [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], @@ -4634,10 +4690,15 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SILCOON]: [ [ EVOLVE_MOVE, Moves.HARDEN ], + [ RELEARN_MOVE, Moves.TACKLE ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STRING_SHOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.POISON_STING ], // Previous Stage Move + [ RELEARN_MOVE, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.HARDEN ], ], [Species.BEAUTIFLY]: [ [ EVOLVE_MOVE, Moves.GUST ], + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.BUG_BITE ], [ 1, Moves.GUST ], [ 1, Moves.HARDEN ], @@ -4658,10 +4719,15 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.CASCOON]: [ [ EVOLVE_MOVE, Moves.HARDEN ], + [ RELEARN_MOVE, Moves.TACKLE ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STRING_SHOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.POISON_STING ], // Previous Stage Move + [ RELEARN_MOVE, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.HARDEN ], ], [Species.DUSTOX]: [ [ EVOLVE_MOVE, Moves.GUST ], + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.BUG_BITE ], [ 1, Moves.GUST ], [ 1, Moves.HARDEN ], @@ -4701,6 +4767,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.ABSORB ], [ 1, Moves.FLAIL ], [ 1, Moves.FAKE_OUT ], + [ 1, Moves.RAIN_DANCE ], // Previous Stage Move [ 1, Moves.KNOCK_OFF ], [ 1, Moves.TEETER_DANCE ], [ 1, Moves.ASTONISH ], @@ -4728,6 +4795,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ RELEARN_MOVE, Moves.ASTONISH ], [ RELEARN_MOVE, Moves.ENERGY_BALL ], [ RELEARN_MOVE, Moves.ZEN_HEADBUTT ], + [ RELEARN_MOVE, Moves.LEECH_SEED ], // Previous Stage Move + [ RELEARN_MOVE, Moves.GIGA_DRAIN ], // Previous Stage Move [ 1, Moves.FAKE_OUT ], [ 1, Moves.BUBBLE_BEAM ], [ 1, Moves.RAIN_DANCE ], @@ -4757,8 +4826,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.EXPLOSION ], [ 1, Moves.TACKLE ], [ 1, Moves.HARDEN ], + [ 1, Moves.BIDE ], // Previous Stage Move [ 1, Moves.ABSORB ], [ 1, Moves.ASTONISH ], + [ 1, Moves.HEADBUTT ], // Previous Stage Move [ 9, Moves.GROWTH ], [ 12, Moves.ROLLOUT ], [ 18, Moves.MEGA_DRAIN ], @@ -4773,11 +4844,13 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ EVOLVE_MOVE, Moves.LEAF_BLADE ], [ RELEARN_MOVE, Moves.WHIRLWIND ], [ RELEARN_MOVE, Moves.TACKLE ], + [ RELEARN_MOVE, Moves.BIDE ], // Previous Stage Move [ RELEARN_MOVE, Moves.ABSORB ], [ RELEARN_MOVE, Moves.MEGA_DRAIN ], [ RELEARN_MOVE, Moves.GROWTH ], [ RELEARN_MOVE, Moves.RAZOR_LEAF ], [ RELEARN_MOVE, Moves.HARDEN ], + [ RELEARN_MOVE, Moves.HEADBUTT ], // Previous Stage Move [ RELEARN_MOVE, Moves.EXPLOSION ], [ RELEARN_MOVE, Moves.ROLLOUT ], [ RELEARN_MOVE, Moves.SWAGGER ], @@ -4930,11 +5003,17 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 38, Moves.STICKY_WEB ], ], [Species.MASQUERAIN]: [ + [ RELEARN_MOVE, Moves.BATON_PASS ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STICKY_WEB ], // Previous Stage Move [ 1, Moves.WHIRLWIND ], [ 1, Moves.WATER_GUN ], [ 1, Moves.QUICK_ATTACK ], [ 1, Moves.SWEET_SCENT ], [ 1, Moves.SOAK ], + [ 1, Moves.BUBBLE_BEAM ], // Previous Stage Move + [ 1, Moves.AGILITY ], // Previous Stage Move + [ 1, Moves.MIST ], // Previous Stage Move + [ 1, Moves.HAZE ], // Previous Stage Move [ 1, Moves.OMINOUS_WIND ], [ 17, Moves.GUST ], [ 22, Moves.SCARY_FACE ], @@ -4963,6 +5042,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ EVOLVE_MOVE, Moves.MACH_PUNCH ], [ RELEARN_MOVE, Moves.SPORE ], [ 1, Moves.POISON_POWDER ], + [ 1, Moves.GIGA_DRAIN ], // Previous Stage Move [ 1, Moves.GROWTH ], [ 1, Moves.TOXIC ], [ 1, Moves.ABSORB ], @@ -4994,9 +5074,16 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 38, Moves.PLAY_ROUGH ], ], [Species.VIGOROTH]: [ + [ RELEARN_MOVE, Moves.PLAY_ROUGH ], // Previous Stage Move [ 1, Moves.SCRATCH ], + [ 1, Moves.YAWN ], // Previous Stage Move [ 1, Moves.FOCUS_ENERGY ], + [ 1, Moves.SLACK_OFF ], // Previous Stage Move [ 1, Moves.ENCORE ], + [ 1, Moves.HEADBUTT ], // Previous Stage Move + [ 1, Moves.AMNESIA ], // Previous Stage Move + [ 1, Moves.COVET ], // Previous Stage Move + [ 1, Moves.FLAIL ], // Previous Stage Move [ 1, Moves.UPROAR ], [ 14, Moves.FURY_SWIPES ], [ 17, Moves.ENDURE ], @@ -5008,11 +5095,20 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SLAKING]: [ [ EVOLVE_MOVE, Moves.SWAGGER ], + [ RELEARN_MOVE, Moves.PLAY_ROUGH ], // Previous Stage Move + [ RELEARN_MOVE, Moves.FOCUS_PUNCH ], // Previous Stage Move [ 1, Moves.SUCKER_PUNCH ], [ 1, Moves.SCRATCH ], [ 1, Moves.YAWN ], + [ 1, Moves.FOCUS_ENERGY ], // Previous Stage Move [ 1, Moves.ENCORE ], [ 1, Moves.SLACK_OFF ], + [ 1, Moves.UPROAR ], // Previous Stage Move + [ 1, Moves.FURY_SWIPES ], // Previous Stage Move + [ 1, Moves.ENDURE ], // Previous Stage Move + [ 1, Moves.HEADBUTT ], // Previous Stage Move + [ 1, Moves.SLASH ], // Previous Stage Move + [ 1, Moves.REVERSAL ], // Previous Stage Move [ 17, Moves.AMNESIA ], [ 23, Moves.COVET ], [ 27, Moves.THROAT_CHOP ], @@ -5148,6 +5244,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.BRINE ], [ 1, Moves.TACKLE ], [ 1, Moves.FOCUS_ENERGY ], + [ 1, Moves.SAND_ATTACK ], // Previous Stage Move [ 1, Moves.ARM_THRUST ], [ 10, Moves.FAKE_OUT ], [ 13, Moves.FORCE_PALM ], @@ -5351,6 +5448,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.DETECT ], [ 1, Moves.WORK_UP ], [ 1, Moves.BIDE ], + [ 1, Moves.REVERSAL ], // Previous Stage Move [ 12, Moves.ENDURE ], [ 15, Moves.FEINT ], [ 17, Moves.FORCE_PALM ], @@ -5520,6 +5618,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.POISON_GAS ], [ 1, Moves.WRING_OUT ], [ 1, Moves.SLUDGE ], + [ 1, Moves.PAIN_SPLIT ], // Previous Stage Move [ 12, Moves.AMNESIA ], [ 17, Moves.ACID_SPRAY ], [ 20, Moves.ENCORE ], @@ -5565,7 +5664,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.WAILMER]: [ [ 1, Moves.SPLASH ], - [ 1, Moves.TACKLE ], //Custom + [ 1, Moves.TACKLE ], // Custom [ 3, Moves.GROWL ], [ 6, Moves.ASTONISH ], [ 12, Moves.WATER_GUN ], @@ -5586,6 +5685,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.SOAK ], [ 1, Moves.NOBLE_ROAR ], [ 1, Moves.SPLASH ], + [ 1, Moves.TACKLE ], // Previous Stage Move, Custom [ 1, Moves.GROWL ], [ 1, Moves.ASTONISH ], [ 1, Moves.WATER_GUN ], @@ -5620,6 +5720,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.CAMERUPT]: [ [ EVOLVE_MOVE, Moves.ROCK_SLIDE ], + [ RELEARN_MOVE, Moves.FLAMETHROWER ], // Previous Stage Move + [ RELEARN_MOVE, Moves.DOUBLE_EDGE ], // Previous Stage Move [ 1, Moves.FISSURE ], [ 1, Moves.ERUPTION ], [ 1, Moves.GROWL ], @@ -5658,7 +5760,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SPOINK]: [ [ 1, Moves.SPLASH ], - [ 5, Moves.CONFUSION ], //Custom, Moved from Level 7 to 5 + [ 5, Moves.CONFUSION ], // Custom, Moved from Level 7 to 5 [ 10, Moves.GROWL ], [ 14, Moves.PSYBEAM ], [ 18, Moves.PSYCH_UP ], @@ -5676,6 +5778,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.BELCH ], [ 1, Moves.SPLASH ], [ 1, Moves.CONFUSION ], + [ 1, Moves.GROWL ], // Previous Stage Move [ 1, Moves.PSYBEAM ], [ 18, Moves.PSYCH_UP ], [ 22, Moves.CONFUSE_RAY ], @@ -6167,7 +6270,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SHUPPET]: [ [ 1, Moves.ASTONISH ], - [ 1, Moves.PURSUIT ], //Custom + [ 1, Moves.PURSUIT ], // Custom [ 4, Moves.SCREECH ], [ 7, Moves.NIGHT_SHADE ], [ 10, Moves.SPITE ], @@ -6183,6 +6286,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.BANETTE]: [ [ EVOLVE_MOVE, Moves.KNOCK_OFF ], + [ 1, Moves.ASTONISH ], // Previous Stage Move + [ 1, Moves.PURSUIT ], // Previous Stage Move, Custom [ 1, Moves.SCREECH ], [ 1, Moves.NIGHT_SHADE ], [ 1, Moves.SPITE ], @@ -6199,7 +6304,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.DUSKULL]: [ [ 1, Moves.ASTONISH ], [ 1, Moves.LEER ], - [ 1, Moves.PURSUIT ], //Custom + [ 1, Moves.PURSUIT ], // Custom [ 4, Moves.DISABLE ], [ 8, Moves.SHADOW_SNEAK ], [ 12, Moves.CONFUSE_RAY ], @@ -6221,6 +6326,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.BIND ], [ 1, Moves.ASTONISH ], [ 1, Moves.LEER ], + [ 1, Moves.PURSUIT ], // Previous Stage Move, Custom [ 1, Moves.DISABLE ], [ 1, Moves.SHADOW_SNEAK ], [ 12, Moves.CONFUSE_RAY ], @@ -6252,7 +6358,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.CHIMECHO]: [ [ 1, Moves.HEALING_WISH ], + [ 1, Moves.LAST_RESORT ], // Previous Stage Move + [ 1, Moves.ENTRAINMENT ], // Previous Stage Move [ 1, Moves.WRAP ], + [ 1, Moves.PSYWAVE ], // Previous Stage Move, Custom [ 1, Moves.GROWL ], [ 1, Moves.ASTONISH ], [ 1, Moves.CONFUSION ], @@ -6392,6 +6501,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 50, Moves.SHELL_SMASH ], ], [Species.HUNTAIL]: [ + [ 1, Moves.CLAMP ], // Previous Stage Move [ 1, Moves.WATER_GUN ], [ 1, Moves.IRON_DEFENSE ], [ 1, Moves.SHELL_SMASH ], @@ -6412,6 +6522,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 50, Moves.HYDRO_PUMP ], ], [Species.GOREBYSS]: [ + [ 1, Moves.CLAMP ], // Previous Stage Move [ 1, Moves.WATER_GUN ], [ 1, Moves.IRON_DEFENSE ], [ 1, Moves.SHELL_SMASH ], @@ -6497,6 +6608,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SALAMENCE]: [ [ EVOLVE_MOVE, Moves.FLY ], + [ RELEARN_MOVE, Moves.OUTRAGE ], // Previous Stage Move [ 1, Moves.PROTECT ], [ 1, Moves.DRAGON_TAIL ], [ 1, Moves.DUAL_WINGBEAT ], @@ -6712,7 +6824,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 98, Moves.DOOM_DESIRE ], ], [Species.DEOXYS]: [ - [ 1, Moves.CONFUSION ], //Custom + [ 1, Moves.CONFUSION ], // Custom [ 1, Moves.LEER ], [ 1, Moves.WRAP ], [ 7, Moves.NIGHT_SHADE ], @@ -6731,8 +6843,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.TURTWIG]: [ [ 1, Moves.TACKLE ], [ 5, Moves.WITHDRAW ], - [ 5, Moves.LEAFAGE ], //Custom, moved from 10 to 5, BDSP - [ 9, Moves.GROWTH ], //Fill empty moveslot, from BDSP level 6 + [ 5, Moves.LEAFAGE ], // Custom, moved from 10 to 5, BDSP + [ 9, Moves.GROWTH ], // Fill empty moveslot, from BDSP level 6 [ 13, Moves.RAZOR_LEAF ], [ 17, Moves.CURSE ], [ 21, Moves.BITE ], @@ -6748,6 +6860,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.ABSORB ], [ 1, Moves.WITHDRAW ], [ 1, Moves.LEAFAGE ], + [ 1, Moves.GROWTH ], // Previous Stage Move [ 13, Moves.RAZOR_LEAF ], [ 17, Moves.CURSE ], [ 22, Moves.BITE ], @@ -6763,6 +6876,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.ABSORB ], [ 1, Moves.LEAFAGE ], + [ 1, Moves.GROWTH ], // Previous Stage Move [ 1, Moves.RAZOR_LEAF ], [ 1, Moves.WITHDRAW ], [ 1, Moves.WOOD_HAMMER ], @@ -6779,7 +6893,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.CHIMCHAR]: [ [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], - [ 5, Moves.EMBER ], //Custom, moved from 7 to 5 + [ 5, Moves.EMBER ], // Custom, moved from 7 to 5 [ 9, Moves.TAUNT ], [ 15, Moves.FURY_SWIPES ], [ 17, Moves.FLAME_WHEEL ], @@ -6793,6 +6907,9 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.MONFERNO]: [ [ EVOLVE_MOVE, Moves.MACH_PUNCH ], + [ RELEARN_MOVE, Moves.NASTY_PLOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.FACADE ], // Previous Stage Move + [ RELEARN_MOVE, Moves.FLAMETHROWER ], // Previous Stage Move [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], [ 1, Moves.EMBER ], @@ -6810,7 +6927,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.INFERNAPE]: [ [ EVOLVE_MOVE, Moves.CLOSE_COMBAT ], [ RELEARN_MOVE, Moves.TAUNT ], + [ RELEARN_MOVE, Moves.NASTY_PLOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.FACADE ], // Previous Stage Move [ RELEARN_MOVE, Moves.SLACK_OFF ], + [ RELEARN_MOVE, Moves.FLAMETHROWER ], // Previous Stage Move [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], [ 1, Moves.EMBER ], @@ -6828,7 +6948,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.PIPLUP]: [ [ 1, Moves.POUND ], [ 4, Moves.GROWL ], - [ 5, Moves.WATER_GUN ], //Custom, moved from 8 to 5 + [ 5, Moves.WATER_GUN ], // Custom, moved from 8 to 5 [ 11, Moves.CHARM ], [ 15, Moves.PECK ], [ 18, Moves.BUBBLE_BEAM ], @@ -6845,6 +6965,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], [ 1, Moves.WATER_GUN ], + [ 1, Moves.CHARM ], // Previous Stage Move [ 15, Moves.PECK ], [ 19, Moves.BUBBLE_BEAM ], [ 24, Moves.SWAGGER ], @@ -6860,6 +6981,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], [ 1, Moves.WATER_GUN ], + [ 1, Moves.CHARM ], // Previous Stage Move [ 1, Moves.METAL_CLAW ], [ 11, Moves.SWORDS_DANCE ], [ 15, Moves.PECK ], @@ -6963,6 +7085,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], [ 1, Moves.BIDE ], + [ 1, Moves.STRUGGLE_BUG ], // Previous Stage Move + [ 1, Moves.BUG_BITE ], // Previous Stage Move [ 14, Moves.ABSORB ], [ 18, Moves.SING ], [ 22, Moves.FOCUS_ENERGY ], @@ -7113,13 +7237,14 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.BURMY]: [ [ 1, Moves.PROTECT ], - [ 1, Moves.STRUGGLE_BUG ], //Custom + [ 1, Moves.STRUGGLE_BUG ], // Custom [ 10, Moves.TACKLE ], [ 15, Moves.BUG_BITE ], [ 20, Moves.STRING_SHOT ], ], [Species.WORMADAM]: [ [ EVOLVE_MOVE, Moves.QUIVER_DANCE ], + [ 1, Moves.STRUGGLE_BUG ], // Previous Stage Move, Custom [ 1, Moves.TACKLE ], [ 1, Moves.PROTECT ], [ 1, Moves.SUCKER_PUNCH ], @@ -7140,6 +7265,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.MOTHIM]: [ [ EVOLVE_MOVE, Moves.QUIVER_DANCE ], + [ 1, Moves.STRUGGLE_BUG ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.PROTECT ], [ 1, Moves.BUG_BITE ], @@ -7221,6 +7347,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 49, Moves.WAVE_CRASH ], ], [Species.FLOATZEL]: [ + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.GROWL ], [ 1, Moves.QUICK_ATTACK ], [ 1, Moves.CRUNCH ], @@ -7368,6 +7495,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.LOPUNNY]: [ [ EVOLVE_MOVE, Moves.RETURN ], + [ 1, Moves.FRUSTRATION ], // Previous Stage Move [ 1, Moves.POUND ], [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.SPLASH ], @@ -7389,6 +7517,16 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 56, Moves.HIGH_JUMP_KICK ], ], [Species.MISMAGIUS]: [ + // Previous Stage Relearn Learnset + [ RELEARN_MOVE, Moves.CONFUSION ], + [ RELEARN_MOVE, Moves.CONFUSE_RAY ], + [ RELEARN_MOVE, Moves.MEAN_LOOK ], + [ RELEARN_MOVE, Moves.HEX ], + [ RELEARN_MOVE, Moves.PSYBEAM ], + [ RELEARN_MOVE, Moves.PAIN_SPLIT ], + [ RELEARN_MOVE, Moves.PAYBACK ], + [ RELEARN_MOVE, Moves.SHADOW_BALL ], + [ RELEARN_MOVE, Moves.PERISH_SONG ], [ 1, Moves.GROWL ], [ 1, Moves.SPITE ], [ 1, Moves.PSYWAVE ], @@ -7400,11 +7538,18 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.MYSTICAL_FIRE ], ], [Species.HONCHKROW]: [ - [ 1, Moves.WING_ATTACK ], - [ 1, Moves.HAZE ], + [ 1, Moves.PECK ], // Previous Stage Move [ 1, Moves.ASTONISH ], + [ 1, Moves.GUST ], // Previous Stage Move + [ 1, Moves.HAZE ], + [ 1, Moves.WING_ATTACK ], + [ 1, Moves.NIGHT_SHADE ], // Previous Stage Move + [ 1, Moves.ASSURANCE ], // Previous Stage Move + [ 1, Moves.TAUNT ], // Previous Stage Move + [ 1, Moves.MEAN_LOOK ], // Previous Stage Move [ 1, Moves.SUCKER_PUNCH ], [ 1, Moves.NIGHT_SLASH ], + [ 1, Moves.TORMENT ], // Previous Stage Move [ 1, Moves.QUASH ], [ 1, Moves.PURSUIT ], [ 25, Moves.SWAGGER ], @@ -7449,7 +7594,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.CHINGLING]: [ [ 1, Moves.WRAP ], - [ 1, Moves.PSYWAVE ], //Custom + [ 1, Moves.PSYWAVE ], // Custom [ 4, Moves.GROWL ], [ 7, Moves.ASTONISH ], [ 10, Moves.CONFUSION ], @@ -7482,6 +7627,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.SMOKESCREEN ], [ 1, Moves.POISON_GAS ], [ 1, Moves.FEINT ], + [ 1, Moves.ACID_SPRAY ], // Previous Stage Move [ 12, Moves.FURY_SWIPES ], [ 15, Moves.FOCUS_ENERGY ], [ 18, Moves.BITE ], @@ -7533,7 +7679,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.BONSLY]: [ [ 1, Moves.FAKE_TEARS ], [ 1, Moves.COPYCAT ], - [ 1, Moves.TACKLE ], //Custom + [ 1, Moves.TACKLE ], // Custom [ 4, Moves.FLAIL ], [ 8, Moves.ROCK_THROW ], [ 12, Moves.BLOCK ], @@ -7554,11 +7700,11 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 4, Moves.BATON_PASS ], [ 8, Moves.ENCORE ], [ 12, Moves.CONFUSION ], - [ 16, Moves.MIMIC ], //Custom, swapped with Role Play to be closer to USUM + [ 16, Moves.MIMIC ], // Custom, swapped with Role Play to be closer to USUM [ 20, Moves.PROTECT ], [ 24, Moves.RECYCLE ], [ 28, Moves.PSYBEAM ], - [ 32, Moves.ROLE_PLAY ], //Custom, swapped with Mimic + [ 32, Moves.ROLE_PLAY ], // Custom, swapped with Mimic [ 36, Moves.LIGHT_SCREEN ], [ 36, Moves.REFLECT ], [ 36, Moves.SAFEGUARD ], @@ -7696,6 +7842,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.LUCARIO]: [ [ EVOLVE_MOVE, Moves.AURA_SPHERE ], [ 1, Moves.QUICK_ATTACK ], + [ 1, Moves.ENDURE ], // Previous Stage Move [ 1, Moves.SCREECH ], [ 1, Moves.REVERSAL ], [ 1, Moves.DETECT ], @@ -7836,7 +7983,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.CARNIVINE]: [ [ 1, Moves.BIND ], [ 1, Moves.GROWTH ], - [ 1, Moves.LEAFAGE ], //Custom + [ 1, Moves.LEAFAGE ], // Custom [ 7, Moves.BITE ], [ 11, Moves.VINE_WHIP ], [ 17, Moves.SWEET_SCENT ], @@ -7973,6 +8120,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.SUPERSONIC ], [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.LICK ], + [ 1, Moves.TACKLE ], // Previous Stage Move, Custom [ 1, Moves.ROLLOUT ], [ 1, Moves.WRING_OUT ], [ 6, Moves.REST ], @@ -8086,7 +8234,9 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ RELEARN_MOVE, Moves.HYPNOSIS ], [ 1, Moves.TACKLE ], [ 1, Moves.DOUBLE_TEAM ], + [ 1, Moves.AIR_CUTTER ], // Previous Stage Move [ 1, Moves.NIGHT_SLASH ], + [ 1, Moves.WING_ATTACK ], // Previous Stage Move [ 1, Moves.AIR_SLASH ], [ 1, Moves.BUG_BUZZ ], [ 14, Moves.QUICK_ATTACK ], @@ -8102,6 +8252,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.LEAFEON]: [ [ EVOLVE_MOVE, Moves.SAPPY_SEED ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -8129,6 +8280,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.GLACEON]: [ [ EVOLVE_MOVE, Moves.FREEZY_FROST ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -8154,8 +8306,11 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 60, Moves.LAST_RESORT ], ], [Species.GLISCOR]: [ + [ 1, Moves.POISON_STING ], // Previous Stage Move [ 1, Moves.SAND_ATTACK ], [ 1, Moves.HARDEN ], + [ 1, Moves.POISON_TAIL ], // Previous Stage Move + [ 1, Moves.SLASH ], // Previous Stage Move [ 1, Moves.POISON_JAB ], [ 1, Moves.THUNDER_FANG ], [ 1, Moves.ICE_FANG ], @@ -8248,8 +8403,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.PROBOPASS]: [ [ EVOLVE_MOVE, Moves.TRI_ATTACK ], [ 1, Moves.TACKLE ], + [ 1, Moves.HARDEN ], // Previous Stage Move [ 1, Moves.IRON_DEFENSE ], [ 1, Moves.BLOCK ], + [ 1, Moves.ROCK_THROW ], // Previous Stage Move [ 1, Moves.GRAVITY ], [ 1, Moves.MAGNET_RISE ], [ 1, Moves.WIDE_GUARD ], @@ -8275,6 +8432,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.LEER ], [ 1, Moves.DISABLE ], [ 1, Moves.ASTONISH ], + [ 1, Moves.PURSUIT ], // Previous Stage Move, Custom [ 1, Moves.SHADOW_PUNCH ], [ 1, Moves.GRAVITY ], [ 1, Moves.SHADOW_SNEAK ], @@ -8298,6 +8456,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.POWDER_SNOW ], [ 1, Moves.PROTECT ], [ 1, Moves.DESTINY_BOND ], + [ 1, Moves.WEATHER_BALL ], // Previous Stage Move [ 1, Moves.CRUNCH ], [ 1, Moves.ASTONISH ], [ 1, Moves.ICE_FANG ], @@ -8538,7 +8697,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.DARKRAI]: [ [ 1, Moves.DISABLE ], [ 1, Moves.OMINOUS_WIND ], - [ 1, Moves.PURSUIT ], //Custom + [ 1, Moves.PURSUIT ], // Custom [ 11, Moves.QUICK_ATTACK ], [ 20, Moves.HYPNOSIS ], [ 29, Moves.SUCKER_PUNCH ], @@ -8551,7 +8710,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 93, Moves.DARK_PULSE ], ], [Species.SHAYMIN]: [ - [ 1, Moves.LEAFAGE ], //Custom + [ 1, Moves.LEAFAGE ], // Custom [ 1, Moves.GROWTH ], [ 10, Moves.MAGICAL_LEAF ], [ 19, Moves.LEECH_SEED ], @@ -8603,7 +8762,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SNIVY]: [ [ 1, Moves.TACKLE ], [ 4, Moves.LEER ], - [ 5, Moves.VINE_WHIP ], //Custom, moved from 7 to 5 + [ 5, Moves.VINE_WHIP ], // Custom, moved from 7 to 5 [ 10, Moves.WRAP ], [ 13, Moves.GROWTH ], [ 16, Moves.MAGICAL_LEAF ], @@ -8651,7 +8810,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.TEPIG]: [ [ 1, Moves.TACKLE ], [ 3, Moves.TAIL_WHIP ], - [ 5, Moves.EMBER ], //Custom, moved from 7 to 5 + [ 5, Moves.EMBER ], // Custom, moved from 7 to 5 [ 9, Moves.ENDURE ], [ 13, Moves.DEFENSE_CURL ], [ 15, Moves.FLAME_CHARGE ], @@ -8705,7 +8864,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.OSHAWOTT]: [ [ 1, Moves.TACKLE ], [ 5, Moves.TAIL_WHIP ], - [ 5, Moves.WATER_GUN ], //Custom, moved from 7 to 5 + [ 5, Moves.WATER_GUN ], // Custom, moved from 7 to 5 [ 11, Moves.SOAK ], [ 13, Moves.FOCUS_ENERGY ], [ 17, Moves.RAZOR_SHELL ], @@ -8776,6 +8935,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.WATCHOG]: [ [ EVOLVE_MOVE, Moves.CONFUSE_RAY ], + [ RELEARN_MOVE, Moves.WORK_UP ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.LEER ], [ 1, Moves.BITE ], @@ -8895,6 +9055,19 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 43, Moves.CRUNCH ], ], [Species.SIMISAGE]: [ + // Previous Stage Relearn Learnset + [ RELEARN_MOVE, Moves.SCRATCH ], + [ RELEARN_MOVE, Moves.PLAY_NICE ], + [ RELEARN_MOVE, Moves.VINE_WHIP ], + [ RELEARN_MOVE, Moves.LEECH_SEED ], + [ RELEARN_MOVE, Moves.BITE ], + [ RELEARN_MOVE, Moves.TORMENT ], + [ RELEARN_MOVE, Moves.FLING ], + [ RELEARN_MOVE, Moves.ACROBATICS ], + [ RELEARN_MOVE, Moves.GRASS_KNOT ], + [ RELEARN_MOVE, Moves.RECYCLE ], + [ RELEARN_MOVE, Moves.NATURAL_GIFT ], + [ RELEARN_MOVE, Moves.CRUNCH ], [ 1, Moves.LEER ], [ 1, Moves.LICK ], [ 1, Moves.FURY_SWIPES ], @@ -8919,6 +9092,19 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 43, Moves.CRUNCH ], ], [Species.SIMISEAR]: [ + // Previous Stage Relearn Learnset + [ RELEARN_MOVE, Moves.SCRATCH ], + [ RELEARN_MOVE, Moves.PLAY_NICE ], + [ RELEARN_MOVE, Moves.INCINERATE ], + [ RELEARN_MOVE, Moves.YAWN ], + [ RELEARN_MOVE, Moves.BITE ], + [ RELEARN_MOVE, Moves.AMNESIA ], + [ RELEARN_MOVE, Moves.FLING ], + [ RELEARN_MOVE, Moves.ACROBATICS ], + [ RELEARN_MOVE, Moves.FIRE_BLAST ], + [ RELEARN_MOVE, Moves.RECYCLE ], + [ RELEARN_MOVE, Moves.NATURAL_GIFT ], + [ RELEARN_MOVE, Moves.CRUNCH ], [ 1, Moves.LEER ], [ 1, Moves.LICK ], [ 1, Moves.FURY_SWIPES ], @@ -8943,6 +9129,19 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 43, Moves.CRUNCH ], ], [Species.SIMIPOUR]: [ + // Previous Stage Relearn Learnset + [ RELEARN_MOVE, Moves.SCRATCH ], + [ RELEARN_MOVE, Moves.PLAY_NICE ], + [ RELEARN_MOVE, Moves.WATER_GUN ], + [ RELEARN_MOVE, Moves.WATER_SPORT ], + [ RELEARN_MOVE, Moves.BITE ], + [ RELEARN_MOVE, Moves.TAUNT ], + [ RELEARN_MOVE, Moves.FLING ], + [ RELEARN_MOVE, Moves.ACROBATICS ], + [ RELEARN_MOVE, Moves.BRINE ], + [ RELEARN_MOVE, Moves.RECYCLE ], + [ RELEARN_MOVE, Moves.NATURAL_GIFT ], + [ RELEARN_MOVE, Moves.CRUNCH ], [ 1, Moves.LEER ], [ 1, Moves.LICK ], [ 1, Moves.FURY_SWIPES ], @@ -8967,6 +9166,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 52, Moves.WONDER_ROOM ], ], [Species.MUSHARNA]: [ + [ 1, Moves.PSYWAVE ], // Previous Stage Move [ 1, Moves.PSYBEAM ], [ 1, Moves.PSYCHIC ], [ 1, Moves.HYPNOSIS ], @@ -9295,7 +9495,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 70, Moves.HYDRO_PUMP ], ], [Species.THROH]: [ - [ 1, Moves.ROCK_SMASH ], //Custom + [ 1, Moves.ROCK_SMASH ], // Custom [ 1, Moves.LEER ], [ 1, Moves.BIDE ], [ 1, Moves.MAT_BLOCK ], @@ -9355,9 +9555,15 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.LEAVANNY]: [ [ EVOLVE_MOVE, Moves.SLASH ], [ RELEARN_MOVE, Moves.BUG_BITE ], + [ RELEARN_MOVE, Moves.STICKY_WEB ], // Previous Stage Move + [ RELEARN_MOVE, Moves.BUG_BUZZ ], // Previous Stage Move + [ 1, Moves.PROTECT ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.RAZOR_LEAF ], [ 1, Moves.STRING_SHOT ], + [ 1, Moves.GRASS_WHISTLE ], // Previous Stage Move + [ 1, Moves.ENDURE ], // Previous Stage Move + [ 1, Moves.FLAIL ], // Previous Stage Move [ 1, Moves.FALSE_SWIPE ], [ 22, Moves.STRUGGLE_BUG ], [ 29, Moves.FELL_STINGER ], @@ -9890,6 +10096,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TORMENT ], [ 1, Moves.U_TURN ], [ 1, Moves.HONE_CLAWS ], + [ 1, Moves.SCARY_FACE ], // Previous Stage Move [ 1, Moves.PURSUIT ], [ 12, Moves.FURY_SWIPES ], [ 20, Moves.TAUNT ], @@ -9964,6 +10171,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 28, Moves.FAKE_TEARS ], [ 34, Moves.HEAL_BLOCK ], [ 35, Moves.PSYCH_UP ], + [ 40, Moves.PSYCHIC ], // Previous Stage Move, Gothitelle Level [ 46, Moves.FLATTER ], [ 52, Moves.FUTURE_SIGHT ], [ 58, Moves.MAGIC_ROOM ], @@ -10081,7 +10289,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.VANILLITE]: [ [ 1, Moves.HARDEN ], [ 1, Moves.ASTONISH ], - [ 1, Moves.POWDER_SNOW ], //Custom + [ 1, Moves.POWDER_SNOW ], // Custom [ 4, Moves.TAUNT ], [ 8, Moves.MIST ], [ 12, Moves.ICY_WIND ], @@ -10100,6 +10308,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.HARDEN ], [ 1, Moves.TAUNT ], [ 1, Moves.ASTONISH ], + [ 1, Moves.POWDER_SNOW ], // Previous Stage Move, Custom [ 12, Moves.ICY_WIND ], [ 16, Moves.AVALANCHE ], [ 20, Moves.HAIL ], @@ -10116,6 +10325,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.HARDEN ], [ 1, Moves.TAUNT ], [ 1, Moves.ASTONISH ], + [ 1, Moves.POWDER_SNOW ], // Previous Stage Move, Custom [ 1, Moves.WEATHER_BALL ], [ 1, Moves.ICICLE_CRASH ], [ 1, Moves.FREEZE_DRY ], @@ -10428,6 +10638,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.EELEKTRIK]: [ [ EVOLVE_MOVE, Moves.CRUNCH ], + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.HEADBUTT ], [ 1, Moves.THUNDER_WAVE ], [ 1, Moves.SPARK ], @@ -10445,7 +10656,15 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 74, Moves.THRASH ], ], [Species.EELEKTROSS]: [ + [ RELEARN_MOVE, Moves.THUNDERBOLT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.ACID_SPRAY ], // Previous Stage Move + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.HEADBUTT ], + [ 1, Moves.THUNDER_WAVE ], // Previous Stage Move + [ 1, Moves.SPARK ], // Previous Stage Move + [ 1, Moves.CHARGE_BEAM ], // Previous Stage Move + [ 1, Moves.ION_DELUGE ], // Previous Stage Move + [ 1, Moves.BIND ], // Previous Stage Move [ 1, Moves.THRASH ], [ 1, Moves.ACID ], [ 1, Moves.ZAP_CANNON ], @@ -10688,6 +10907,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.BODY_SLAM ], [ 1, Moves.ACID ], [ 1, Moves.ABSORB ], + [ 1, Moves.PROTECT ], // Previous Stage Move [ 1, Moves.QUICK_ATTACK ], [ 1, Moves.DOUBLE_TEAM ], [ 1, Moves.ACID_ARMOR ], @@ -10881,6 +11101,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.BRAVIARY]: [ [ EVOLVE_MOVE, Moves.SUPERPOWER ], + [ RELEARN_MOVE, Moves.BRAVE_BIRD ], // Previous Stage Move [ 1, Moves.WING_ATTACK ], [ 1, Moves.LEER ], [ 1, Moves.PECK ], @@ -11422,6 +11643,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.GROWL ], [ 1, Moves.WATER_GUN ], [ 1, Moves.QUICK_ATTACK ], + [ 1, Moves.ROUND ], // Previous Stage Move + [ 1, Moves.FLING ], // Previous Stage Move + [ 1, Moves.SMACK_DOWN ], // Previous Stage Move + [ 1, Moves.BOUNCE ], // Previous Stage Move [ 1, Moves.HAZE ], [ 1, Moves.MAT_BLOCK ], [ 1, Moves.ROLE_PLAY ], @@ -11529,10 +11754,19 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SPEWPA]: [ [ EVOLVE_MOVE, Moves.PROTECT ], + [ RELEARN_MOVE, Moves.TACKLE ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STRING_SHOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STUN_SPORE ], // Previous Stage Move + [ RELEARN_MOVE, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.HARDEN ], ], [Species.VIVILLON]: [ [ EVOLVE_MOVE, Moves.GUST ], + [ 1, Moves.PROTECT ], // Previous Stage Move + [ 1, Moves.TACKLE ], // Previous Stage Move + [ 1, Moves.STRING_SHOT ], // Previous Stage Move + [ 1, Moves.HARDEN ], // Previous Stage Move + [ 1, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.POISON_POWDER ], [ 1, Moves.STUN_SPORE ], [ 1, Moves.SLEEP_POWDER ], @@ -11615,6 +11849,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 58, Moves.SOLAR_BEAM ], ], [Species.FLORGES]: [ + [ 1, Moves.VINE_WHIP ], // Previous Stage Move + [ 1, Moves.TACKLE ], // Previous Stage Move + [ 1, Moves.FAIRY_WIND ], // Previous Stage Move + [ 1, Moves.RAZOR_LEAF ], // Previous Stage Move [ 1, Moves.SOLAR_BEAM ], [ 1, Moves.PETAL_DANCE ], [ 1, Moves.SAFEGUARD ], @@ -12106,6 +12344,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SYLVEON]: [ [ EVOLVE_MOVE, Moves.SPARKLY_SWIRL ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -12216,6 +12455,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.WATER_GUN ], [ 1, Moves.ABSORB ], + [ 1, Moves.ACID_ARMOR ], // Previous Stage Move [ 1, Moves.DRAGON_BREATH ], [ 1, Moves.POISON_TAIL ], [ 1, Moves.FEINT ], @@ -12225,6 +12465,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 20, Moves.FLAIL ], [ 25, Moves.WATER_PULSE ], [ 30, Moves.RAIN_DANCE ], + [ 35, Moves.DRAGON_PULSE ], // Previous Stage Move, NatDex / Hisui Goodra Level [ 43, Moves.CURSE ], [ 49, Moves.BODY_SLAM ], [ 58, Moves.MUDDY_WATER ], @@ -12285,7 +12526,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.PUMPKABOO]: [ [ 1, Moves.ASTONISH ], [ 1, Moves.TRICK_OR_TREAT ], - [ 1, Moves.LEAFAGE ], //Custom + [ 1, Moves.LEAFAGE ], // Custom [ 4, Moves.SHADOW_SNEAK ], [ 8, Moves.CONFUSE_RAY ], [ 12, Moves.RAZOR_LEAF ], @@ -12302,6 +12543,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.CONFUSE_RAY ], [ 1, Moves.EXPLOSION ], [ 1, Moves.ASTONISH ], + [ 1, Moves.LEAFAGE ], // Previous Stage Move, Custom [ 1, Moves.SHADOW_SNEAK ], [ 1, Moves.TRICK_OR_TREAT ], [ 1, Moves.MOONBLAST ], @@ -12813,11 +13055,14 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.CRABOMINABLE]: [ [ EVOLVE_MOVE, Moves.ICE_PUNCH ], + [ RELEARN_MOVE, Moves.CRABHAMMER ], // Previous Stage Move + [ 1, Moves.VISE_GRIP ], // Previous Stage Move [ 1, Moves.LEER ], [ 1, Moves.PROTECT ], [ 1, Moves.ROCK_SMASH ], [ 1, Moves.BUBBLE ], [ 1, Moves.PURSUIT ], + [ 1, Moves.PAYBACK ], // Previous Stage Move [ 17, Moves.BUBBLE_BEAM ], [ 22, Moves.BRICK_BREAK ], [ 25, Moves.SLAM ], @@ -13006,6 +13251,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.BUG_BITE ], [ 1, Moves.WIDE_GUARD ], [ 1, Moves.INFESTATION ], + [ 1, Moves.WATER_SPORT ], // Previous Stage Move [ 1, Moves.SPIDER_WEB ], [ 12, Moves.BUBBLE_BEAM ], [ 16, Moves.AQUA_RING ], @@ -13154,7 +13400,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.BOUNSWEET]: [ [ 1, Moves.SPLASH ], - [ 1, Moves.LEAFAGE ], //Custom + [ 1, Moves.LEAFAGE ], // Custom [ 4, Moves.PLAY_NICE ], [ 8, Moves.RAPID_SPIN ], [ 12, Moves.RAZOR_LEAF ], @@ -13165,6 +13411,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 32, Moves.AROMATIC_MIST ], ], [Species.STEENEE]: [ + [ 1, Moves.LEAFAGE ], // Previous Stage Move, Custom [ 1, Moves.RAZOR_LEAF ], [ 1, Moves.SPLASH ], [ 1, Moves.FLAIL ], @@ -13179,6 +13426,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.TSAREENA]: [ [ EVOLVE_MOVE, Moves.TROP_KICK ], + [ 1, Moves.LEAFAGE ], // Previous Stage Move, Custom [ 1, Moves.RAZOR_LEAF ], [ 1, Moves.SPLASH ], [ 1, Moves.FLAIL ], @@ -13303,7 +13551,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 68, Moves.SANDSTORM ], ], [Species.PYUKUMUKU]: [ - [ 1, Moves.COUNTER ], //Custom, Moved from Level 20 to 1 + [ 1, Moves.COUNTER ], // Custom, Moved from Level 20 to 1 [ 1, Moves.HARDEN ], [ 1, Moves.BATON_PASS ], [ 1, Moves.BIDE ], @@ -13312,7 +13560,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 5, Moves.HELPING_HAND ], [ 10, Moves.TAUNT ], [ 15, Moves.SAFEGUARD ], - [ 20, Moves.MIRROR_COAT ], //Custom + [ 20, Moves.MIRROR_COAT ], // Custom [ 25, Moves.PURIFY ], [ 30, Moves.CURSE ], [ 35, Moves.GASTRO_ACID ], @@ -13629,15 +13877,19 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.COSMOG]: [ [ 1, Moves.TELEPORT ], [ 1, Moves.SPLASH ], - [ 1, Moves.STORED_POWER ], //Custom + [ 1, Moves.STORED_POWER ], // Custom ], [Species.COSMOEM]: [ [ EVOLVE_MOVE, Moves.COSMIC_POWER ], [ 1, Moves.TELEPORT ], + [ 1, Moves.SPLASH ], // Previous Stage Move + [ 1, Moves.STORED_POWER ], // Previous Stage Move, Custom ], [Species.SOLGALEO]: [ [ EVOLVE_MOVE, Moves.SUNSTEEL_STRIKE ], [ 1, Moves.TELEPORT ], + [ 1, Moves.SPLASH ], // Previous Stage Move + [ 1, Moves.STORED_POWER ], // Previous Stage Move, Custom [ 1, Moves.METAL_CLAW ], [ 1, Moves.COSMIC_POWER ], [ 1, Moves.NOBLE_ROAR ], @@ -13660,6 +13912,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.CONFUSION ], [ 1, Moves.HYPNOSIS ], [ 1, Moves.TELEPORT ], + [ 1, Moves.SPLASH ], // Previous Stage Move + [ 1, Moves.STORED_POWER ], // Previous Stage Move, Custom [ 1, Moves.COSMIC_POWER ], [ 7, Moves.NIGHT_SHADE ], [ 14, Moves.CONFUSE_RAY ], @@ -13826,7 +14080,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.MAGEARNA]: [ [ 1, Moves.HELPING_HAND ], [ 1, Moves.GYRO_BALL ], - [ 1, Moves.DISARMING_VOICE ], //Custom + [ 1, Moves.DISARMING_VOICE ], // Custom [ 1, Moves.CRAFTY_SHIELD ], [ 1, Moves.GEAR_UP ], [ 6, Moves.DEFENSE_CURL ], @@ -13867,7 +14121,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 99, Moves.CLOSE_COMBAT ], ], [Species.POIPOLE]: [ - [ RELEARN_MOVE, Moves.DRAGON_PULSE ], //Custom, made relearn + [ RELEARN_MOVE, Moves.DRAGON_PULSE ], // Custom, made relearn [ 1, Moves.GROWL ], [ 1, Moves.ACID ], [ 1, Moves.PECK ], @@ -13986,7 +14240,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.GROOKEY]: [ [ 1, Moves.SCRATCH ], [ 1, Moves.GROWL ], - [ 5, Moves.BRANCH_POKE ], //Custom, moved from 6 to 5 + [ 5, Moves.BRANCH_POKE ], // Custom, moved from 6 to 5 [ 8, Moves.TAUNT ], [ 12, Moves.RAZOR_LEAF ], [ 17, Moves.SCREECH ], @@ -14031,7 +14285,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SCORBUNNY]: [ [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], - [ 5, Moves.EMBER ], //Custom, moved from 6 to 5 + [ 5, Moves.EMBER ], // Custom, moved from 6 to 5 [ 8, Moves.QUICK_ATTACK ], [ 12, Moves.DOUBLE_KICK ], [ 17, Moves.FLAME_CHARGE ], @@ -14073,7 +14327,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SOBBLE]: [ [ 1, Moves.POUND ], [ 1, Moves.GROWL ], - [ 5, Moves.WATER_GUN ], //Custom, moved from 6 to 5 + [ 5, Moves.WATER_GUN ], // Custom, moved from 6 to 5 [ 8, Moves.BIND ], [ 12, Moves.WATER_PULSE ], [ 17, Moves.TEARFUL_LOOK ], @@ -14400,10 +14654,11 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.APPLIN]: [ [ 1, Moves.WITHDRAW ], [ 1, Moves.ASTONISH ], - [ 1, Moves.LEAFAGE ], //Custom + [ 1, Moves.LEAFAGE ], // Custom ], [Species.FLAPPLE]: [ [ EVOLVE_MOVE, Moves.WING_ATTACK ], + [ 1, Moves.LEAFAGE ], // Previous Stage Move, Custom [ 1, Moves.GROWTH ], [ 1, Moves.WITHDRAW ], [ 1, Moves.TWISTER ], @@ -14423,6 +14678,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.APPLETUN]: [ [ EVOLVE_MOVE, Moves.HEADBUTT ], + [ 1, Moves.LEAFAGE ], // Previous Stage Move, Custom [ 1, Moves.GROWTH ], [ 1, Moves.WITHDRAW ], [ 1, Moves.SWEET_SCENT ], @@ -14443,7 +14699,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SILICOBRA]: [ [ 1, Moves.SAND_ATTACK ], [ 1, Moves.WRAP ], - [ 1, Moves.MUD_SLAP ], //Custom + [ 1, Moves.MUD_SLAP ], // Custom [ 5, Moves.MINIMIZE ], [ 10, Moves.BRUTAL_SWING ], [ 15, Moves.BULLDOZE ], @@ -14458,6 +14714,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SANDACONDA]: [ [ 1, Moves.SAND_ATTACK ], [ 1, Moves.WRAP ], + [ 1, Moves.MUD_SLAP ], // Previous Stage Move, Custom [ 1, Moves.MINIMIZE ], [ 1, Moves.BRUTAL_SWING ], [ 15, Moves.BULLDOZE ], @@ -14605,7 +14862,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SINISTEA]: [ [ 1, Moves.WITHDRAW ], [ 1, Moves.ASTONISH ], - [ 1, Moves.ABSORB ], //Custom + [ 1, Moves.ABSORB ], // Custom [ 6, Moves.AROMATIC_MIST ], [ 12, Moves.MEGA_DRAIN ], [ 24, Moves.SUCKER_PUNCH ], @@ -14618,6 +14875,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.POLTEAGEIST]: [ [ EVOLVE_MOVE, Moves.TEATIME ], + [ 1, Moves.ABSORB ], // Previous Stage Move, Custom [ 1, Moves.MEGA_DRAIN ], [ 1, Moves.WITHDRAW ], [ 1, Moves.ASTONISH ], @@ -14805,6 +15063,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.MR_RIME]: [ [ 1, Moves.POUND ], + [ 1, Moves.BARRIER ], // Previous Stage Move + [ 1, Moves.TICKLE ], // Previous Stage Move [ 1, Moves.MIMIC ], [ 1, Moves.LIGHT_SCREEN ], [ 1, Moves.REFLECT ], @@ -15133,6 +15393,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.DRAGAPULT]: [ [ EVOLVE_MOVE, Moves.DRAGON_DARTS ], + [ RELEARN_MOVE, Moves.DRAGON_PULSE ], // Previous Stage Move [ 1, Moves.BITE ], [ 1, Moves.QUICK_ATTACK ], [ 1, Moves.DRAGON_BREATH ], @@ -15339,6 +15600,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.WYRDEER]: [ [ EVOLVE_MOVE, Moves.PSYSHIELD_BASH ], [ 1, Moves.TACKLE ], + [ 1, Moves.ME_FIRST ], // Previous Stage Move [ 3, Moves.LEER ], [ 7, Moves.ASTONISH ], [ 10, Moves.HYPNOSIS ], @@ -15355,6 +15617,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.KLEAVOR]: [ [ EVOLVE_MOVE, Moves.STONE_AXE ], + [ 1, Moves.WING_ATTACK ], // Previous Stage Move + [ 1, Moves.AIR_SLASH ], // Previous Stage Move [ 1, Moves.LEER ], [ 1, Moves.QUICK_ATTACK ], [ 4, Moves.FURY_CUTTER ], @@ -15364,6 +15628,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 20, Moves.DOUBLE_HIT ], [ 24, Moves.SLASH ], [ 28, Moves.FOCUS_ENERGY ], + [ 30, Moves.STEEL_WING ], // Custom [ 32, Moves.AGILITY ], [ 36, Moves.ROCK_SLIDE ], [ 40, Moves.X_SCISSOR ], @@ -15374,8 +15639,11 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], [ 1, Moves.LICK ], - [ 1, Moves.FAKE_TEARS ], [ 1, Moves.COVET ], + [ 1, Moves.FLING ], // Previous Stage Move + [ 1, Moves.BABY_DOLL_EYES ], // Previous Stage Move + [ 1, Moves.FAKE_TEARS ], + [ 1, Moves.CHARM ], // Previous Stage Moves [ 8, Moves.FURY_SWIPES ], [ 13, Moves.PAYBACK ], [ 17, Moves.SWEET_SCENT ], @@ -15390,6 +15658,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 64, Moves.HAMMER_ARM ], ], [Species.BASCULEGION]: [ + [ RELEARN_MOVE, Moves.FINAL_GAMBIT ], // Previous Stage Move, White Stripe currently shares moveset with other forms [ 1, Moves.TAIL_WHIP ], [ 1, Moves.WATER_GUN ], [ 1, Moves.SHADOW_BALL ], @@ -15949,10 +16218,12 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.GARGANACL]: [ [ EVOLVE_MOVE, Moves.HAMMER_ARM ], + [ RELEARN_MOVE, Moves.IRON_DEFENSE ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.HARDEN ], [ 1, Moves.BLOCK ], [ 1, Moves.ROCK_BLAST ], + [ 1, Moves.SMACK_DOWN ], // Previous Stage Move [ 1, Moves.WIDE_GUARD ], [ 5, Moves.ROCK_THROW ], [ 7, Moves.MUD_SHOT ], @@ -16140,6 +16411,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ EVOLVE_MOVE, Moves.DOODLE ], [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], + [ 1, Moves.BITE ], // Previous Stage Move [ 5, Moves.ACID_SPRAY ], [ 8, Moves.FURY_SWIPES ], [ 11, Moves.SWITCHEROO ], @@ -16294,6 +16566,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.CONFUSION ], [ 1, Moves.DEFENSE_CURL ], + [ 1, Moves.MUD_SHOT ], // Previous Stage Move + [ 1, Moves.DIG ], // Previous Stage Move [ 4, Moves.SAND_ATTACK ], [ 7, Moves.STRUGGLE_BUG ], [ 11, Moves.ROLLOUT ], @@ -16717,6 +16991,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.LEER ], [ 1, Moves.COUNTER ], [ 1, Moves.FOCUS_ENERGY ], + [ 1, Moves.COVET ], // Previous Stage Move [ 1, Moves.FLING ], [ 5, Moves.FURY_SWIPES ], [ 8, Moves.LOW_KICK ], @@ -16734,6 +17009,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.CLODSIRE]: [ [ EVOLVE_MOVE, Moves.AMNESIA ], + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.TAIL_WHIP ], [ 1, Moves.POISON_STING ], [ 4, Moves.TOXIC_SPIKES ], @@ -16768,6 +17044,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.DUDUNSPARCE]: [ [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.FLAIL ], + [ 1, Moves.TACKLE ], // Previous Stage Move, Custom [ 4, Moves.MUD_SLAP ], [ 8, Moves.ROLLOUT ], [ 12, Moves.GLARE ], @@ -16864,7 +17141,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.CONFUSE_RAY ], [ 1, Moves.SPITE ], [ 1, Moves.ASTONISH ], - [ 1, Moves.PSYBEAM ], //Custom, moved from 7 to 1 + [ 1, Moves.PSYBEAM ], // Custom, moved from 7 to 1 [ 14, Moves.MEAN_LOOK ], [ 21, Moves.MEMENTO ], [ 28, Moves.WISH ], @@ -16939,7 +17216,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.IRON_BUNDLE]: [ [ RELEARN_MOVE, Moves.ELECTRIC_TERRAIN ], [ 1, Moves.PRESENT ], - [ 1, Moves.WATER_GUN ], //Custom + [ 1, Moves.WATER_GUN ], // Custom [ 7, Moves.POWDER_SNOW ], [ 14, Moves.WHIRLPOOL ], [ 21, Moves.TAKE_DOWN ], @@ -17058,6 +17335,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 18, Moves.FOCUS_ENERGY ], [ 24, Moves.BITE ], [ 29, Moves.ICE_FANG ], + [ 32, Moves.DRAGON_CLAW ], // Previous Stage Move, Frigibax Level [ 40, Moves.TAKE_DOWN ], [ 45, Moves.ICE_BEAM ], [ 50, Moves.CRUNCH ], @@ -17305,6 +17583,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.DIPPLIN]: [ [ EVOLVE_MOVE, Moves.DOUBLE_HIT ], [ RELEARN_MOVE, Moves.DRAGON_CHEER ], // Custom + [ 1, Moves.LEAFAGE ], [ 1, Moves.WITHDRAW ], [ 1, Moves.SWEET_SCENT ], [ 1, Moves.RECYCLE ], @@ -17324,7 +17603,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.STUN_SPORE ], [ 1, Moves.WITHDRAW ], [ 1, Moves.ASTONISH ], - [ 5, Moves.ABSORB ], //Custom, Moved from Level 6 to 5 + [ 5, Moves.ABSORB ], // Custom, Moved from Level 6 to 5 [ 12, Moves.LIFE_DEW ], [ 18, Moves.FOUL_PLAY ], [ 24, Moves.MEGA_DRAIN ], @@ -17337,6 +17616,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SINISTCHA]: [ [ EVOLVE_MOVE, Moves.MATCHA_GOTCHA ], + [ RELEARN_MOVE, Moves.GIGA_DRAIN ], // Previous Stage Move [ 1, Moves.STUN_SPORE ], [ 1, Moves.WITHDRAW ], [ 1, Moves.ASTONISH ], @@ -17419,6 +17699,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.ARCHALUDON]: [ [ EVOLVE_MOVE, Moves.ELECTRO_SHOT ], + [ RELEARN_MOVE, Moves.LASER_FOCUS ], // Previous Stage Move [ 1, Moves.LEER ], [ 1, Moves.METAL_CLAW ], [ 6, Moves.ROCK_SMASH ], @@ -17438,6 +17719,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ RELEARN_MOVE, Moves.YAWN ], [ RELEARN_MOVE, Moves.DOUBLE_HIT ], [ RELEARN_MOVE, Moves.INFESTATION ], + [ RELEARN_MOVE, Moves.DRAGON_CHEER ], // Previous Stage Move, Custom + [ 1, Moves.LEAFAGE ], // Previous Stage Move, Custom [ 1, Moves.WITHDRAW ], [ 1, Moves.SWEET_SCENT ], [ 1, Moves.RECYCLE ], @@ -17809,6 +18092,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.CHARGE ], [ 1, Moves.ROCK_POLISH ], + [ 1, Moves.ROLLOUT ], // Previous Stage Move [ 1, Moves.HEAVY_SLAM ], [ 12, Moves.SPARK ], [ 16, Moves.ROCK_THROW ], @@ -18051,6 +18335,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.GALAR_MR_MIME]: [ [ 1, Moves.POUND ], + [ 1, Moves.BARRIER ], // Previous Stage Move + [ 1, Moves.TICKLE ], // Previous Stage Move [ 1, Moves.MIMIC ], [ 1, Moves.LIGHT_SCREEN ], [ 1, Moves.REFLECT ], @@ -18411,6 +18697,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.TAIL_WHIP ], [ 1, Moves.WATER_GUN ], + [ 1, Moves.SOAK ], // Previous Stage Move [ 1, Moves.SLASH ], [ 1, Moves.MEGAHORN ], [ 1, Moves.SUCKER_PUNCH ], @@ -18436,6 +18723,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.STUN_SPORE ], [ 1, Moves.SLEEP_POWDER ], [ 1, Moves.GIGA_DRAIN ], + [ 1, Moves.CHARM ], // Previous Stage Move [ 1, Moves.SYNTHESIS ], [ 1, Moves.SUNNY_DAY ], [ 1, Moves.HELPING_HAND ], @@ -18487,6 +18775,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.HISUI_BRAVIARY]: [ [ EVOLVE_MOVE, Moves.ESPER_WING ], + [ RELEARN_MOVE, Moves.BRAVE_BIRD ], // Previous Stage Move [ 1, Moves.WING_ATTACK ], [ 1, Moves.LEER ], [ 1, Moves.PECK ], @@ -18511,6 +18800,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.ABSORB ], [ 1, Moves.ACID_ARMOR ], [ 1, Moves.DRAGON_BREATH ], + [ 1, Moves.BODY_SLAM ], // Previous Stage Move [ 15, Moves.PROTECT ], [ 20, Moves.FLAIL ], [ 25, Moves.WATER_PULSE ], @@ -18525,6 +18815,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.WATER_GUN ], [ 1, Moves.ABSORB ], + [ 1, Moves.ACID_ARMOR ], // Previous Stage Move [ 1, Moves.DRAGON_BREATH ], [ 1, Moves.FEINT ], [ 1, Moves.ACID_SPRAY ], @@ -18565,9 +18856,11 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.HISUI_DECIDUEYE]: [ [ EVOLVE_MOVE, Moves.TRIPLE_ARROWS ], + [ RELEARN_MOVE, Moves.NASTY_PLOT ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], [ 1, Moves.U_TURN ], + [ 1, Moves.ASTONISH ], // Previous Stage Move [ 1, Moves.LEAF_STORM ], [ 1, Moves.LEAFAGE ], [ 9, Moves.PECK ], @@ -18633,7 +18926,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { }; export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { - [Species.PIKACHU]: { //Custom + [Species.PIKACHU]: { // Custom 1: [ [ 1, Moves.TAIL_WHIP ], [ 1, Moves.GROWL ], @@ -18648,14 +18941,14 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 8, Moves.DOUBLE_TEAM ], [ 12, Moves.ELECTRO_BALL ], [ 16, Moves.FEINT ], - [ 20, Moves.ZIPPY_ZAP ], //Custom + [ 20, Moves.ZIPPY_ZAP ], // Custom [ 24, Moves.AGILITY ], [ 28, Moves.IRON_TAIL ], [ 32, Moves.DISCHARGE ], - [ 34, Moves.FLOATY_FALL ], //Custom + [ 34, Moves.FLOATY_FALL ], // Custom [ 36, Moves.THUNDERBOLT ], [ 40, Moves.LIGHT_SCREEN ], - [ 42, Moves.SPLISHY_SPLASH ], //Custom + [ 42, Moves.SPLISHY_SPLASH ], // Custom [ 44, Moves.THUNDER ], [ 48, Moves.PIKA_PAPOW ], ], @@ -18816,19 +19109,19 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 8, Moves.DOUBLE_TEAM ], [ 12, Moves.ELECTRO_BALL ], [ 16, Moves.FEINT ], - [ 20, Moves.ZIPPY_ZAP ], //Custom + [ 20, Moves.ZIPPY_ZAP ], // Custom [ 24, Moves.AGILITY ], [ 28, Moves.IRON_TAIL ], [ 32, Moves.DISCHARGE ], - [ 34, Moves.FLOATY_FALL ], //Custom + [ 34, Moves.FLOATY_FALL ], // Custom [ 36, Moves.THUNDERBOLT ], [ 40, Moves.LIGHT_SCREEN ], - [ 42, Moves.SPLISHY_SPLASH ], //Custom + [ 42, Moves.SPLISHY_SPLASH ], // Custom [ 44, Moves.THUNDER ], [ 48, Moves.PIKA_PAPOW ], ], }, - [Species.EEVEE]: { //Custom + [Species.EEVEE]: { // Custom 1: [ [ 1, Moves.TACKLE ], [ 1, Moves.TAIL_WHIP ], @@ -18838,21 +19131,21 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 5, Moves.SAND_ATTACK ], [ 10, Moves.QUICK_ATTACK ], [ 15, Moves.BABY_DOLL_EYES ], - [ 18, Moves.BOUNCY_BUBBLE ], //Custom - [ 18, Moves.SIZZLY_SLIDE ], //Custom - [ 18, Moves.BUZZY_BUZZ ], //Custom + [ 18, Moves.BOUNCY_BUBBLE ], // Custom + [ 18, Moves.SIZZLY_SLIDE ], // Custom + [ 18, Moves.BUZZY_BUZZ ], // Custom [ 20, Moves.SWIFT ], [ 25, Moves.BITE ], [ 30, Moves.COPYCAT ], - [ 33, Moves.BADDY_BAD ], //Custom - [ 33, Moves.GLITZY_GLOW ], //Custom + [ 33, Moves.BADDY_BAD ], // Custom + [ 33, Moves.GLITZY_GLOW ], // Custom [ 35, Moves.BATON_PASS ], - [ 40, Moves.VEEVEE_VOLLEY ], //Custom, replaces Take Down - [ 43, Moves.FREEZY_FROST ], //Custom - [ 43, Moves.SAPPY_SEED ], //Custom + [ 40, Moves.VEEVEE_VOLLEY ], // Custom, replaces Take Down + [ 43, Moves.FREEZY_FROST ], // Custom + [ 43, Moves.SAPPY_SEED ], // Custom [ 45, Moves.CHARM ], [ 50, Moves.DOUBLE_EDGE ], - [ 53, Moves.SPARKLY_SWIRL ], //Custom + [ 53, Moves.SPARKLY_SWIRL ], // Custom [ 55, Moves.LAST_RESORT ], ], 2: [ @@ -18864,27 +19157,27 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 5, Moves.SAND_ATTACK ], [ 10, Moves.QUICK_ATTACK ], [ 15, Moves.BABY_DOLL_EYES ], - [ 18, Moves.BOUNCY_BUBBLE ], //Custom - [ 18, Moves.SIZZLY_SLIDE ], //Custom - [ 18, Moves.BUZZY_BUZZ ], //Custom + [ 18, Moves.BOUNCY_BUBBLE ], // Custom + [ 18, Moves.SIZZLY_SLIDE ], // Custom + [ 18, Moves.BUZZY_BUZZ ], // Custom [ 20, Moves.SWIFT ], [ 25, Moves.BITE ], [ 30, Moves.COPYCAT ], - [ 33, Moves.BADDY_BAD ], //Custom - [ 33, Moves.GLITZY_GLOW ], //Custom + [ 33, Moves.BADDY_BAD ], // Custom + [ 33, Moves.GLITZY_GLOW ], // Custom [ 35, Moves.BATON_PASS ], - [ 40, Moves.VEEVEE_VOLLEY ], //Custom, replaces Take Down - [ 43, Moves.FREEZY_FROST ], //Custom - [ 43, Moves.SAPPY_SEED ], //Custom + [ 40, Moves.VEEVEE_VOLLEY ], // Custom, replaces Take Down + [ 43, Moves.FREEZY_FROST ], // Custom + [ 43, Moves.SAPPY_SEED ], // Custom [ 45, Moves.CHARM ], [ 50, Moves.DOUBLE_EDGE ], - [ 53, Moves.SPARKLY_SWIRL ], //Custom + [ 53, Moves.SPARKLY_SWIRL ], // Custom [ 55, Moves.LAST_RESORT ], ], }, [Species.DEOXYS]: { 1: [ - [ 1, Moves.CONFUSION ], //Custom + [ 1, Moves.CONFUSION ], // Custom [ 1, Moves.WRAP ], [ 1, Moves.LEER ], [ 7, Moves.NIGHT_SHADE ], @@ -18901,7 +19194,7 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 73, Moves.HYPER_BEAM ], ], 2: [ - [ 1, Moves.CONFUSION ], //Custom + [ 1, Moves.CONFUSION ], // Custom [ 1, Moves.WRAP ], [ 1, Moves.LEER ], [ 7, Moves.NIGHT_SHADE ], @@ -18920,7 +19213,7 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 73, Moves.MIRROR_COAT ], ], 3: [ - [ 1, Moves.CONFUSION ], //Custom + [ 1, Moves.CONFUSION ], // Custom [ 1, Moves.WRAP ], [ 1, Moves.LEER ], [ 7, Moves.NIGHT_SHADE ], @@ -18940,6 +19233,7 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [Species.WORMADAM]: { 1: [ [ EVOLVE_MOVE, Moves.QUIVER_DANCE ], + [ 1, Moves.STRUGGLE_BUG ], // Previous Stage Move, Custom [ 1, Moves.TACKLE ], [ 1, Moves.PROTECT ], [ 1, Moves.SUCKER_PUNCH ], @@ -18960,6 +19254,7 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { ], 2: [ [ EVOLVE_MOVE, Moves.QUIVER_DANCE ], + [ 1, Moves.STRUGGLE_BUG ], // Previous Stage Move, Custom [ 1, Moves.METAL_BURST ], [ 1, Moves.TACKLE ], [ 1, Moves.PROTECT ], @@ -19064,7 +19359,7 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { }, [Species.SHAYMIN]: { 1: [ - [ 1, Moves.LEAFAGE ], //Custom + [ 1, Moves.LEAFAGE ], // Custom [ 1, Moves.GROWTH ], [ 10, Moves.MAGICAL_LEAF ], [ 19, Moves.LEECH_SEED ], @@ -19166,6 +19461,10 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 1, Moves.GROWL ], [ 1, Moves.WATER_GUN ], [ 1, Moves.QUICK_ATTACK ], + [ 1, Moves.ROUND ], // Previous Stage Move + [ 1, Moves.FLING ], // Previous Stage Move + [ 1, Moves.SMACK_DOWN ], // Previous Stage Move + [ 1, Moves.BOUNCE ], // Previous Stage Move [ 1, Moves.HAZE ], [ 1, Moves.ROLE_PLAY ], [ 1, Moves.NIGHT_SLASH ], From 1ad4f3b376a60f33f55a734a061d0dec9b74bc40 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 22 Oct 2024 20:11:02 -0700 Subject: [PATCH 08/17] [Test] Add `STATUS_ACTIVATION_OVERRIDE` to `overrides.ts` (#4689) This applies to Paralysis and Freeze Added Paralysis test to demonstrate usage - Consolidate `this.cancel()` calls --- src/overrides.ts | 2 + src/phases/move-phase.ts | 18 ++- src/test/data/status-effect.test.ts | 118 ++++++++++------ src/test/utils/helpers/moveHelper.ts | 36 +++-- src/test/utils/helpers/overridesHelper.ts | 157 ++++++++++++---------- 5 files changed, 203 insertions(+), 128 deletions(-) diff --git a/src/overrides.ts b/src/overrides.ts index e1bfbd240f0..6760db79205 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -75,6 +75,8 @@ class DefaultOverrides { readonly ITEM_UNLOCK_OVERRIDE: Unlockables[] = []; /** Set to `true` to show all tutorials */ readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false; + /** Set to `true` to force Paralysis and Freeze to always activate, or `false` to force them to not activate */ + readonly STATUS_ACTIVATION_OVERRIDE: boolean | null = null; // ---------------- // PLAYER OVERRIDES diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index e9d8887e9cb..66dbd06f5be 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -11,6 +11,7 @@ import { getTerrainBlockMessage } from "#app/data/weather"; import { MoveUsedEvent } from "#app/events/battle-scene"; import Pokemon, { MoveResult, PokemonMove, TurnMove } from "#app/field/pokemon"; 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 { MoveEffectPhase } from "#app/phases/move-effect-phase"; @@ -168,10 +169,7 @@ export class MovePhase extends BattlePhase { switch (this.pokemon.status.effect) { case StatusEffect.PARALYSIS: - if (!this.pokemon.randSeedInt(4)) { - activated = true; - this.cancelled = true; - } + activated = (!this.pokemon.randSeedInt(4) || Overrides.STATUS_ACTIVATION_OVERRIDE === true) && Overrides.STATUS_ACTIVATION_OVERRIDE !== false; break; case StatusEffect.SLEEP: applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove()); @@ -180,16 +178,22 @@ export class MovePhase extends BattlePhase { this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value; healed = this.pokemon.status.sleepTurnsRemaining <= 0; activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP); - this.cancelled = activated; break; case StatusEffect.FREEZE: - healed = !!this.move.getMove().findAttr(attr => attr instanceof HealStatusEffectAttr && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE)) || !this.pokemon.randSeedInt(5); + healed = + !!this.move.getMove().findAttr((attr) => + attr instanceof HealStatusEffectAttr + && attr.selfTarget + && attr.isOfEffect(StatusEffect.FREEZE)) + || (!this.pokemon.randSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) + || Overrides.STATUS_ACTIVATION_OVERRIDE === false; + activated = !healed; - this.cancelled = activated; break; } if (activated) { + this.cancel(); this.scene.queueMessage(getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon))); this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1))); } else if (healed) { diff --git a/src/test/data/status-effect.test.ts b/src/test/data/status-effect.test.ts index 8b37da45d8d..1b1a97fc51f 100644 --- a/src/test/data/status-effect.test.ts +++ b/src/test/data/status-effect.test.ts @@ -8,10 +8,10 @@ import { getStatusEffectOverlapText, } from "#app/data/status-effect"; import { MoveResult } from "#app/field/pokemon"; -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 { mockI18next } from "#test/utils/testUtils"; import i18next from "i18next"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -306,58 +306,98 @@ describe("Status Effect Messages", () => { }); }); -describe("Status Effects - Sleep", () => { - let phaserGame: Phaser.Game; - let game: GameManager; +describe("Status Effects", () => { + describe("Paralysis", () => { + let phaserGame: Phaser.Game; + let game: GameManager; - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override + .enemySpecies(Species.MAGIKARP) + .enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.BALL_FETCH) + .moveset([ Moves.QUICK_ATTACK ]) + .ability(Abilities.BALL_FETCH) + .statusEffect(StatusEffect.PARALYSIS); + }); + + it("causes the pokemon's move to fail when activated", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.QUICK_ATTACK); + await game.move.forceStatusActivation(true); + await game.toNextTurn(); + + expect(game.scene.getEnemyPokemon()!.isFullHp()).toBe(true); + expect(game.scene.getPlayerPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); }); }); - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); + describe("Sleep", () => { + let phaserGame: Phaser.Game; + let game: GameManager; - 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); - }); + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); - it("should last the appropriate number of turns", async () => { - await game.classicMode.startBattle([ Species.FEEBAS ]); + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); - const player = game.scene.getPlayerPokemon()!; - player.status = new Status(StatusEffect.SLEEP, 0, 4); + 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); + }); - game.move.select(Moves.SPLASH); - await game.toNextTurn(); + it("should last the appropriate number of turns", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); - expect(player.status.effect).toBe(StatusEffect.SLEEP); + const player = game.scene.getPlayerPokemon()!; + player.status = new Status(StatusEffect.SLEEP, 0, 4); - game.move.select(Moves.SPLASH); - await game.toNextTurn(); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); - expect(player.status.effect).toBe(StatusEffect.SLEEP); + expect(player.status.effect).toBe(StatusEffect.SLEEP); - game.move.select(Moves.SPLASH); - await game.toNextTurn(); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); - expect(player.status.effect).toBe(StatusEffect.SLEEP); - expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + expect(player.status.effect).toBe(StatusEffect.SLEEP); - game.move.select(Moves.SPLASH); - await game.toNextTurn(); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); - expect(player.status?.effect).toBeUndefined(); - expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + expect(player.status.effect).toBe(StatusEffect.SLEEP); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(player.status?.effect).toBeUndefined(); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + }); }); }); diff --git a/src/test/utils/helpers/moveHelper.ts b/src/test/utils/helpers/moveHelper.ts index a0667d91f4c..73fe63395fd 100644 --- a/src/test/utils/helpers/moveHelper.ts +++ b/src/test/utils/helpers/moveHelper.ts @@ -1,12 +1,13 @@ import { BattlerIndex } from "#app/battle"; -import { Moves } from "#app/enums/moves"; +import Overrides from "#app/overrides"; import { CommandPhase } from "#app/phases/command-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { Command } from "#app/ui/command-ui-handler"; import { Mode } from "#app/ui/ui"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "#test/utils/gameManagerUtils"; +import { GameManagerHelper } from "#test/utils/helpers/gameManagerHelper"; import { vi } from "vitest"; -import { getMovePosition } from "../gameManagerUtils"; -import { GameManagerHelper } from "./gameManagerHelper"; /** * Helper to handle a Pokemon's move @@ -17,7 +18,7 @@ export class MoveHelper extends GameManagerHelper { * {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `true`. * Used to force a move to hit. */ - async forceHit(): Promise { + public async forceHit(): Promise { await this.game.phaseInterceptor.to(MoveEffectPhase, false); vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); } @@ -26,9 +27,9 @@ export class MoveHelper extends GameManagerHelper { * Intercepts {@linkcode MoveEffectPhase} and mocks the * {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `false`. * Used to force a move to miss. - * @param firstTargetOnly Whether the move should force miss on the first target only, in the case of multi-target moves. + * @param firstTargetOnly - Whether the move should force miss on the first target only, in the case of multi-target moves. */ - async forceMiss(firstTargetOnly: boolean = false): Promise { + public async forceMiss(firstTargetOnly: boolean = false): Promise { await this.game.phaseInterceptor.to(MoveEffectPhase, false); const hitCheck = vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck"); @@ -40,12 +41,12 @@ export class MoveHelper extends GameManagerHelper { } /** - * Select the move to be used by the given Pokemon(-index). Triggers during the next {@linkcode CommandPhase} - * @param move the move to use - * @param pkmIndex the pokemon index. Relevant for double-battles only (defaults to 0) - * @param targetIndex The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves, or `null` if a manual call to `selectTarget()` is required - */ - select(move: Moves, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null) { + * Select the move to be used by the given Pokemon(-index). Triggers during the next {@linkcode CommandPhase} + * @param move - the move to use + * @param pkmIndex - the pokemon index. Relevant for double-battles only (defaults to 0) + * @param targetIndex - The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves, or `null` if a manual call to `selectTarget()` is required + */ + public select(move: Moves, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null) { const movePosition = getMovePosition(this.game.scene, pkmIndex, move); this.game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { @@ -59,4 +60,15 @@ export class MoveHelper extends GameManagerHelper { this.game.selectTarget(movePosition, targetIndex); } } + + /** + * Forces the Paralysis or Freeze status to activate on the next move by temporarily mocking {@linkcode Overrides.STATUS_ACTIVATION_OVERRIDE}, + * advancing to the next `MovePhase`, and then resetting the override to `null` + * @param activated - `true` to force the status to activate, `false` to force the status to not activate (will cause Freeze to heal) + */ + public async forceStatusActivation(activated: boolean): Promise { + vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(activated); + await this.game.phaseInterceptor.to("MovePhase"); + vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(null); + } } diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index ec4d8dbbe4c..404f5c34a26 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -29,7 +29,7 @@ export class OverridesHelper extends GameManagerHelper { * @warning Any event listeners that are attached to [NewArenaEvent](events\battle-scene.ts) may need to be handled down the line * @param biome the biome to set */ - startingBiome(biome: Biome): this { + public startingBiome(biome: Biome): this { this.game.scene.newArena(biome); this.log(`Starting biome set to ${Biome[biome]} (=${biome})!`); return this; @@ -38,9 +38,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the starting wave (index) * @param wave the wave (index) to set. Classic: `1`-`200` - * @returns this + * @returns `this` */ - startingWave(wave: number): this { + public startingWave(wave: number): this { vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(wave); this.log(`Starting wave set to ${wave}!`); return this; @@ -49,9 +49,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) starting level * @param level the (pokemon) level to set - * @returns this + * @returns `this` */ - startingLevel(level: Species | number): this { + public startingLevel(level: Species | number): this { vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(level); this.log(`Player Pokemon starting level set to ${level}!`); return this; @@ -62,7 +62,7 @@ export class OverridesHelper extends GameManagerHelper { * @param value the XP multiplier to set * @returns `this` */ - xpMultiplier(value: number): this { + public xpMultiplier(value: number): this { vi.spyOn(Overrides, "XP_MULTIPLIER_OVERRIDE", "get").mockReturnValue(value); this.log(`XP Multiplier set to ${value}!`); return this; @@ -71,9 +71,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) starting held items * @param items the items to hold - * @returns this + * @returns `this` */ - startingHeldItems(items: ModifierOverride[]) { + public startingHeldItems(items: ModifierOverride[]): this { vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items); this.log("Player Pokemon starting held items set to:", items); return this; @@ -82,9 +82,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) {@linkcode Species | species} * @param species the (pokemon) {@linkcode Species | species} to set - * @returns this + * @returns `this` */ - starterSpecies(species: Species | number): this { + public starterSpecies(species: Species | number): this { vi.spyOn(Overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(species); this.log(`Player Pokemon species set to ${Species[species]} (=${species})!`); return this; @@ -92,9 +92,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) to be a random fusion - * @returns this + * @returns `this` */ - enableStarterFusion(): this { + public enableStarterFusion(): this { vi.spyOn(Overrides, "STARTER_FUSION_OVERRIDE", "get").mockReturnValue(true); this.log("Player Pokemon is a random fusion!"); return this; @@ -103,9 +103,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) fusion species * @param species the fusion species to set - * @returns this + * @returns `this` */ - starterFusionSpecies(species: Species | number): this { + public starterFusionSpecies(species: Species | number): this { vi.spyOn(Overrides, "STARTER_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species); this.log(`Player Pokemon fusion species set to ${Species[species]} (=${species})!`); return this; @@ -114,9 +114,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemons) forms * @param forms the (pokemon) forms to set - * @returns this + * @returns `this` */ - starterForms(forms: Partial>): this { + public starterForms(forms: Partial>): this { vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue(forms); const formsStr = Object.entries(forms) .map(([ speciesId, formIndex ]) => `${Species[speciesId]}=${formIndex}`) @@ -128,9 +128,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player's starting modifiers * @param modifiers the modifiers to set - * @returns this + * @returns `this` */ - startingModifier(modifiers: ModifierOverride[]): this { + public startingModifier(modifiers: ModifierOverride[]): this { vi.spyOn(Overrides, "STARTING_MODIFIER_OVERRIDE", "get").mockReturnValue(modifiers); this.log(`Player starting modifiers set to: ${modifiers}`); return this; @@ -139,9 +139,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) {@linkcode Abilities | ability} * @param ability the (pokemon) {@linkcode Abilities | ability} to set - * @returns this + * @returns `this` */ - ability(ability: Abilities): this { + public ability(ability: Abilities): this { vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(ability); this.log(`Player Pokemon ability set to ${Abilities[ability]} (=${ability})!`); return this; @@ -150,9 +150,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) **passive** {@linkcode Abilities | ability} * @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set - * @returns this + * @returns `this` */ - passiveAbility(passiveAbility: Abilities): this { + public passiveAbility(passiveAbility: Abilities): this { vi.spyOn(Overrides, "PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility); this.log(`Player Pokemon PASSIVE ability set to ${Abilities[passiveAbility]} (=${passiveAbility})!`); return this; @@ -161,9 +161,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) {@linkcode Moves | moves}set * @param moveset the {@linkcode Moves | moves}set to set - * @returns this + * @returns `this` */ - moveset(moveset: Moves | Moves[]): this { + public moveset(moveset: Moves | Moves[]): this { vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue(moveset); if (!Array.isArray(moveset)) { moveset = [ moveset ]; @@ -178,7 +178,7 @@ export class OverridesHelper extends GameManagerHelper { * @param statusEffect the {@linkcode StatusEffect | status-effect} to set * @returns */ - statusEffect(statusEffect: StatusEffect): this { + public statusEffect(statusEffect: StatusEffect): this { vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect); this.log(`Player Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`); return this; @@ -186,9 +186,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override each wave to not have standard trainer battles - * @returns this + * @returns `this` */ - disableTrainerWaves(): this { + public disableTrainerWaves(): this { const realFn = getGameMode; vi.spyOn(GameMode, "getGameMode").mockImplementation((gameMode: GameModes) => { const mode = realFn(gameMode); @@ -201,9 +201,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override each wave to not have critical hits - * @returns this + * @returns `this` */ - disableCrits() { + public disableCrits(): this { vi.spyOn(Overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true); this.log("Critical hits are disabled!"); return this; @@ -212,9 +212,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the {@linkcode WeatherType | weather (type)} * @param type {@linkcode WeatherType | weather type} to set - * @returns this + * @returns `this` */ - weather(type: WeatherType): this { + public weather(type: WeatherType): this { vi.spyOn(Overrides, "WEATHER_OVERRIDE", "get").mockReturnValue(type); this.log(`Weather set to ${Weather[type]} (=${type})!`); return this; @@ -223,9 +223,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the seed * @param seed the seed to set - * @returns this + * @returns `this` */ - seed(seed: string): this { + public seed(seed: string): this { vi.spyOn(this.game.scene, "resetSeed").mockImplementation(() => { this.game.scene.waveSeed = seed; Phaser.Math.RND.sow([ seed ]); @@ -239,9 +239,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the battle type (single or double) * @param battleType battle type to set - * @returns this + * @returns `this` */ - battleType(battleType: "single" | "double" | null): this { + public battleType(battleType: "single" | "double" | null): this { vi.spyOn(Overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue(battleType); this.log(`Battle type set to ${battleType} only!`); return this; @@ -250,9 +250,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) {@linkcode Species | species} * @param species the (pokemon) {@linkcode Species | species} to set - * @returns this + * @returns `this` */ - enemySpecies(species: Species | number): this { + public enemySpecies(species: Species | number): this { vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(species); this.log(`Enemy Pokemon species set to ${Species[species]} (=${species})!`); return this; @@ -260,9 +260,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) to be a random fusion - * @returns this + * @returns `this` */ - enableEnemyFusion(): this { + public enableEnemyFusion(): this { vi.spyOn(Overrides, "OPP_FUSION_OVERRIDE", "get").mockReturnValue(true); this.log("Enemy Pokemon is a random fusion!"); return this; @@ -271,9 +271,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) fusion species * @param species the fusion species to set - * @returns this + * @returns `this` */ - enemyFusionSpecies(species: Species | number): this { + public enemyFusionSpecies(species: Species | number): this { vi.spyOn(Overrides, "OPP_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species); this.log(`Enemy Pokemon fusion species set to ${Species[species]} (=${species})!`); return this; @@ -282,9 +282,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) {@linkcode Abilities | ability} * @param ability the (pokemon) {@linkcode Abilities | ability} to set - * @returns this + * @returns `this` */ - enemyAbility(ability: Abilities): this { + public enemyAbility(ability: Abilities): this { vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(ability); this.log(`Enemy Pokemon ability set to ${Abilities[ability]} (=${ability})!`); return this; @@ -293,9 +293,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) **passive** {@linkcode Abilities | ability} * @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set - * @returns this + * @returns `this` */ - enemyPassiveAbility(passiveAbility: Abilities): this { + public enemyPassiveAbility(passiveAbility: Abilities): this { vi.spyOn(Overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility); this.log(`Enemy Pokemon PASSIVE ability set to ${Abilities[passiveAbility]} (=${passiveAbility})!`); return this; @@ -304,9 +304,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) {@linkcode Moves | moves}set * @param moveset the {@linkcode Moves | moves}set to set - * @returns this + * @returns `this` */ - enemyMoveset(moveset: Moves | Moves[]): this { + public enemyMoveset(moveset: Moves | Moves[]): this { vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(moveset); if (!Array.isArray(moveset)) { moveset = [ moveset ]; @@ -319,9 +319,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) level * @param level the level to set - * @returns this + * @returns `this` */ - enemyLevel(level: number): this { + public enemyLevel(level: number): this { vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(level); this.log(`Enemy Pokemon level set to ${level}!`); return this; @@ -332,7 +332,7 @@ export class OverridesHelper extends GameManagerHelper { * @param statusEffect the {@linkcode StatusEffect | status-effect} to set * @returns */ - enemyStatusEffect(statusEffect: StatusEffect): this { + public enemyStatusEffect(statusEffect: StatusEffect): this { vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect); this.log(`Enemy Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`); return this; @@ -341,9 +341,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) held items * @param items the items to hold - * @returns this + * @returns `this` */ - enemyHeldItems(items: ModifierOverride[]) { + public enemyHeldItems(items: ModifierOverride[]): this { vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items); this.log("Enemy Pokemon held items set to:", items); return this; @@ -354,7 +354,7 @@ export class OverridesHelper extends GameManagerHelper { * @param unlockable The Unlockable(s) to enable. * @returns `this` */ - enableUnlockable(unlockable: Unlockables[]) { + public enableUnlockable(unlockable: Unlockables[]): this { vi.spyOn(Overrides, "ITEM_UNLOCK_OVERRIDE", "get").mockReturnValue(unlockable); this.log("Temporarily unlocked the following content: ", unlockable); return this; @@ -363,9 +363,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the items rolled at the end of a battle * @param items the items to be rolled - * @returns this + * @returns `this` */ - itemRewards(items: ModifierOverride[]) { + public itemRewards(items: ModifierOverride[]): this { vi.spyOn(Overrides, "ITEM_REWARD_OVERRIDE", "get").mockReturnValue(items); this.log("Item rewards set to:", items); return this; @@ -375,8 +375,9 @@ export class OverridesHelper extends GameManagerHelper { * Override player shininess * @param shininess - `true` or `false` to force the player's pokemon to be shiny or not shiny, * `null` to disable the override and re-enable RNG shinies. + * @returns `this` */ - shiny(shininess: boolean | null): this { + public shiny(shininess: boolean | null): this { vi.spyOn(Overrides, "SHINY_OVERRIDE", "get").mockReturnValue(shininess); if (shininess === null) { this.log("Disabled player Pokemon shiny override!"); @@ -389,8 +390,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override player shiny variant * @param variant - The player's shiny variant. + * @returns `this` */ - shinyVariant(variant: Variant): this { + public shinyVariant(variant: Variant): this { vi.spyOn(Overrides, "VARIANT_OVERRIDE", "get").mockReturnValue(variant); this.log(`Set player Pokemon's shiny variant to ${variant}!`); return this; @@ -420,23 +422,38 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (Pokemon) to have the given amount of health segments * @param healthSegments the number of segments to give - * default: 0, the health segments will be handled like in the game based on wave, level and species - * 1: the Pokemon will not be a boss - * 2+: the Pokemon will be a boss with the given number of health segments - * @returns this + * - `0` (default): the health segments will be handled like in the game based on wave, level and species + * - `1`: the Pokemon will not be a boss + * - `2`+: the Pokemon will be a boss with the given number of health segments + * @returns `this` */ - enemyHealthSegments(healthSegments: number) { + public enemyHealthSegments(healthSegments: number): this { vi.spyOn(Overrides, "OPP_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments); this.log("Enemy Pokemon health segments set to:", healthSegments); return this; } + /** + * Override statuses (Paralysis and Freeze) to always or never activate + * @param activate - `true` to force activation, `false` to force no activation, `null` to disable the override + * @returns `this` + */ + public statusActivation(activate: boolean | null): this { + vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(activate); + if (activate !== null) { + this.log(`Paralysis and Freeze forced to ${activate ? "always" : "never"} activate!`); + } else { + this.log("Status activation override disabled!"); + } + return this; + } + /** * Override the encounter chance for a mystery encounter. * @param percentage the encounter chance in % - * @returns spy instance + * @returns `this` */ - mysteryEncounterChance(percentage: number) { + public mysteryEncounterChance(percentage: number): this { const maxRate: number = 256; // 100% const rate = maxRate * (percentage / 100); vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate); @@ -446,10 +463,10 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the encounter chance for a mystery encounter. - * @returns spy instance - * @param tier + * @param tier - The {@linkcode MysteryEncounterTier} to encounter + * @returns `this` */ - mysteryEncounterTier(tier: MysteryEncounterTier) { + public mysteryEncounterTier(tier: MysteryEncounterTier): this { vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier); this.log(`Mystery encounter tier set to ${tier}!`); return this; @@ -457,10 +474,10 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the encounter that spawns for the scene - * @param encounterType - * @returns spy instance + * @param encounterType - The {@linkcode MysteryEncounterType} of the encounter + * @returns `this` */ - mysteryEncounter(encounterType: MysteryEncounterType) { + public mysteryEncounter(encounterType: MysteryEncounterType): this { vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType); this.log(`Mystery encounter override set to ${encounterType}!`); return this; From a0baf892977b9d78d14b73c1c652d909b0a331a6 Mon Sep 17 00:00:00 2001 From: bjparker1226 <32974077+bjparker1226@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:04:37 -0400 Subject: [PATCH 09/17] [P2] Fix dark deal reducing transformed Pokemon's held item stack to 1 (#4707) --- src/data/mystery-encounters/encounters/dark-deal-encounter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 7f199b5487c..2c13086ccb8 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -172,7 +172,8 @@ export const DarkDealEncounter: MysteryEncounter = isBoss: true, modifierConfigs: bossModifiers.map(m => { return { - modifier: m + modifier: m, + stackCount: m.getStackCount(), }; }) }; From 16b71943669eda5a1d3519e58e3dba5b959e9d89 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Wed, 23 Oct 2024 17:05:13 +0200 Subject: [PATCH 10/17] [P3] Fix Egg Summary not showing new abilities in blue (#4712) --- src/ui/pokemon-info-container.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ui/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index 242e59c599b..5c3a22639dd 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -279,11 +279,8 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { this.pokemonAbilityText.setColor(getTextColor(abilityTextStyle, false, this.scene.uiTheme)); this.pokemonAbilityText.setShadowColor(getTextColor(abilityTextStyle, true, this.scene.uiTheme)); - - const ownedAbilityAttrs = pokemon.scene.gameData.starterData[pokemon.species.getRootSpeciesId()].abilityAttr; - // Check if the player owns ability for the root form - const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs); + const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(starterEntry.abilityAttr); if (!playerOwnsThisAbility) { this.pokemonAbilityLabelText.setColor(getTextColor(TextStyle.SUMMARY_BLUE, false, this.scene.uiTheme)); From 03025b267464d6e306dd30fc92042fef11490c3b Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Wed, 23 Oct 2024 08:08:40 -0700 Subject: [PATCH 11/17] [P2] Fix various charge move bugs (#4595) * Add charge move classes and phase * Integrate `MoveChargePhase` in battle phase sequence * Fix Protean + charge move interaction * Fix effect chance applying to semi-invulnerability * Remove `ChargeAttr` and fix ChargeAnim loading * Restore move history entry for charge phases * Gravity now cancels Fly, etc. after charge turn * Dig integration tests * Fly integration tests * Dive integration test + fix Dive in Harsh Sun bug * Solar Beam integration tests + `CHARGING` tag fixes * Fix dive test * Electro Shot integration tests * fix import in MoveChargePhase * Electro Shot Multi Lens test * Geomancy integration tests * Fix duplicate move queue * Update import * Docs + Fix Meteor Beam being boosted by Sheer Force * Fix volt absorb test * Apply PigeonBar's suggested move-phase changes Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> * Make Electro Shot Sheer Force boosted again * Apply PigeonBar's feedback pt. 2 * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fix mistimed/dupe showMoveText and leftover TODO --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> --- src/data/ability.ts | 8 +- src/data/arena-tag.ts | 3 + src/data/battle-anims.ts | 31 +- src/data/battler-tags.ts | 7 +- src/data/move.ts | 390 +++++++++++++++---------- src/field/pokemon.ts | 4 +- src/phases/move-charge-phase.ts | 84 ++++++ src/phases/move-effect-phase.ts | 163 +++++------ src/phases/move-phase.ts | 59 ++-- src/test/abilities/volt_absorb.test.ts | 4 +- src/test/arena/arena_gravity.test.ts | 80 +++-- src/test/moves/dig.test.ts | 114 ++++++++ src/test/moves/dive.test.ts | 137 +++++++++ src/test/moves/electro_shot.test.ts | 104 +++++++ src/test/moves/fly.test.ts | 122 ++++++++ src/test/moves/geomancy.test.ts | 78 +++++ src/test/moves/solar_beam.test.ts | 102 +++++++ src/test/moves/whirlwind.test.ts | 3 +- 18 files changed, 1188 insertions(+), 305 deletions(-) create mode 100644 src/phases/move-charge-phase.ts create mode 100644 src/test/moves/dig.test.ts create mode 100644 src/test/moves/dive.test.ts create mode 100644 src/test/moves/electro_shot.test.ts create mode 100644 src/test/moves/fly.test.ts create mode 100644 src/test/moves/geomancy.test.ts create mode 100644 src/test/moves/solar_beam.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index ebdd5105bb4..cc95045f8b7 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -7,7 +7,7 @@ import { Weather, WeatherType } from "./weather"; import { BattlerTag, 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, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; +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 { TerrainType } from "./terrain"; @@ -1139,7 +1139,9 @@ export class MoveEffectChanceMultiplierAbAttr extends AbAttr { apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { // Disable showAbility during getTargetBenefitScore this.showAbility = args[4]; - if ((args[0] as Utils.NumberHolder).value <= 0 || (args[1] as Move).id === Moves.ORDER_UP) { + + const exceptMoves = [ Moves.ORDER_UP, Moves.ELECTRO_SHOT ]; + if ((args[0] as Utils.NumberHolder).value <= 0 || exceptMoves.includes((args[1] as Move).id)) { return false; } @@ -1329,7 +1331,6 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr { */ const exceptAttrs: Constructor[] = [ MultiHitAttr, - ChargeAttr, SacrificialAttr, SacrificialAttrOnHit ]; @@ -1345,6 +1346,7 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr { /** 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; diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index aa6aec6f73a..d2c95b7ccdf 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -970,6 +970,9 @@ export class GravityTag extends ArenaTag { if (pokemon !== null) { pokemon.removeTag(BattlerTagType.FLOATING); pokemon.removeTag(BattlerTagType.TELEKINESIS); + if (pokemon.getTag(BattlerTagType.FLYING)) { + pokemon.addTag(BattlerTagType.INTERRUPTED); + } } }); } diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 03bf0809fa6..37900a3ab5a 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -1,6 +1,6 @@ //import { battleAnimRawData } from "./battle-anim-raw-data"; import BattleScene from "../battle-scene"; -import { AttackMove, BeakBlastHeaderAttr, ChargeAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move"; +import { AttackMove, BeakBlastHeaderAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move"; import Pokemon from "../field/pokemon"; import * as Utils from "../utils"; import { BattlerIndex } from "../battle"; @@ -476,8 +476,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { } else { const loadedCheckTimer = setInterval(() => { if (moveAnims.get(move) !== null) { - const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0] || allMoves[move].getAttrs(DelayedAttackAttr)[0]; - if (chargeAttr && chargeAnims.get(chargeAttr.chargeAnim) === null) { + const chargeAnimSource = (allMoves[move].isChargingMove()) + ? allMoves[move] + : (allMoves[move].getAttrs(DelayedAttackAttr)[0] + ?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]); + if (chargeAnimSource && chargeAnims.get(chargeAnimSource.chargeAnim) === null) { return; } clearInterval(loadedCheckTimer); @@ -507,11 +510,12 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { } else { populateMoveAnim(move, ba); } - const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0] - || allMoves[move].getAttrs(DelayedAttackAttr)[0] - || allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]; - if (chargeAttr) { - initMoveChargeAnim(scene, chargeAttr.chargeAnim).then(() => resolve()); + const chargeAnimSource = (allMoves[move].isChargingMove()) + ? allMoves[move] + : (allMoves[move].getAttrs(DelayedAttackAttr)[0] + ?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]); + if (chargeAnimSource) { + initMoveChargeAnim(scene, chargeAnimSource.chargeAnim).then(() => resolve()); } else { resolve(); } @@ -638,11 +642,12 @@ export function loadMoveAnimAssets(scene: BattleScene, moveIds: Moves[], startLo return new Promise(resolve => { const moveAnimations = moveIds.map(m => moveAnims.get(m) as AnimConfig).flat(); for (const moveId of moveIds) { - const chargeAttr = allMoves[moveId].getAttrs(ChargeAttr)[0] - || allMoves[moveId].getAttrs(DelayedAttackAttr)[0] - || allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0]; - if (chargeAttr) { - const moveChargeAnims = chargeAnims.get(chargeAttr.chargeAnim); + const chargeAnimSource = (allMoves[moveId].isChargingMove()) + ? allMoves[moveId] + : (allMoves[moveId].getAttrs(DelayedAttackAttr)[0] + ?? allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0]); + if (chargeAnimSource) { + const moveChargeAnims = chargeAnims.get(chargeAnimSource.chargeAnim); moveAnimations.push(moveChargeAnims instanceof AnimConfig ? moveChargeAnims : moveChargeAnims![0]); // TODO: is the bang correct? if (Array.isArray(moveChargeAnims)) { moveAnimations.push(moveChargeAnims[1]); diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index c3b7765d062..d671c56ab26 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -11,7 +11,6 @@ import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "#app/d import Move, { allMoves, applyMoveAttrs, - ChargeAttr, ConsecutiveUseDoublePowerAttr, HealOnAllyAttr, MoveCategory, @@ -949,10 +948,6 @@ export class EncoreTag extends BattlerTag { return false; } - if (allMoves[repeatableMove.move].hasAttr(ChargeAttr) && repeatableMove.result === MoveResult.OTHER) { - return false; - } - this.moveId = repeatableMove.move; return true; @@ -2591,7 +2586,7 @@ export class TormentTag extends MoveRestrictionBattlerTag { // This checks for locking / momentum moves like Rollout and Hydro Cannon + if the user is under the influence of BattlerTagType.FRENZY // Because Uproar's unique behavior is not implemented, it does not check for Uproar. Torment has been marked as partial in moves.ts const moveObj = allMoves[lastMove.move]; - const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY) || moveObj.hasAttr(ChargeAttr); + const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY); const validLastMoveResult = (lastMove.result === MoveResult.SUCCESS) || (lastMove.result === MoveResult.MISS); if (lastMove.move === move && validLastMoveResult && lastMove.move !== Moves.STRUGGLE && !isUnaffected) { return true; diff --git a/src/data/move.ts b/src/data/move.ts index ec25844909e..cf88fad0ac5 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -289,10 +289,9 @@ export default class Move implements Localizable { } /** - * Getter function that returns if the move targets itself or an ally + * Getter function that returns if the move targets the user or its ally * @returns boolean */ - isAllyTarget(): boolean { switch (this.moveTarget) { case MoveTarget.USER: @@ -306,6 +305,10 @@ export default class Move implements Localizable { return false; } + isChargingMove(): this is ChargingMove { + return false; + } + /** * Checks if the move is immune to certain types. * Currently looks at cases of Grass types with powder moves and Dark types with moves affected by Prankster. @@ -893,6 +896,85 @@ export class SelfStatusMove extends Move { } } +type SubMove = new (...args: any[]) => Move; + +function ChargeMove(Base: TBase) { + return class extends Base { + /** The animation to play during the move's charging phase */ + public readonly chargeAnim: ChargeAnim = ChargeAnim[`${Moves[this.id]}_CHARGING`]; + /** The message to show during the move's charging phase */ + private _chargeText: string; + + /** Move attributes that apply during the move's charging phase */ + public chargeAttrs: MoveAttr[] = []; + + override isChargingMove(): this is ChargingMove { + return true; + } + + /** + * Sets the text to be displayed during this move's charging phase. + * References to the user Pokemon should be written as "{USER}", and + * references to the target Pokemon should be written as "{TARGET}". + * @param chargeText the text to set + * @returns this {@linkcode Move} (for chaining API purposes) + */ + chargeText(chargeText: string): this { + this._chargeText = chargeText; + return this; + } + + /** + * Queues the charge text to display to the player + * @param user the {@linkcode Pokemon} using this move + * @param target the {@linkcode Pokemon} targeted by this move (optional) + */ + showChargeText(user: Pokemon, target?: Pokemon): void { + user.scene.queueMessage(this._chargeText + .replace("{USER}", getPokemonNameWithAffix(user)) + .replace("{TARGET}", getPokemonNameWithAffix(target)) + ); + } + + /** + * Gets all charge attributes of the given attribute type. + * @param attrType any attribute that extends {@linkcode MoveAttr} + * @returns Array of attributes that match `attrType`, or an empty array if + * no matches are found. + */ + getChargeAttrs(attrType: Constructor): T[] { + return this.chargeAttrs.filter((attr): attr is T => attr instanceof attrType); + } + + /** + * Checks if this move has an attribute of the given type. + * @param attrType any attribute that extends {@linkcode MoveAttr} + * @returns `true` if a matching attribute is found; `false` otherwise + */ + hasChargeAttr(attrType: Constructor): boolean { + return this.chargeAttrs.some((attr) => attr instanceof attrType); + } + + /** + * Adds an attribute to this move to be applied during the move's charging phase + * @param ChargeAttrType the type of {@linkcode MoveAttr} being added + * @param args the parameters to construct the given {@linkcode MoveAttr} with + * @returns this {@linkcode Move} (for chaining API purposes) + */ + chargeAttr>(ChargeAttrType: T, ...args: ConstructorParameters): this { + const chargeAttr = new ChargeAttrType(...args); + this.chargeAttrs.push(chargeAttr); + + return this; + } + }; +} + +export class ChargingAttackMove extends ChargeMove(AttackMove) {} +export class ChargingSelfStatusMove extends ChargeMove(SelfStatusMove) {} + +export type ChargingMove = ChargingAttackMove | ChargingSelfStatusMove; + /** * Base class defining all {@linkcode Move} Attributes * @abstract @@ -2574,6 +2656,63 @@ export class OneHitKOAttr extends MoveAttr { } } +/** + * Attribute that allows charge moves to resolve in 1 turn under a given condition. + * Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`. + * @extends MoveAttr + */ +export class InstantChargeAttr extends MoveAttr { + /** The condition in which the move with this attribute instantly charges */ + protected readonly condition: UserMoveConditionFunc; + + constructor(condition: UserMoveConditionFunc) { + super(true); + this.condition = condition; + } + + /** + * Flags the move with this attribute as instantly charged if this attribute's condition is met. + * @param user the {@linkcode Pokemon} using the move + * @param target n/a + * @param move the {@linkcode Move} associated with this attribute + * @param args + * - `[0]` a {@linkcode Utils.BooleanHolder | BooleanHolder} for the "instant charge" flag + * @returns `true` if the instant charge condition is met; `false` otherwise. + */ + override apply(user: Pokemon, target: Pokemon | null, move: Move, args: any[]): boolean { + const instantCharge = args[0]; + if (!(instantCharge instanceof Utils.BooleanHolder)) { + return false; + } + + if (this.condition(user, move)) { + instantCharge.value = true; + return true; + } + return false; + } +} + +/** + * Attribute that allows charge moves to resolve in 1 turn while specific {@linkcode WeatherType | Weather} + * is active. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`. + * @extends InstantChargeAttr + */ +export class WeatherInstantChargeAttr extends InstantChargeAttr { + constructor(weatherTypes: WeatherType[]) { + super((user, move) => { + const currentWeather = user.scene.arena.weather; + + if (Utils.isNullOrUndefined(currentWeather?.weatherType)) { + return false; + } else { + return !currentWeather?.isEffectSuppressed(user.scene) + && weatherTypes.includes(currentWeather?.weatherType); + } + }); + } +} + export class OverrideMoveEffectAttr extends MoveAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise { //const overridden = args[0] as Utils.BooleanHolder; @@ -2582,111 +2721,6 @@ export class OverrideMoveEffectAttr extends MoveAttr { } } -export class ChargeAttr extends OverrideMoveEffectAttr { - public chargeAnim: ChargeAnim; - private chargeText: string; - private tagType: BattlerTagType | null; - private chargeEffect: boolean; - public followUpPriority: integer | null; - - constructor(chargeAnim: ChargeAnim, chargeText: string, tagType?: BattlerTagType | null, chargeEffect: boolean = false) { - super(); - - this.chargeAnim = chargeAnim; - this.chargeText = chargeText; - this.tagType = tagType!; // TODO: is this bang correct? - this.chargeEffect = chargeEffect; - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - return new Promise(resolve => { - const lastMove = user.getLastXMoves().find(() => true); - if (!lastMove || lastMove.move !== move.id || (lastMove.result !== MoveResult.OTHER && lastMove.turn !== user.scene.currentBattle.turn)) { - (args[0] as Utils.BooleanHolder).value = true; - new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => { - user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user))); - if (this.tagType) { - user.addTag(this.tagType, 1, move.id, user.id); - } - if (this.chargeEffect) { - applyMoveAttrs(MoveEffectAttr, user, target, move); - } - user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER }); - user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true }); - user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id); - resolve(true); - }); - } else { - user.lapseTag(BattlerTagType.CHARGING); - resolve(false); - } - }); - } - - usedChargeEffect(user: Pokemon, target: Pokemon | null, move: Move): boolean { - if (!this.chargeEffect) { - return false; - } - // Account for move history being populated when this function is called - const lastMoves = user.getLastXMoves(2); - return lastMoves.length === 2 && lastMoves[1].move === move.id && lastMoves[1].result === MoveResult.OTHER; - } -} - -export class SunlightChargeAttr extends ChargeAttr { - constructor(chargeAnim: ChargeAnim, chargeText: string) { - super(chargeAnim, chargeText); - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - return new Promise(resolve => { - const weatherType = user.scene.arena.weather?.weatherType; - if (!user.scene.arena.weather?.isEffectSuppressed(user.scene) && (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN)) { - resolve(false); - } else { - super.apply(user, target, move, args).then(result => resolve(result)); - } - }); - } -} - -export class ElectroShotChargeAttr extends ChargeAttr { - private statIncreaseApplied: boolean; - constructor() { - super(ChargeAnim.ELECTRO_SHOT_CHARGING, i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" }), null, true); - // Add a flag because ChargeAttr skills use themselves twice instead of once over one-to-two turns - this.statIncreaseApplied = false; - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - return new Promise(resolve => { - const weatherType = user.scene.arena.weather?.weatherType; - if (!user.scene.arena.weather?.isEffectSuppressed(user.scene) && (weatherType === WeatherType.RAIN || weatherType === WeatherType.HEAVY_RAIN)) { - // Apply the SPATK increase every call when used in the rain - const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true); - statChangeAttr.apply(user, target, move, args); - // After the SPATK is raised, execute the move resolution e.g. deal damage - resolve(false); - } else { - if (!this.statIncreaseApplied) { - // Apply the SPATK increase only if it hasn't been applied before e.g. on the first turn charge up animation - const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true); - statChangeAttr.apply(user, target, move, args); - // Set the flag to true so that on the following turn it doesn't raise SPATK a second time - this.statIncreaseApplied = true; - } - super.apply(user, target, move, args).then(result => { - if (!result) { - // On the second turn, reset the statIncreaseApplied flag without applying the SPATK increase - this.statIncreaseApplied = false; - } - resolve(result); - }); - } - }); - } -} - export class DelayedAttackAttr extends OverrideMoveEffectAttr { public tagType: ArenaTagType; public chargeAnim: ChargeAnim; @@ -4878,6 +4912,37 @@ export const frenzyMissFunc: UserMoveConditionFunc = (user: Pokemon, move: Move) return true; }; +/** + * Attribute that grants {@link https://bulbapedia.bulbagarden.net/wiki/Semi-invulnerable_turn | semi-invulnerability} to the user during + * the associated move's charging phase. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`. + * @extends MoveEffectAttr + */ +export class SemiInvulnerableAttr extends MoveEffectAttr { + /** The type of {@linkcode SemiInvulnerableTag} to grant to the user */ + public tagType: BattlerTagType; + + constructor(tagType: BattlerTagType) { + super(true); + this.tagType = tagType; + } + + /** + * Grants a {@linkcode SemiInvulnerableTag} to the associated move's user. + * @param user the {@linkcode Pokemon} using the move + * @param target n/a + * @param move the {@linkcode Move} being used + * @param args n/a + * @returns `true` if semi-invulnerability was successfully granted; `false` otherwise. + */ + override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { + if (!super.apply(user, target, move, args)) { + return false; + } + + return user.addTag(this.tagType, 1, move.id, user.id); + } +} + export class AddBattlerTagAttr extends MoveEffectAttr { public tagType: BattlerTagType; public turnCountMin: integer; @@ -6138,7 +6203,7 @@ const lastMoveCopiableCondition: MoveConditionFunc = (user, target, move) => { return false; } - if (allMoves[copiableMove].hasAttr(ChargeAttr)) { + if (allMoves[copiableMove].isChargingMove()) { return false; } @@ -6286,7 +6351,7 @@ const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => { return false; } - if (allMoves[copiableMove.move].hasAttr(ChargeAttr) && copiableMove.result === MoveResult.OTHER) { + if (allMoves[copiableMove.move].isChargingMove() && copiableMove.result === MoveResult.OTHER) { return false; } @@ -6985,6 +7050,20 @@ function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null }); } +function applyMoveChargeAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, args: any[]): Promise { + return new Promise(resolve => { + const chargeAttrPromises: Promise[] = []; + const chargeMoveAttrs = move.chargeAttrs.filter(a => attrFilter(a)); + for (const attr of chargeMoveAttrs) { + const result = attr.apply(user, target, move, args); + if (result instanceof Promise) { + chargeAttrPromises.push(result); + } + } + Promise.allSettled(chargeAttrPromises).then(() => resolve()); + }); +} + export function applyMoveAttrs(attrType: Constructor, user: Pokemon | null, target: Pokemon | null, move: Move, ...args: any[]): Promise { return applyMoveAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args); } @@ -6993,6 +7072,10 @@ export function applyFilteredMoveAttrs(attrFilter: MoveAttrFilter, user: Pokemon return applyMoveAttrsInternal(attrFilter, user, target, move, args); } +export function applyMoveChargeAttrs(attrType: Constructor, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, ...args: any[]): Promise { + return applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args); +} + export class MoveCondition { protected func: MoveConditionFunc; @@ -7237,8 +7320,8 @@ export function initMoves() { new AttackMove(Moves.GUILLOTINE, Type.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) .attr(OneHitKOAttr) .attr(OneHitKOAccuracyAttr), - new AttackMove(Moves.RAZOR_WIND, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1) - .attr(ChargeAttr, ChargeAnim.RAZOR_WIND_CHARGING, i18next.t("moveTriggers:whippedUpAWhirlwind", { pokemonName: "{USER}" })) + new ChargingAttackMove(Moves.RAZOR_WIND, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1) + .chargeText(i18next.t("moveTriggers:whippedUpAWhirlwind", { pokemonName: "{USER}" })) .attr(HighCritAttr) .windMove() .ignoresVirtual() @@ -7258,8 +7341,9 @@ export function initMoves() { .hidesTarget() .windMove() .partial(), // Should force random switches - new AttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1) - .attr(ChargeAttr, ChargeAnim.FLY_CHARGING, i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }), BattlerTagType.FLYING) + new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1) + .chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) .condition(failOnGravityCondition) .ignoresVirtual(), new AttackMove(Moves.BIND, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1) @@ -7408,8 +7492,9 @@ export function initMoves() { .makesContact(false) .slicingMove() .target(MoveTarget.ALL_NEAR_ENEMIES), - new AttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1) - .attr(SunlightChargeAttr, ChargeAnim.SOLAR_BEAM_CHARGING, i18next.t("moveTriggers:tookInSunlight", { pokemonName: "{USER}" })) + new ChargingAttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1) + .chargeText(i18next.t("moveTriggers:tookInSunlight", { pokemonName: "{USER}" })) + .chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]) .attr(AntiSunlightPowerDecreaseAttr) .ignoresVirtual(), new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1) @@ -7458,8 +7543,9 @@ export function initMoves() { .attr(OneHitKOAccuracyAttr) .attr(HitsTagAttr, BattlerTagType.UNDERGROUND) .makesContact(false), - new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1) - .attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", { pokemonName: "{USER}" }), BattlerTagType.UNDERGROUND) + new ChargingAttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1) + .chargeText(i18next.t("moveTriggers:dugAHole", { pokemonName: "{USER}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND) .ignoresVirtual(), new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.TOXIC) @@ -7555,9 +7641,9 @@ export function initMoves() { .attr(TrapAttr, BattlerTagType.CLAMP), new AttackMove(Moves.SWIFT, Type.NORMAL, MoveCategory.SPECIAL, 60, -1, 20, -1, 0, 1) .target(MoveTarget.ALL_NEAR_ENEMIES), - new AttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1) - .attr(ChargeAttr, ChargeAnim.SKULL_BASH_CHARGING, i18next.t("moveTriggers:loweredItsHead", { pokemonName: "{USER}" }), null, true) - .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true) + new ChargingAttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1) + .chargeText(i18next.t("moveTriggers:loweredItsHead", { pokemonName: "{USER}" })) + .chargeAttr(StatStageChangeAttr, [ Stat.DEF ], 1, true) .ignoresVirtual(), new AttackMove(Moves.SPIKE_CANNON, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 15, -1, 0, 1) .attr(MultiHitAttr) @@ -7594,8 +7680,8 @@ export function initMoves() { .triageMove(), new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.SLEEP), - new AttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1) - .attr(ChargeAttr, ChargeAnim.SKY_ATTACK_CHARGING, i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) + new ChargingAttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1) + .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .attr(HighCritAttr) .attr(FlinchAttr) .makesContact(false) @@ -8060,9 +8146,10 @@ export function initMoves() { new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3) .makesContact(false) .attr(SecretPowerAttr), - new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3) - .attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }), BattlerTagType.UNDERWATER, true) - .attr(GulpMissileTagAttr) + new ChargingAttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3) + .chargeText(i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERWATER) + .chargeAttr(GulpMissileTagAttr) .ignoresVirtual(), new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3) .attr(MultiHitAttr), @@ -8195,8 +8282,9 @@ export function initMoves() { .attr(RechargeAttr), new SelfStatusMove(Moves.BULK_UP, Type.FIGHTING, -1, 20, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1, true), - new AttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3) - .attr(ChargeAttr, ChargeAnim.BOUNCE_CHARGING, i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" }), BattlerTagType.FLYING) + new ChargingAttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3) + .chargeText(i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .condition(failOnGravityCondition) .ignoresVirtual(), @@ -8551,8 +8639,9 @@ export function initMoves() { new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .windMove(), - new AttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4) - .attr(ChargeAttr, ChargeAnim.SHADOW_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }), BattlerTagType.HIDDEN) + new ChargingAttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4) + .chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN) .ignoresProtect() .ignoresVirtual(), new SelfStatusMove(Moves.HONE_CLAWS, Type.DARK, -1, 15, -1, 0, 5) @@ -8675,12 +8764,13 @@ export function initMoves() { .attr( MovePowerMultiplierAttr, (user, target, move) => target.status || target.hasAbility(Abilities.COMATOSE) ? 2 : 1), - new AttackMove(Moves.SKY_DROP, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) - .partial() // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/ - .attr(ChargeAttr, ChargeAnim.SKY_DROP_CHARGING, i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" }), BattlerTagType.FLYING) // TODO: Add 2nd turn message + new ChargingAttackMove(Moves.SKY_DROP, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) + .chargeText(i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) .condition(failOnGravityCondition) .condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE)) - .ignoresVirtual(), + .ignoresVirtual() + .partial(), // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/ new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5) .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true), @@ -8830,12 +8920,12 @@ export function initMoves() { new AttackMove(Moves.FIERY_DANCE, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, 50, 0, 5) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) .danceMove(), - new AttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5) - .attr(ChargeAttr, ChargeAnim.FREEZE_SHOCK_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingLight", { pokemonName: "{USER}" })) + new ChargingAttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5) + .chargeText(i18next.t("moveTriggers:becameCloakedInFreezingLight", { pokemonName: "{USER}" })) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .makesContact(false), - new AttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5) - .attr(ChargeAttr, ChargeAnim.ICE_BURN_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingAir", { pokemonName: "{USER}" })) + new ChargingAttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5) + .chargeText(i18next.t("moveTriggers:becameCloakedInFreezingAir", { pokemonName: "{USER}" })) .attr(StatusEffectAttr, StatusEffect.BURN) .ignoresVirtual(), new AttackMove(Moves.SNARL, Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5) @@ -8877,8 +8967,9 @@ export function initMoves() { .target(MoveTarget.ENEMY_SIDE), new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6) .attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ), - new AttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) - .attr(ChargeAttr, ChargeAnim.PHANTOM_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }), BattlerTagType.HIDDEN) + new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) + .chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN) .ignoresProtect() .ignoresVirtual(), new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) @@ -8988,8 +9079,8 @@ export function initMoves() { .ignoresSubstitute() .powderMove() .unimplemented(), - new SelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) - .attr(ChargeAttr, ChargeAnim.GEOMANCY_CHARGING, i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" })) + new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) + .chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" })) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) .ignoresVirtual(), new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) @@ -9198,8 +9289,9 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK ], -1) .condition((user, target, move) => target.getStatStage(Stat.ATK) > -6) .triageMove(), - new AttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7) - .attr(SunlightChargeAttr, ChargeAnim.SOLAR_BLADE_CHARGING, i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) + new ChargingAttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7) + .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) + .chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]) .attr(AntiSunlightPowerDecreaseAttr) .slicingMove(), new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7) @@ -9625,9 +9717,9 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true) .attr(MultiHitAttr) .makesContact(false), - new AttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, 100, 0, 8) - .attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" }), null, true) - .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) + new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8) + .chargeText(i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" })) + .chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) .ignoresVirtual(), new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8) .attr(ShellSideArmCategoryAttr) @@ -10079,8 +10171,10 @@ export function initMoves() { .attr(IvyCudgelTypeAttr) .attr(HighCritAttr) .makesContact(false), - new AttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9) - .attr(ElectroShotChargeAttr) + new ChargingAttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9) + .chargeText(i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" })) + .chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) + .chargeAttr(WeatherInstantChargeAttr, [ WeatherType.RAIN, WeatherType.HEAVY_RAIN ]) .ignoresVirtual(), new AttackMove(Moves.TERA_STARSTORM, Type.NORMAL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) .attr(TeraMoveCategoryAttr) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index a3d7429ed9b..6c4ae3b7ff9 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, ChargeAttr, 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 } 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"; @@ -2121,7 +2121,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // Trainers get a weight bump to stat buffing moves movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].getAttrs(StatStageChangeAttr).some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1) ]); // Trainers get a weight decrease to multiturn moves - movePool = movePool.map(m => [ m[0], m[1] * (!!allMoves[m[0]].hasAttr(ChargeAttr) || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1) ]); + movePool = movePool.map(m => [ m[0], m[1] * (!!allMoves[m[0]].isChargingMove() || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1) ]); } // Weight towards higher power moves, by reducing the power of moves below the highest power. diff --git a/src/phases/move-charge-phase.ts b/src/phases/move-charge-phase.ts new file mode 100644 index 00000000000..d1dc340b81b --- /dev/null +++ b/src/phases/move-charge-phase.ts @@ -0,0 +1,84 @@ +import BattleScene from "#app/battle-scene"; +import { BattlerIndex } from "#app/battle"; +import { MoveChargeAnim } from "#app/data/battle-anims"; +import { applyMoveChargeAttrs, MoveEffectAttr, InstantChargeAttr } from "#app/data/move"; +import Pokemon, { MoveResult, PokemonMove } from "#app/field/pokemon"; +import { BooleanHolder } from "#app/utils"; +import { MovePhase } from "#app/phases/move-phase"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MoveEndPhase } from "#app/phases/move-end-phase"; + +/** + * Phase for the "charging turn" of two-turn moves (e.g. Dig). + * @extends {@linkcode PokemonPhase} + */ +export class MoveChargePhase extends PokemonPhase { + /** The move instance that this phase applies */ + public move: PokemonMove; + /** The field index targeted by the move (Charging moves assume single target) */ + public targetIndex: BattlerIndex; + + constructor(scene: BattleScene, battlerIndex: BattlerIndex, targetIndex: BattlerIndex, move: PokemonMove) { + super(scene, battlerIndex); + this.move = move; + this.targetIndex = targetIndex; + } + + public override start() { + super.start(); + + const user = this.getUserPokemon(); + const target = this.getTargetPokemon(); + const move = this.move.getMove(); + + // If the target is somehow not defined, or the move is somehow not a ChargingMove, + // immediately end this phase. + if (!target || !(move.isChargingMove())) { + console.warn("Invalid parameters for MoveChargePhase"); + return super.end(); + } + + new MoveChargeAnim(move.chargeAnim, move.id, user).play(this.scene, false, () => { + move.showChargeText(user, target); + + applyMoveChargeAttrs(MoveEffectAttr, user, target, move).then(() => { + user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id); + this.end(); + }); + }); + } + + /** Checks the move's instant charge conditions, then ends this phase. */ + public override end() { + const user = this.getUserPokemon(); + const move = this.move.getMove(); + + if (move.isChargingMove()) { + const instantCharge = new BooleanHolder(false); + + applyMoveChargeAttrs(InstantChargeAttr, user, null, move, instantCharge); + + if (instantCharge.value) { + // this MoveEndPhase will be duplicated by the queued MovePhase if not removed + this.scene.tryRemovePhase((phase) => phase instanceof MoveEndPhase && phase.getPokemon() === user); + // queue a new MovePhase for this move's attack phase + this.scene.unshiftPhase(new MovePhase(this.scene, user, [ this.targetIndex ], this.move, false)); + } else { + user.getMoveQueue().push({ move: move.id, targets: [ this.targetIndex ]}); + } + + // Add this move's charging phase to the user's move history + user.pushMoveHistory({ move: this.move.moveId, targets: [ this.targetIndex ], result: MoveResult.OTHER }); + } + super.end(); + } + + public getUserPokemon(): Pokemon { + return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; + } + + public getTargetPokemon(): Pokemon | undefined { + return this.scene.getField(true).find((p) => this.targetIndex === p.getBattlerIndex()); + } +} diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 8d1a255d268..2b898f7d66b 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -4,7 +4,7 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, 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, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move"; +import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } 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"; @@ -24,10 +24,10 @@ export class MoveEffectPhase extends PokemonPhase { super(scene, battlerIndex); this.move = move; /** - * In double battles, if the right Pokemon selects a spread move and the left Pokemon dies - * with no party members available to switch in, then the right Pokemon takes the index - * of the left Pokemon and gets hit unless this is checked. - */ + * In double battles, if the right Pokemon selects a spread move and the left Pokemon dies + * with no party members available to switch in, then the right Pokemon takes the index + * of the left Pokemon and gets hit unless this is checked. + */ if (targets.includes(battlerIndex) && this.move.getMove().moveTarget === MoveTarget.ALL_NEAR_OTHERS) { const i = targets.indexOf(battlerIndex); targets.splice(i, i + 1); @@ -49,9 +49,9 @@ 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. - */ + * 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); /** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */ const move = this.move.getMove(); @@ -66,10 +66,10 @@ export class MoveEffectPhase extends PokemonPhase { user.lapseTags(BattlerTagLapseType.MOVE_EFFECT); /** - * If this phase is for the first hit of the invoked move, - * resolve the move's total hit count. This block combines the - * effects of the move itself, Parental Bond, and Multi-Lens to do so. - */ + * If this phase is for the first hit of the invoked move, + * resolve the move's total hit count. This block combines the + * effects of the move itself, Parental Bond, and Multi-Lens to do so. + */ if (user.turnData.hitsLeft === -1) { const hitCount = new Utils.IntegerHolder(1); // Assume single target for multi hit @@ -86,16 +86,16 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Log to be entered into the user's move history once the move result is resolved. - * Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully - * used in the sense of "Does it have an effect on the user?". - */ + * Log to be entered into the user's move history once the move result is resolved. + * Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully + * used in the sense of "Does it have an effect on the user?". + */ const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual }; /** - * Stores results of hit checks of the invoked move against all targets, organized by battler index. - * @see {@linkcode hitCheck} - */ + * Stores results of hit checks of the invoked move against all targets, organized by battler index. + * @see {@linkcode hitCheck} + */ const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ])); const hasActiveTargets = targets.some(t => t.isActive(true)); @@ -104,11 +104,10 @@ export class MoveEffectPhase extends PokemonPhase { && !targets[0].getTag(SemiInvulnerableTag); /** - * If no targets are left for the move to hit (FAIL), or the invoked move is single-target - * (and not random target) and failed the hit check against its target (MISS), log the move - * as FAILed or MISSed (depending on the conditions above) and end this phase. - */ - + * If no targets are left for the move to hit (FAIL), or the invoked move is single-target + * (and not random target) and failed the hit check against its target (MISS), log the move + * as FAILed or MISSed (depending on the conditions above) and end this phase. + */ if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { this.stopMultiHit(); if (hasActiveTargets) { @@ -154,9 +153,9 @@ export class MoveEffectPhase extends PokemonPhase { && !target.getTag(SemiInvulnerableTag); /** - * 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 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()]) { this.stopMultiHit(target); this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); @@ -177,23 +176,23 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Since all fail/miss checks have applied, the move is considered successfully applied. - * It's worth noting that if the move has no effect or is protected against, this assignment - * is overwritten and the move is logged as a FAIL. - */ + * Since all fail/miss checks have applied, the move is considered successfully applied. + * It's worth noting that if the move has no effect or is protected against, this assignment + * is overwritten and the move is logged as a FAIL. + */ moveHistoryEntry.result = MoveResult.SUCCESS; /** - * Stores the result of applying the invoked move to the target. - * If the target is protected, the result is always `NO_EFFECT`. - * Otherwise, the hit result is based on type effectiveness, immunities, - * and other factors that may negate the attack or status application. - * - * Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated - * (for attack moves) and the target's HP is updated. However, this isn't - * made visible to the user until the resulting {@linkcode DamagePhase} - * is invoked. - */ + * Stores the result of applying the invoked move to the target. + * If the target is protected, the result is always `NO_EFFECT`. + * Otherwise, the hit result is based on type effectiveness, immunities, + * and other factors that may negate the attack or status application. + * + * Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated + * (for attack moves) and the target's HP is updated. However, this isn't + * made visible to the user until the resulting {@linkcode DamagePhase} + * is invoked. + */ const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT; /** Does {@linkcode hitResult} indicate that damage was dealt to the target? */ @@ -211,9 +210,9 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * If the move has no effect on the target (i.e. the target is protected or immune), - * change the logged move result to FAIL. - */ + * If the move has no effect on the target (i.e. the target is protected or immune), + * change the logged move result to FAIL. + */ if (hitResult === HitResult.NO_EFFECT) { moveHistoryEntry.result = MoveResult.FAIL; } @@ -222,43 +221,41 @@ export class MoveEffectPhase extends PokemonPhase { const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); /** - * If the user can change forms by using the invoked move, - * it only changes forms after the move's last hit - * (see Relic Song's interaction with Parental Bond when used by Meloetta). - */ + * If the user can change forms by using the invoked move, + * it only changes forms after the move's last hit + * (see Relic Song's interaction with Parental Bond when used by Meloetta). + */ if (lastHit) { this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); } /** - * Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs. - * 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. - */ + * Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs. + * 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) { - /** Are the move's effects tied to the first turn of a charge move? */ - const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget() ?? null, move)); /** - * 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. - */ - Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY - && attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => { + * 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 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); @@ -267,7 +264,7 @@ export class MoveEffectPhase extends PokemonPhase { } } // If the move was not protected against, apply all HIT effects - Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT + 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(() => { @@ -286,9 +283,9 @@ export class MoveEffectPhase extends PokemonPhase { // 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 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); } @@ -343,12 +340,12 @@ export class MoveEffectPhase extends PokemonPhase { end() { const user = this.getUserPokemon(); /** - * If this phase isn't for the invoked move's last strike, - * unshift another MoveEffectPhase for the next strike. - * Otherwise, queue a message indicating the number of times the move has struck - * (if the move has struck more than once), then apply the heal from Shell Bell - * to the user. - */ + * If this phase isn't for the invoked move's last strike, + * unshift another MoveEffectPhase for the next strike. + * Otherwise, queue a message indicating the number of times the move has struck + * (if the move has struck more than once), then apply the heal from Shell Bell + * to the user. + */ if (user) { if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { this.scene.unshiftPhase(this.getNewHitPhase()); @@ -447,9 +444,9 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Removes the given {@linkcode Pokemon} from this phase's target list - * @param target {@linkcode Pokemon} the Pokemon to be removed - */ + * Removes the given {@linkcode Pokemon} from this phase's target list + * @param target {@linkcode Pokemon} the Pokemon to be removed + */ removeTarget(target: Pokemon): void { const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex()); if (targetIndex !== -1) { @@ -458,19 +455,19 @@ 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 - */ + * Prevents subsequent strikes of this phase's invoked move from occurring + * @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 */ 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. - */ + * 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? diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 66dbd06f5be..a516fd8593d 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -3,13 +3,13 @@ import BattleScene from "#app/battle-scene"; import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability"; import { CommonAnim } from "#app/data/battle-anims"; import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags"; -import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, ChargeAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move"; +import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, 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"; import { getTerrainBlockMessage } from "#app/data/weather"; import { MoveUsedEvent } from "#app/events/battle-scene"; -import Pokemon, { MoveResult, PokemonMove, TurnMove } from "#app/field/pokemon"; +import Pokemon, { MoveResult, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; import { BattlePhase } from "#app/phases/battle-phase"; @@ -23,6 +23,7 @@ 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; @@ -135,6 +136,8 @@ export class MovePhase extends BattlePhase { if (this.cancelled || this.failed) { this.handlePreMoveFailures(); + } else if (this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING)) { + this.chargeMove(); } else { this.useMove(); } @@ -226,12 +229,15 @@ export class MovePhase extends BattlePhase { this.showMoveText(); - // TODO: Clean up implementation of two-turn moves. if (moveQueue.length > 0) { // Using .shift here clears out two turn moves once they've been used this.ignorePp = moveQueue.shift()?.ignorePP ?? false; } + if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) { + this.pokemon.lapseTag(BattlerTagType.CHARGING); + } + // "commit" to using the move, deducting PP. if (!this.ignorePp) { const ppUsed = 1 + this.getPpIncreaseFromPressure(targets); @@ -295,6 +301,9 @@ export class MovePhase extends BattlePhase { } this.showFailedText(failedText); + + // Remove the user from its semi-invulnerable state (if applicable) + this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); } // Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`). @@ -306,6 +315,35 @@ export class MovePhase extends BattlePhase { } } + /** Queues a {@linkcode MoveChargePhase} for this phase's invoked move. */ + protected chargeMove() { + const move = this.move.getMove(); + const targets = this.getActiveTargetPokemon(); + + if (move.applyConditions(this.pokemon, targets[0], move)) { + // Protean and Libero apply on the charging turn of charge moves + applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove()); + + this.showMoveText(); + this.scene.unshiftPhase(new MoveChargePhase(this.scene, this.pokemon.getBattlerIndex(), this.targets[0], this.move)); + } 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(failedText); + + // Remove the user from its semi-invulnerable state (if applicable) + this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); + } + } + /** * Queues a {@linkcode MoveEndPhase} if the move wasn't a {@linkcode followUp} and {@linkcode canMove()} returns `true`, * then ends the phase. @@ -419,8 +457,6 @@ export class MovePhase extends BattlePhase { * - Lapses `AFTER_MOVE` tags: * - This handles the effects of {@link Moves.SUBSTITUTE Substitute} * - Removes the second turn of charge moves - * - * TODO: handle charge moves more gracefully */ protected handlePreMoveFailures(): void { if (this.cancelled || this.failed) { @@ -452,18 +488,7 @@ export class MovePhase extends BattlePhase { return; } - if (this.move.getMove().hasAttr(ChargeAttr)) { - const lastMove = this.pokemon.getLastXMoves() as TurnMove[]; - if (!lastMove.length || lastMove[0].move !== this.move.getMove().id || lastMove[0].result !== MoveResult.OTHER) { - this.scene.queueMessage(i18next.t("battle:useMove", { - pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), - moveName: this.move.getName() - }), 500); - return; - } - } - - if (this.pokemon.getTag(BattlerTagType.RECHARGING || BattlerTagType.INTERRUPTED)) { + if (this.pokemon.getTag(BattlerTagType.RECHARGING) || this.pokemon.getTag(BattlerTagType.INTERRUPTED)) { return; } diff --git a/src/test/abilities/volt_absorb.test.ts b/src/test/abilities/volt_absorb.test.ts index ec82b00ec5a..4fee7653b99 100644 --- a/src/test/abilities/volt_absorb.test.ts +++ b/src/test/abilities/volt_absorb.test.ts @@ -52,6 +52,7 @@ describe("Abilities - Volt Absorb", () => { expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined(); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }); + it("should activate regardless of accuracy checks", async () => { game.override.moveset(Moves.THUNDERBOLT); game.override.enemyMoveset(Moves.SPLASH); @@ -71,6 +72,7 @@ describe("Abilities - Volt Absorb", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); }); + it("regardless of accuracy should not trigger on pokemon in semi invulnerable state", async () => { game.override.moveset(Moves.THUNDERBOLT); game.override.enemyMoveset(Moves.DIVE); @@ -84,9 +86,7 @@ describe("Abilities - Volt Absorb", () => { game.move.select(Moves.THUNDERBOLT); enemyPokemon.hp = enemyPokemon.hp - 1; await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to("MoveEffectPhase"); - await game.move.forceMiss(); await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); }); diff --git a/src/test/arena/arena_gravity.test.ts b/src/test/arena/arena_gravity.test.ts index b6982896571..13e9c23a35c 100644 --- a/src/test/arena/arena_gravity.test.ts +++ b/src/test/arena/arena_gravity.test.ts @@ -1,8 +1,8 @@ +import { BattlerIndex } from "#app/battle"; import { allMoves } from "#app/data/move"; -import { Abilities } from "#app/enums/abilities"; -import { ArenaTagType } from "#app/enums/arena-tag-type"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -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 GameManager from "#test/utils/gameManager"; @@ -31,7 +31,8 @@ describe("Arena - Gravity", () => { .ability(Abilities.UNNERVE) .enemyAbility(Abilities.BALL_FETCH) .enemySpecies(Species.SHUCKLE) - .enemyMoveset(Moves.SPLASH); + .enemyMoveset(Moves.SPLASH) + .enemyLevel(5); }); // Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move) @@ -42,102 +43,121 @@ describe("Arena - Gravity", () => { vi.spyOn(moveToCheck, "calculateBattleAccuracy"); // Setup Gravity on first turn - await game.startBattle([ Species.PIKACHU ]); + await game.classicMode.startBattle([ Species.PIKACHU ]); game.move.select(Moves.GRAVITY); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); // Use non-OHKO move on second turn await game.toNextTurn(); game.move.select(Moves.TACKLE); - await game.phaseInterceptor.to(MoveEffectPhase); + await game.phaseInterceptor.to("MoveEffectPhase"); - expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(100 * 1.67); + expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(100 * 1.67); }); it("OHKO move accuracy is not affected", async () => { - game.override.startingLevel(5); - game.override.enemyLevel(5); - /** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */ const moveToCheck = allMoves[Moves.FISSURE]; vi.spyOn(moveToCheck, "calculateBattleAccuracy"); // Setup Gravity on first turn - await game.startBattle([ Species.PIKACHU ]); + await game.classicMode.startBattle([ Species.PIKACHU ]); game.move.select(Moves.GRAVITY); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); // Use OHKO move on second turn await game.toNextTurn(); game.move.select(Moves.FISSURE); - await game.phaseInterceptor.to(MoveEffectPhase); + await game.phaseInterceptor.to("MoveEffectPhase"); - expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(30); + expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(30); }); describe("Against flying types", () => { it("can be hit by ground-type moves now", async () => { game.override - .startingLevel(5) - .enemyLevel(5) .enemySpecies(Species.PIDGEOT) .moveset([ Moves.GRAVITY, Moves.EARTHQUAKE ]); - await game.startBattle([ Species.PIKACHU ]); + await game.classicMode.startBattle([ Species.PIKACHU ]); const pidgeot = game.scene.getEnemyPokemon()!; vi.spyOn(pidgeot, "getAttackTypeEffectiveness"); // Try earthquake on 1st turn (fails!); game.move.select(Moves.EARTHQUAKE); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); - expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(0); + expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(0); // Setup Gravity on 2nd turn await game.toNextTurn(); game.move.select(Moves.GRAVITY); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); // Use ground move on 3rd turn await game.toNextTurn(); game.move.select(Moves.EARTHQUAKE); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); - expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(1); + expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(1); }); it("keeps super-effective moves super-effective after using gravity", async () => { game.override - .startingLevel(5) - .enemyLevel(5) .enemySpecies(Species.PIDGEOT) .moveset([ Moves.GRAVITY, Moves.THUNDERBOLT ]); - await game.startBattle([ Species.PIKACHU ]); + await game.classicMode.startBattle([ Species.PIKACHU ]); const pidgeot = game.scene.getEnemyPokemon()!; vi.spyOn(pidgeot, "getAttackTypeEffectiveness"); // Setup Gravity on 1st turn game.move.select(Moves.GRAVITY); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); // Use electric move on 2nd turn await game.toNextTurn(); game.move.select(Moves.THUNDERBOLT); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); - expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(2); + expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(2); }); }); + + it("cancels Fly if its user is semi-invulnerable", async () => { + game.override + .enemySpecies(Species.SNORLAX) + .enemyMoveset(Moves.FLY) + .moveset([ Moves.GRAVITY, Moves.SPLASH ]); + + await game.classicMode.startBattle([ Species.CHARIZARD ]); + + const charizard = game.scene.getPlayerPokemon()!; + const snorlax = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SPLASH); + + await game.toNextTurn(); + expect(snorlax.getTag(BattlerTagType.FLYING)).toBeDefined(); + + game.move.select(Moves.GRAVITY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to("MoveEffectPhase"); + expect(snorlax.getTag(BattlerTagType.INTERRUPTED)).toBeDefined(); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(charizard.hp).toBe(charizard.getMaxHp()); + }); }); diff --git a/src/test/moves/dig.test.ts b/src/test/moves/dig.test.ts new file mode 100644 index 00000000000..4c6b5d3b75d --- /dev/null +++ b/src/test/moves/dig.test.ts @@ -0,0 +1,114 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import { MoveResult } from "#app/field/pokemon"; +import { describe, beforeAll, afterEach, beforeEach, it, expect } from "vitest"; +import GameManager from "#test/utils/gameManager"; + +describe("Moves - Dig", () => { + 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.DIG) + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + }); + + it("should make the user semi-invulnerable, then attack over 2 turns", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DIG); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeDefined(); + expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS); + expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp()); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.DIG); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + + const playerDig = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIG); + expect(playerDig?.ppUsed).toBe(1); + }); + + it("should not allow the user to evade attacks from Pokemon with No Guard", async () => { + game.override.enemyAbility(Abilities.NO_GUARD); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DIG); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + }); + + it("should not expend PP when the attack phase is cancelled", async () => { + game.override + .enemyAbility(Abilities.NO_GUARD) + .enemyMoveset(Moves.SPORE); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.DIG); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeUndefined(); + expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP); + + const playerDig = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIG); + expect(playerDig?.ppUsed).toBe(0); + }); + + it("should cause the user to take double damage from Earthquake", async () => { + await game.classicMode.startBattle([ Species.DONDOZO ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + const preDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage; + + game.move.select(Moves.DIG); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to("MoveEffectPhase"); + + const postDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage; + // these hopefully get avoid rounding errors :shrug: + expect(postDigEarthquakeDmg).toBeGreaterThanOrEqual(2 * preDigEarthquakeDmg); + expect(postDigEarthquakeDmg).toBeLessThan(2 * (preDigEarthquakeDmg + 1)); + }); +}); diff --git a/src/test/moves/dive.test.ts b/src/test/moves/dive.test.ts new file mode 100644 index 00000000000..b60416d7740 --- /dev/null +++ b/src/test/moves/dive.test.ts @@ -0,0 +1,137 @@ +import { BattlerTagType } from "#enums/battler-tag-type"; +import { StatusEffect } from "#enums/status-effect"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest"; +import { WeatherType } from "#enums/weather-type"; + +describe("Moves - Dive", () => { + 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.DIVE) + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + }); + + it("should make the user semi-invulnerable, then attack over 2 turns", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DIVE); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeDefined(); + expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS); + expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp()); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.DIVE); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + + const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE); + expect(playerDive?.ppUsed).toBe(1); + }); + + it("should not allow the user to evade attacks from Pokemon with No Guard", async () => { + game.override.enemyAbility(Abilities.NO_GUARD); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DIVE); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + }); + + it("should not expend PP when the attack phase is cancelled", async () => { + game.override + .enemyAbility(Abilities.NO_GUARD) + .enemyMoveset(Moves.SPORE); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.DIVE); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined(); + expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP); + + const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE); + expect(playerDive?.ppUsed).toBe(0); + }); + + it("should trigger on-contact post-defend ability effects", async () => { + game.override + .enemyAbility(Abilities.ROUGH_SKIN) + .enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DIVE); + + await game.phaseInterceptor.to("TurnEndPhase"); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + expect(enemyPokemon.battleData.abilitiesApplied[0]).toBe(Abilities.ROUGH_SKIN); + }); + + it("should cancel attack after Harsh Sunlight is set", async () => { + game.override.enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DIVE); + + await game.phaseInterceptor.to("TurnEndPhase"); + await game.phaseInterceptor.to("TurnStartPhase", false); + game.scene.arena.trySetWeather(WeatherType.HARSH_SUN, false); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined(); + + const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE); + expect(playerDive?.ppUsed).toBe(1); + }); +}); diff --git a/src/test/moves/electro_shot.test.ts b/src/test/moves/electro_shot.test.ts new file mode 100644 index 00000000000..1373b4941eb --- /dev/null +++ b/src/test/moves/electro_shot.test.ts @@ -0,0 +1,104 @@ +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Stat } from "#enums/stat"; +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, it, expect } from "vitest"; + +describe("Moves - Electro Shot", () => { + 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.ELECTRO_SHOT) + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should increase the user's Sp. Atk on the first turn, then attack on the second turn", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.ELECTRO_SHOT); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeDefined(); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + + const playerElectroShot = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.ELECTRO_SHOT); + expect(playerElectroShot?.ppUsed).toBe(1); + }); + + it.each([ + { weatherType: WeatherType.RAIN, name: "Rain" }, + { weatherType: WeatherType.HEAVY_RAIN, name: "Heavy Rain" } + ])("should fully resolve in one turn if $name is active", async ({ weatherType }) => { + game.override.weather(weatherType); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.ELECTRO_SHOT); + + await game.phaseInterceptor.to("MoveEffectPhase", false); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + + const playerElectroShot = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.ELECTRO_SHOT); + expect(playerElectroShot?.ppUsed).toBe(1); + }); + + it("should only increase Sp. Atk once with Multi-Lens", async () => { + game.override + .weather(WeatherType.RAIN) + .startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.ELECTRO_SHOT); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.turnData.hitCount).toBe(2); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); + }); +}); diff --git a/src/test/moves/fly.test.ts b/src/test/moves/fly.test.ts new file mode 100644 index 00000000000..6ae758fe3dc --- /dev/null +++ b/src/test/moves/fly.test.ts @@ -0,0 +1,122 @@ +import { BattlerTagType } from "#enums/battler-tag-type"; +import { StatusEffect } from "#enums/status-effect"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; + +describe("Moves - Fly", () => { + 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.FLY) + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + + vi.spyOn(allMoves[Moves.FLY], "accuracy", "get").mockReturnValue(100); + }); + + it("should make the user semi-invulnerable, then attack over 2 turns", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.FLY); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeDefined(); + expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS); + expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp()); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.FLY); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + + const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY); + expect(playerFly?.ppUsed).toBe(1); + }); + + it("should not allow the user to evade attacks from Pokemon with No Guard", async () => { + game.override.enemyAbility(Abilities.NO_GUARD); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.FLY); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + }); + + it("should not expend PP when the attack phase is cancelled", async () => { + game.override + .enemyAbility(Abilities.NO_GUARD) + .enemyMoveset(Moves.SPORE); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.FLY); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined(); + expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP); + + const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY); + expect(playerFly?.ppUsed).toBe(0); + }); + + it("should be cancelled when another Pokemon uses Gravity", async () => { + game.override.enemyMoveset([ Moves.SPLASH, Moves.GRAVITY ]); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.FLY); + + await game.forceEnemyMove(Moves.SPLASH); + + await game.toNextTurn(); + await game.forceEnemyMove(Moves.GRAVITY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + + const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY); + expect(playerFly?.ppUsed).toBe(0); + }); +}); diff --git a/src/test/moves/geomancy.test.ts b/src/test/moves/geomancy.test.ts new file mode 100644 index 00000000000..6e2f40b9144 --- /dev/null +++ b/src/test/moves/geomancy.test.ts @@ -0,0 +1,78 @@ +import { EffectiveStat, Stat } from "#enums/stat"; +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, it, expect } from "vitest"; + +describe("Moves - Geomancy", () => { + 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.GEOMANCY) + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should boost the user's stats on the second turn of use", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const player = game.scene.getPlayerPokemon()!; + const affectedStats: EffectiveStat[] = [ Stat.SPATK, Stat.SPDEF, Stat.SPD ]; + + game.move.select(Moves.GEOMANCY); + + await game.phaseInterceptor.to("TurnEndPhase"); + affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(0)); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER); + + await game.phaseInterceptor.to("TurnEndPhase"); + affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(2)); + expect(player.getMoveHistory()).toHaveLength(2); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + + const playerGeomancy = player.getMoveset().find((mv) => mv && mv.moveId === Moves.GEOMANCY); + expect(playerGeomancy?.ppUsed).toBe(1); + }); + + it("should execute over 2 turns between waves", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const player = game.scene.getPlayerPokemon()!; + const affectedStats: EffectiveStat[] = [ Stat.SPATK, Stat.SPDEF, Stat.SPD ]; + + game.move.select(Moves.GEOMANCY); + + await game.phaseInterceptor.to("MoveEndPhase", false); + await game.doKillOpponents(); + + await game.toNextWave(); + + await game.phaseInterceptor.to("TurnEndPhase"); + affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(2)); + expect(player.getMoveHistory()).toHaveLength(2); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + + const playerGeomancy = player.getMoveset().find((mv) => mv && mv.moveId === Moves.GEOMANCY); + expect(playerGeomancy?.ppUsed).toBe(1); + }); +}); diff --git a/src/test/moves/solar_beam.test.ts b/src/test/moves/solar_beam.test.ts new file mode 100644 index 00000000000..ebec338932a --- /dev/null +++ b/src/test/moves/solar_beam.test.ts @@ -0,0 +1,102 @@ +import { allMoves } from "#app/data/move"; +import { BattlerTagType } from "#enums/battler-tag-type"; +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, it, expect, vi } from "vitest"; + +describe("Moves - Solar 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.SOLAR_BEAM) + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should deal damage in two turns if no weather is active", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SOLAR_BEAM); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeDefined(); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + + const playerSolarBeam = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.SOLAR_BEAM); + expect(playerSolarBeam?.ppUsed).toBe(1); + }); + + it.each([ + { weatherType: WeatherType.SUNNY, name: "Sun" }, + { weatherType: WeatherType.HARSH_SUN, name: "Harsh Sun" } + ])("should deal damage in one turn if $name is active", async ({ weatherType }) => { + game.override.weather(weatherType); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SOLAR_BEAM); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + + const playerSolarBeam = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.SOLAR_BEAM); + expect(playerSolarBeam?.ppUsed).toBe(1); + }); + + it.each([ + { weatherType: WeatherType.RAIN, name: "Rain" }, + { weatherType: WeatherType.HEAVY_RAIN, name: "Heavy Rain" } + ])("should have its power halved in $name", async ({ weatherType }) => { + game.override.weather(weatherType); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const solarBeam = allMoves[Moves.SOLAR_BEAM]; + + vi.spyOn(solarBeam, "calculateBattlePower"); + + game.move.select(Moves.SOLAR_BEAM); + + await game.phaseInterceptor.to("TurnEndPhase"); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(solarBeam.calculateBattlePower).toHaveLastReturnedWith(60); + }); +}); diff --git a/src/test/moves/whirlwind.test.ts b/src/test/moves/whirlwind.test.ts index cc31b2591a2..c16f38111f2 100644 --- a/src/test/moves/whirlwind.test.ts +++ b/src/test/moves/whirlwind.test.ts @@ -41,7 +41,8 @@ describe("Moves - Whirlwind", () => { const staraptor = game.scene.getPlayerPokemon()!; game.move.select(move); - await game.toNextTurn(); + + await game.phaseInterceptor.to("BerryPhase", false); expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined(); expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS); From fd38ab4cb48cca69ac40bda26fdfe49fe334647c Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Wed, 23 Oct 2024 08:10:43 -0700 Subject: [PATCH 12/17] [P2] Missing Minior form (violet) now spawns in the wild (#4711) --- src/battle-scene.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index a21e1b09342..3cbf4d7b422 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1387,7 +1387,7 @@ export default class BattleScene extends SceneBase { case Species.ZYGARDE: return Utils.randSeedInt(4); case Species.MINIOR: - return Utils.randSeedInt(6); + return Utils.randSeedInt(7); case Species.ALCREMIE: return Utils.randSeedInt(9); case Species.MEOWSTIC: From 958d79140c704e76b0d20165e1130e3bb6e5dff5 Mon Sep 17 00:00:00 2001 From: schmidtc1 <62030095+schmidtc1@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:12:53 -0400 Subject: [PATCH 13/17] [P2] Fixes Transform/Imposter not updating type/battle stat changes immediately; set move PP to 5 when transforming (#3462) * Adds updateInfo to transform move/ability, mirrors Transform functionality in Imposter * Implements functionality for reducing pp to 5 or less for each move when transforming * Refactors to async/await pattern, adds back removed anims/sounds from last commit * Eslint fix attempt * Update src/data/ability.ts per DayKev's suggestion Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Merge and fix conflicts * Adds unit tests for pp-change with transform/imposter * Updates to consistency in syntax/deprecated code --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- public/locales | 2 +- src/data/ability.ts | 22 ++++++---- src/data/move.ts | 67 ++++++++++++++++------------- src/test/abilities/imposter.test.ts | 22 +++++++--- src/test/moves/transform.test.ts | 22 +++++++--- 5 files changed, 84 insertions(+), 51 deletions(-) diff --git a/public/locales b/public/locales index fc4a1effd51..3ccef8472dd 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef +Subproject commit 3ccef8472dd7cc7c362538489954cb8fdad27e5f diff --git a/src/data/ability.ts b/src/data/ability.ts index cc95045f8b7..d761657f5cd 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2422,11 +2422,12 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { super(true); } - applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { + async applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): Promise { const targets = pokemon.getOpponents(); if (simulated || !targets.length) { return simulated; } + const promises: Promise[] = []; let target: Pokemon; if (targets.length > 1) { @@ -2435,7 +2436,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { target = targets[0]; } - target = target!; // compiler doesn't know its guranteed to be defined + target = target!; pokemon.summonData.speciesForm = target.getSpeciesForm(); pokemon.summonData.fusionSpeciesForm = target.getFusionSpeciesForm(); pokemon.summonData.ability = target.getAbility().id; @@ -2452,18 +2453,23 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { pokemon.setStatStage(s, target.getStatStage(s)); } - pokemon.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId ?? Moves.NONE, m?.ppUsed, m?.ppUp)); + pokemon.summonData.moveset = target.getMoveset().map(m => { + const pp = m?.getMove().pp ?? 0; + // if PP value is less than 5, do nothing. If greater, we need to reduce the value to 5 using a negative ppUp value. + const ppUp = pp <= 5 ? 0 : (5 - pp) / Math.max(Math.floor(pp / 5), 1); + return new PokemonMove(m?.moveId ?? Moves.NONE, 0, ppUp); + }); pokemon.summonData.types = target.getTypes(); + promises.push(pokemon.updateInfo()); - + pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target!.name, })); pokemon.scene.playSound("battle_anims/PRSFX- Transform"); - - pokemon.loadAssets(false).then(() => { + promises.push(pokemon.loadAssets(false).then(() => { pokemon.playAnim(); pokemon.updateInfo(); - }); + })); - pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target.name, })); + await Promise.all(promises); return true; } diff --git a/src/data/move.ts b/src/data/move.ts index cf88fad0ac5..efdd4568927 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6643,42 +6643,49 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr { } export class TransformAttr extends MoveEffectAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - return new Promise(resolve => { - if (!super.apply(user, target, move, args)) { - return resolve(false); - } + async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { + if (!super.apply(user, target, move, args)) { + return false; + } - 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(); + 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); + // Power Trick's effect will not preserved after using Transform + user.removeTag(BattlerTagType.POWER_TRICK); - // Copy all stats (except HP) - for (const s of EFFECTIVE_STATS) { - user.setStat(s, target.getStat(s, false), false); - } + // Copy all stats (except HP) + for (const s of EFFECTIVE_STATS) { + user.setStat(s, target.getStat(s, false), false); + } - // Copy all stat stages - for (const s of BATTLE_STATS) { - user.setStatStage(s, target.getStatStage(s)); - } + // Copy all stat stages + for (const s of BATTLE_STATS) { + user.setStatStage(s, target.getStatStage(s)); + } - user.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId!, m?.ppUsed, m?.ppUp)); // TODO: is this bang correct? - user.summonData.types = target.getTypes(); - - user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })); - - user.loadAssets(false).then(() => { - user.playAnim(); - user.updateInfo(); - resolve(true); - }); + user.summonData.moveset = target.getMoveset().map(m => { + const pp = m?.getMove().pp ?? 0; + // if PP value is less than 5, do nothing. If greater, we need to reduce the value to 5 using a negative ppUp value. + const ppUp = pp <= 5 ? 0 : (5 - pp) / Math.max(Math.floor(pp / 5), 1); + return new PokemonMove(m?.moveId!, 0, ppUp); }); + user.summonData.types = target.getTypes(); + promises.push(user.updateInfo()); + + user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })); + + promises.push(user.loadAssets(false).then(() => { + user.playAnim(); + user.updateInfo(); + })); + + await Promise.all(promises); + return true; } } diff --git a/src/test/abilities/imposter.test.ts b/src/test/abilities/imposter.test.ts index b7b8e0c5cca..7aaac5ca8c4 100644 --- a/src/test/abilities/imposter.test.ts +++ b/src/test/abilities/imposter.test.ts @@ -36,9 +36,7 @@ describe("Abilities - Imposter", () => { }); it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => { - await game.startBattle([ - Species.DITTO - ]); + await game.classicMode.startBattle([ Species.DITTO ]); game.move.select(Moves.SPLASH); await game.phaseInterceptor.to(TurnEndPhase); @@ -78,9 +76,7 @@ describe("Abilities - Imposter", () => { it("should copy in-battle overridden stats", async () => { game.override.enemyMoveset([ Moves.POWER_SPLIT ]); - await game.startBattle([ - Species.DITTO - ]); + await game.classicMode.startBattle([ Species.DITTO ]); const player = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -97,4 +93,18 @@ describe("Abilities - Imposter", () => { expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); }); + + it("should set each move's pp to a maximum of 5", async () => { + game.override.enemyMoveset([ Moves.SWORDS_DANCE, Moves.GROWL, Moves.SKETCH, Moves.RECOVER ]); + + await game.classicMode.startBattle([ Species.DITTO ]); + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.TACKLE); + await game.phaseInterceptor.to(TurnEndPhase); + + player.getMoveset().forEach(move => { + expect(move!.getMovePp()).toBeLessThanOrEqual(5); + }); + }); }); diff --git a/src/test/moves/transform.test.ts b/src/test/moves/transform.test.ts index 079fdfa5685..8c0f5eda7b2 100644 --- a/src/test/moves/transform.test.ts +++ b/src/test/moves/transform.test.ts @@ -36,9 +36,7 @@ describe("Moves - Transform", () => { }); it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => { - await game.startBattle([ - Species.DITTO - ]); + await game.classicMode.startBattle([ Species.DITTO ]); game.move.select(Moves.TRANSFORM); await game.phaseInterceptor.to(TurnEndPhase); @@ -78,9 +76,7 @@ describe("Moves - Transform", () => { it("should copy in-battle overridden stats", async () => { game.override.enemyMoveset([ Moves.POWER_SPLIT ]); - await game.startBattle([ - Species.DITTO - ]); + await game.classicMode.startBattle([ Species.DITTO ]); const player = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -97,4 +93,18 @@ describe("Moves - Transform", () => { expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); }); + + it ("should set each move's pp to a maximum of 5", async () => { + game.override.enemyMoveset([ Moves.SWORDS_DANCE, Moves.GROWL, Moves.SKETCH, Moves.RECOVER ]); + + await game.classicMode.startBattle([ Species.DITTO ]); + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.TRANSFORM); + await game.phaseInterceptor.to(TurnEndPhase); + + player.getMoveset().forEach(move => { + expect(move!.getMovePp()).toBeLessThanOrEqual(5); + }); + }); }); From c7e9eaf43510919e97051e9bcd6dfde97dde0b20 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Wed, 23 Oct 2024 08:24:50 -0700 Subject: [PATCH 14/17] [P2] Fix binding, etc. not being removed when switching with Baton Pass (#4709) * Fix binding, etc. not being removed when switching with Baton Pass * New baton pass test --- src/phases/switch-summon-phase.ts | 3 ++- src/test/moves/baton_pass.test.ts | 28 ++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 07761b10d6e..37652b3cfa4 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -65,8 +65,9 @@ export class SwitchSummonPhase extends SummonPhase { const pokemon = this.getPokemon(); + (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); + if (this.switchType === SwitchType.SWITCH) { - (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); const substitute = pokemon.getTag(SubstituteTag); if (substitute) { this.scene.tweens.add({ diff --git a/src/test/moves/baton_pass.test.ts b/src/test/moves/baton_pass.test.ts index 5e6e4be21ba..9d4a9358715 100644 --- a/src/test/moves/baton_pass.test.ts +++ b/src/test/moves/baton_pass.test.ts @@ -34,7 +34,7 @@ describe("Moves - Baton Pass", () => { .disableCrits(); }); - it("transfers all stat stages when player uses it", async() => { + it("transfers all stat stages when player uses it", async () => { // arrange await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]); @@ -91,7 +91,7 @@ describe("Moves - Baton Pass", () => { ]); }, 20000); - it("doesn't transfer effects that aren't transferrable", async() => { + it("doesn't transfer effects that aren't transferrable", async () => { game.override.enemyMoveset([ Moves.SALT_CURE ]); await game.classicMode.startBattle([ Species.PIKACHU, Species.FEEBAS ]); @@ -106,4 +106,28 @@ describe("Moves - Baton Pass", () => { expect(player2.findTag((t) => t.tagType === BattlerTagType.SALT_CURED)).toBeUndefined(); }, 20000); + + it("doesn't allow binding effects from the user to persist", async () => { + game.override.moveset([ Moves.FIRE_SPIN, Moves.BATON_PASS ]); + + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.FIRE_SPIN); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.move.forceHit(); + + await game.toNextTurn(); + + expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined(); + + game.move.select(Moves.BATON_PASS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeUndefined(); + }); }); From a13550ec4464b9147622aa87d9ce32fc32446d34 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Wed, 23 Oct 2024 21:46:57 +0200 Subject: [PATCH 15/17] [Balance][ME] Various ME Balance changes (#4700) * balance changes and updates to various MEs * fix import to new item * fix import to new item * Update src/data/mystery-encounters/utils/encounter-pokemon-utils.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/phases/select-modifier-phase.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/modifier/modifier.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/modifier/modifier.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * revert item atlas changes * eslint * revert 'revert item atlas' * update locale repo to latest commit * Fix fiery fallout missing argument * [balance] Training session ME does not update Seen/Defeated GameStats * [balance] update Weird Dream ME maximum spawn wave * [ME] update CombinationRequirements to allow AND or OR combinations * refactor: CombinationPokemonRequirement `.Some()` and `Every()` * chore: rename `orRequirements` to `requirements` * fix: returns of `Some()` and `Any()` * apply `Some()` / `Any()` pattern to `CombinationSceneRequirement` too * revert 'offer you can't refuse' giving Silver Pokeball' * Apply code review suggestions * [me] Weird Dream: apply same old gateau logic to team in options 1 and 2 --------- Co-authored-by: ImperialSympathizer Co-authored-by: ImperialSympathizer <110984302+ben-lear@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: innerthunder --- public/images/items.json | 797 +++++++++--------- public/images/items.png | Bin 58527 -> 58657 bytes public/images/items/pb_silver.png | Bin 0 -> 556 bytes public/images/trainer/future_self_f.json | 41 + public/images/trainer/future_self_f.png | Bin 0 -> 664 bytes public/images/trainer/future_self_m.json | 41 + public/images/trainer/future_self_m.png | Bin 0 -> 695 bytes public/locales | 2 +- src/data/balance/pokemon-evolutions.ts | 6 +- .../an-offer-you-cant-refuse-encounter.ts | 15 +- .../encounters/bug-type-superfan-encounter.ts | 26 +- .../encounters/clowning-around-encounter.ts | 15 +- .../encounters/dark-deal-encounter.ts | 10 +- .../encounters/delibirdy-encounter.ts | 25 +- .../encounters/fiery-fallout-encounter.ts | 78 +- .../mysterious-challengers-encounter.ts | 8 +- .../shady-vitamin-dealer-encounter.ts | 2 +- .../the-expert-pokemon-breeder-encounter.ts | 27 +- .../encounters/training-session-encounter.ts | 1 + .../encounters/trash-to-treasure-encounter.ts | 2 +- .../encounters/weird-dream-encounter.ts | 381 ++++++--- .../mystery-encounter-requirements.ts | 127 ++- .../mystery-encounters/mystery-encounter.ts | 14 + .../requirements/requirement-groups.ts | 17 + .../utils/encounter-pokemon-utils.ts | 20 + src/data/trainer-config.ts | 18 +- src/enums/trainer-type.ts | 2 + src/field/pokemon.ts | 2 - src/modifier/modifier-type.ts | 9 +- src/modifier/modifier.ts | 66 +- src/phases/select-modifier-phase.ts | 11 +- src/phases/victory-phase.ts | 9 +- src/system/game-data.ts | 4 + src/system/pokemon-data.ts | 3 +- .../encounters/delibirdy-encounter.test.ts | 48 +- .../fiery-fallout-encounter.test.ts | 47 +- .../trash-to-treasure-encounter.test.ts | 2 +- .../encounters/weird-dream-encounter.test.ts | 67 +- 38 files changed, 1244 insertions(+), 699 deletions(-) create mode 100644 public/images/items/pb_silver.png create mode 100644 public/images/trainer/future_self_f.json create mode 100644 public/images/trainer/future_self_f.png create mode 100644 public/images/trainer/future_self_m.json create mode 100644 public/images/trainer/future_self_m.png diff --git a/public/images/items.json b/public/images/items.json index 05d021b6a06..3c9cff7a35a 100644 --- a/public/images/items.json +++ b/public/images/items.json @@ -6583,7 +6583,7 @@ } }, { - "filename": "rb", + "filename": "pb_silver", "rotated": false, "trimmed": true, "sourceSize": { @@ -6666,6 +6666,27 @@ "h": 19 } }, + { + "filename": "rb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 254, + "y": 320, + "w": 20, + "h": 20 + } + }, { "filename": "smooth_meteorite", "rotated": false, @@ -6680,27 +6701,6 @@ "w": 20, "h": 20 }, - "frame": { - "x": 254, - "y": 320, - "w": 20, - "h": 20 - } - }, - { - "filename": "strange_ball", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, "frame": { "x": 274, "y": 325, @@ -6709,7 +6709,7 @@ } }, { - "filename": "ub", + "filename": "strange_ball", "rotated": false, "trimmed": true, "sourceSize": { @@ -6751,7 +6751,7 @@ } }, { - "filename": "apicot_berry", + "filename": "ub", "rotated": false, "trimmed": true, "sourceSize": { @@ -6761,13 +6761,13 @@ "spriteSourceSize": { "x": 6, "y": 6, - "w": 19, + "w": 20, "h": 20 }, "frame": { "x": 221, "y": 337, - "w": 19, + "w": 20, "h": 20 } }, @@ -6793,7 +6793,7 @@ } }, { - "filename": "big_mushroom", + "filename": "apicot_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -6804,13 +6804,13 @@ "x": 6, "y": 6, "w": 19, - "h": 19 + "h": 20 }, "frame": { "x": 343, "y": 287, "w": 19, - "h": 19 + "h": 20 } }, { @@ -6834,6 +6834,27 @@ "h": 20 } }, + { + "filename": "big_mushroom", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 19, + "h": 19 + }, + "frame": { + "x": 318, + "y": 306, + "w": 19, + "h": 19 + } + }, { "filename": "hard_stone", "rotated": false, @@ -6849,33 +6870,12 @@ "h": 20 }, "frame": { - "x": 318, - "y": 306, + "x": 314, + "y": 325, "w": 19, "h": 20 } }, - { - "filename": "miracle_seed", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 19, - "h": 19 - }, - "frame": { - "x": 337, - "y": 306, - "w": 19, - "h": 19 - } - }, { "filename": "wl_ability_urge", "rotated": false, @@ -6891,12 +6891,33 @@ "h": 18 }, "frame": { - "x": 314, - "y": 326, + "x": 337, + "y": 307, "w": 20, "h": 18 } }, + { + "filename": "miracle_seed", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 19, + "h": 19 + }, + "frame": { + "x": 333, + "y": 325, + "w": 19, + "h": 19 + } + }, { "filename": "wl_antidote", "rotated": false, @@ -6912,12 +6933,54 @@ "h": 18 }, "frame": { - "x": 356, + "x": 357, "y": 307, "w": 20, "h": 18 } }, + { + "filename": "wl_awakening", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 352, + "y": 325, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_burn_heal", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 241, + "y": 340, + "w": 20, + "h": 18 + } + }, { "filename": "golden_egg", "rotated": false, @@ -6933,33 +6996,12 @@ "h": 20 }, "frame": { - "x": 376, - "y": 307, + "x": 372, + "y": 325, "w": 17, "h": 20 } }, - { - "filename": "wl_awakening", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 393, - "y": 241, - "w": 20, - "h": 18 - } - }, { "filename": "toxic_orb", "rotated": false, @@ -6975,96 +7017,12 @@ "h": 18 }, "frame": { - "x": 413, - "y": 258, + "x": 377, + "y": 307, "w": 18, "h": 18 } }, - { - "filename": "wl_burn_heal", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 393, - "y": 259, - "w": 20, - "h": 18 - } - }, - { - "filename": "ampharosite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 377, - "y": 243, - "w": 16, - "h": 16 - } - }, - { - "filename": "audinite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 377, - "y": 259, - "w": 16, - "h": 16 - } - }, - { - "filename": "relic_gold", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 9, - "y": 11, - "w": 15, - "h": 11 - }, - "frame": { - "x": 377, - "y": 275, - "w": 15, - "h": 11 - } - }, { "filename": "lucky_egg", "rotated": false, @@ -7080,8 +7038,8 @@ "h": 20 }, "frame": { - "x": 381, - "y": 286, + "x": 389, + "y": 325, "w": 17, "h": 20 } @@ -7101,8 +7059,8 @@ "h": 18 }, "frame": { - "x": 398, - "y": 277, + "x": 352, + "y": 343, "w": 20, "h": 18 } @@ -7122,8 +7080,8 @@ "h": 18 }, "frame": { - "x": 398, - "y": 295, + "x": 372, + "y": 345, "w": 20, "h": 18 } @@ -7143,8 +7101,8 @@ "h": 18 }, "frame": { - "x": 393, - "y": 313, + "x": 392, + "y": 345, "w": 20, "h": 18 } @@ -7164,14 +7122,14 @@ "h": 18 }, "frame": { - "x": 240, - "y": 340, + "x": 378, + "y": 241, "w": 20, "h": 18 } }, { - "filename": "banettite", + "filename": "relic_gold", "rotated": false, "trimmed": true, "sourceSize": { @@ -7179,16 +7137,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 + "x": 9, + "y": 11, + "w": 15, + "h": 11 }, "frame": { - "x": 413, - "y": 313, - "w": 16, - "h": 16 + "x": 398, + "y": 241, + "w": 15, + "h": 11 } }, { @@ -7206,8 +7164,8 @@ "h": 18 }, "frame": { - "x": 202, - "y": 358, + "x": 377, + "y": 259, "w": 20, "h": 18 } @@ -7227,8 +7185,8 @@ "h": 18 }, "frame": { - "x": 201, - "y": 376, + "x": 381, + "y": 277, "w": 20, "h": 18 } @@ -7248,8 +7206,8 @@ "h": 18 }, "frame": { - "x": 201, - "y": 394, + "x": 397, + "y": 259, "w": 20, "h": 18 } @@ -7269,33 +7227,12 @@ "h": 18 }, "frame": { - "x": 201, - "y": 412, + "x": 401, + "y": 277, "w": 20, "h": 18 } }, - { - "filename": "beedrillite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 222, - "y": 357, - "w": 16, - "h": 16 - } - }, { "filename": "wl_ice_heal", "rotated": false, @@ -7311,14 +7248,14 @@ "h": 18 }, "frame": { - "x": 238, - "y": 358, + "x": 395, + "y": 295, "w": 20, "h": 18 } }, { - "filename": "blastoisinite", + "filename": "ampharosite", "rotated": false, "trimmed": true, "sourceSize": { @@ -7332,8 +7269,29 @@ "h": 16 }, "frame": { - "x": 222, - "y": 373, + "x": 415, + "y": 295, + "w": 16, + "h": 16 + } + }, + { + "filename": "audinite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 415, + "y": 311, "w": 16, "h": 16 } @@ -7353,12 +7311,33 @@ "h": 18 }, "frame": { - "x": 221, - "y": 389, + "x": 406, + "y": 327, "w": 20, "h": 18 } }, + { + "filename": "banettite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 412, + "y": 345, + "w": 16, + "h": 16 + } + }, { "filename": "wl_item_urge", "rotated": false, @@ -7375,7 +7354,7 @@ }, "frame": { "x": 221, - "y": 407, + "y": 357, "w": 20, "h": 18 } @@ -7396,7 +7375,7 @@ }, "frame": { "x": 241, - "y": 376, + "y": 358, "w": 20, "h": 18 } @@ -7416,8 +7395,8 @@ "h": 18 }, "frame": { - "x": 241, - "y": 394, + "x": 201, + "y": 360, "w": 20, "h": 18 } @@ -7437,8 +7416,8 @@ "h": 18 }, "frame": { - "x": 241, - "y": 412, + "x": 201, + "y": 378, "w": 20, "h": 18 } @@ -7458,8 +7437,8 @@ "h": 18 }, "frame": { - "x": 258, - "y": 358, + "x": 221, + "y": 375, "w": 20, "h": 18 } @@ -7479,8 +7458,8 @@ "h": 18 }, "frame": { - "x": 261, - "y": 376, + "x": 201, + "y": 396, "w": 20, "h": 18 } @@ -7500,8 +7479,8 @@ "h": 18 }, "frame": { - "x": 261, - "y": 394, + "x": 221, + "y": 393, "w": 20, "h": 18 } @@ -7521,8 +7500,8 @@ "h": 18 }, "frame": { - "x": 261, - "y": 412, + "x": 241, + "y": 376, "w": 20, "h": 18 } @@ -7542,12 +7521,33 @@ "h": 18 }, "frame": { - "x": 278, - "y": 345, + "x": 241, + "y": 394, "w": 20, "h": 18 } }, + { + "filename": "beedrillite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 201, + "y": 414, + "w": 16, + "h": 16 + } + }, { "filename": "wl_super_potion", "rotated": false, @@ -7563,12 +7563,33 @@ "h": 18 }, "frame": { - "x": 298, + "x": 261, "y": 345, "w": 20, "h": 18 } }, + { + "filename": "blastoisinite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 261, + "y": 363, + "w": 16, + "h": 16 + } + }, { "filename": "blazikenite", "rotated": false, @@ -7584,8 +7605,8 @@ "h": 16 }, "frame": { - "x": 318, - "y": 344, + "x": 281, + "y": 345, "w": 16, "h": 16 } @@ -7605,8 +7626,8 @@ "h": 16 }, "frame": { - "x": 334, - "y": 326, + "x": 261, + "y": 379, "w": 16, "h": 16 } @@ -7626,8 +7647,8 @@ "h": 16 }, "frame": { - "x": 334, - "y": 342, + "x": 297, + "y": 345, "w": 16, "h": 16 } @@ -7647,8 +7668,8 @@ "h": 16 }, "frame": { - "x": 350, - "y": 325, + "x": 261, + "y": 395, "w": 16, "h": 16 } @@ -7668,8 +7689,8 @@ "h": 16 }, "frame": { - "x": 350, - "y": 341, + "x": 313, + "y": 345, "w": 16, "h": 16 } @@ -7689,8 +7710,8 @@ "h": 16 }, "frame": { - "x": 366, - "y": 327, + "x": 217, + "y": 414, "w": 16, "h": 16 } @@ -7710,8 +7731,8 @@ "h": 16 }, "frame": { - "x": 366, - "y": 343, + "x": 233, + "y": 412, "w": 16, "h": 16 } @@ -7731,8 +7752,8 @@ "h": 16 }, "frame": { - "x": 382, - "y": 331, + "x": 249, + "y": 412, "w": 16, "h": 16 } @@ -7752,8 +7773,8 @@ "h": 16 }, "frame": { - "x": 398, - "y": 331, + "x": 265, + "y": 411, "w": 16, "h": 16 } @@ -7773,8 +7794,8 @@ "h": 16 }, "frame": { - "x": 414, - "y": 329, + "x": 329, + "y": 345, "w": 16, "h": 16 } @@ -7794,8 +7815,8 @@ "h": 16 }, "frame": { - "x": 382, - "y": 347, + "x": 277, + "y": 363, "w": 16, "h": 16 } @@ -7815,8 +7836,8 @@ "h": 16 }, "frame": { - "x": 398, - "y": 347, + "x": 277, + "y": 379, "w": 16, "h": 16 } @@ -7836,8 +7857,8 @@ "h": 16 }, "frame": { - "x": 414, - "y": 345, + "x": 277, + "y": 395, "w": 16, "h": 16 } @@ -7857,8 +7878,8 @@ "h": 16 }, "frame": { - "x": 281, - "y": 363, + "x": 293, + "y": 361, "w": 16, "h": 16 } @@ -7878,8 +7899,8 @@ "h": 16 }, "frame": { - "x": 281, - "y": 379, + "x": 309, + "y": 361, "w": 16, "h": 16 } @@ -7899,8 +7920,8 @@ "h": 16 }, "frame": { - "x": 297, - "y": 363, + "x": 293, + "y": 377, "w": 16, "h": 16 } @@ -7920,8 +7941,8 @@ "h": 16 }, "frame": { - "x": 281, - "y": 395, + "x": 325, + "y": 361, "w": 16, "h": 16 } @@ -7941,8 +7962,8 @@ "h": 16 }, "frame": { - "x": 297, - "y": 379, + "x": 293, + "y": 393, "w": 16, "h": 16 } @@ -7961,6 +7982,90 @@ "w": 16, "h": 16 }, + "frame": { + "x": 309, + "y": 377, + "w": 16, + "h": 16 + } + }, + { + "filename": "mawilite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 309, + "y": 393, + "w": 16, + "h": 16 + } + }, + { + "filename": "medichamite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 325, + "y": 377, + "w": 16, + "h": 16 + } + }, + { + "filename": "metagrossite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 325, + "y": 393, + "w": 16, + "h": 16 + } + }, + { + "filename": "mewtwonite_x", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, "frame": { "x": 281, "y": 411, @@ -7968,90 +8073,6 @@ "h": 16 } }, - { - "filename": "mawilite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 297, - "y": 395, - "w": 16, - "h": 16 - } - }, - { - "filename": "medichamite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 297, - "y": 411, - "w": 16, - "h": 16 - } - }, - { - "filename": "metagrossite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 313, - "y": 363, - "w": 16, - "h": 16 - } - }, - { - "filename": "mewtwonite_x", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 313, - "y": 379, - "w": 16, - "h": 16 - } - }, { "filename": "mewtwonite_y", "rotated": false, @@ -8067,8 +8088,8 @@ "h": 16 }, "frame": { - "x": 313, - "y": 395, + "x": 297, + "y": 409, "w": 16, "h": 16 } @@ -8089,7 +8110,7 @@ }, "frame": { "x": 313, - "y": 411, + "y": 409, "w": 16, "h": 16 } @@ -8109,8 +8130,8 @@ "h": 16 }, "frame": { - "x": 350, - "y": 357, + "x": 329, + "y": 409, "w": 16, "h": 16 } @@ -8130,8 +8151,8 @@ "h": 16 }, "frame": { - "x": 366, - "y": 359, + "x": 341, + "y": 361, "w": 16, "h": 16 } @@ -8151,8 +8172,8 @@ "h": 16 }, "frame": { - "x": 334, - "y": 358, + "x": 341, + "y": 377, "w": 16, "h": 16 } @@ -8172,8 +8193,8 @@ "h": 16 }, "frame": { - "x": 382, - "y": 363, + "x": 341, + "y": 393, "w": 16, "h": 16 } @@ -8193,8 +8214,8 @@ "h": 16 }, "frame": { - "x": 398, - "y": 363, + "x": 345, + "y": 409, "w": 16, "h": 16 } @@ -8214,7 +8235,7 @@ "h": 16 }, "frame": { - "x": 414, + "x": 412, "y": 361, "w": 16, "h": 16 @@ -8235,8 +8256,8 @@ "h": 16 }, "frame": { - "x": 329, - "y": 374, + "x": 357, + "y": 363, "w": 16, "h": 16 } @@ -8256,8 +8277,8 @@ "h": 16 }, "frame": { - "x": 329, - "y": 390, + "x": 357, + "y": 379, "w": 16, "h": 16 } @@ -8277,8 +8298,8 @@ "h": 16 }, "frame": { - "x": 329, - "y": 406, + "x": 373, + "y": 363, "w": 16, "h": 16 } @@ -8298,8 +8319,8 @@ "h": 16 }, "frame": { - "x": 345, - "y": 374, + "x": 373, + "y": 379, "w": 16, "h": 16 } @@ -8319,8 +8340,8 @@ "h": 16 }, "frame": { - "x": 345, - "y": 390, + "x": 389, + "y": 363, "w": 16, "h": 16 } @@ -8340,8 +8361,8 @@ "h": 16 }, "frame": { - "x": 345, - "y": 406, + "x": 389, + "y": 379, "w": 16, "h": 16 } @@ -8361,8 +8382,8 @@ "h": 16 }, "frame": { - "x": 361, - "y": 375, + "x": 405, + "y": 377, "w": 16, "h": 16 } @@ -8383,7 +8404,7 @@ }, "frame": { "x": 361, - "y": 391, + "y": 395, "w": 16, "h": 16 } @@ -8403,8 +8424,8 @@ "h": 16 }, "frame": { - "x": 361, - "y": 407, + "x": 377, + "y": 395, "w": 16, "h": 16 } @@ -8415,6 +8436,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:9ef21166268f7487fc9ff8d0f9b996e4:82658ac7bdd4c2b417e1f59168179262:110e074689c9edd2c54833ce2e4d9270$" + "smartupdate": "$TexturePacker:SmartUpdate:875c6d67e72590dfc6d319101aa31cfa:dd2bb865ecbc5ac7b975ddf70b993334:110e074689c9edd2c54833ce2e4d9270$" } } diff --git a/public/images/items.png b/public/images/items.png index 8aaa0281c0088a4b0ded764af22c3d5456ce167f..1bd7b3af9c3314850fba4eed749f7e3aaf46d8f0 100644 GIT binary patch literal 58657 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 VnKdzxs!0I6wACM})u>HkJz_A)z9D zP}fyZcX0cMC@X*9U}mSMr|0A2dwF?ToSQxVd-?Ce*YEqt_|)RYhPslfj<)X8-QDia z-t&%mO-*fjW=3CspOv}a!`;CA)!oxYd1QRt^V5R0#mB?Dhll!#k*Uq`Dz)z*QP<>} z0V~`4qph5xs^rXau$S+xmR4y#^+xZdisW18)T`G1xrw#ATTZUEdDAH+`Di=)r`fst zcF^;v3jAoWY_Pv2RP+x!?Ib*X{c7)GCndG*;l695LR+a5a@)MOdv)dBnac2ZVgB%P zQIM@GAtG`Q8&c}I`xFt`d-im5{ut~C*y?e(S*-NjG~e#lkg{|fPZx{rKh*KOI#}vH z_F`AbQ%a#E-MN6SMY%j6-rN;L*8aH-sIQ3(2+FN)iPEiZI?G~r{hpr`@gY(s{JqN4 zmLHcpZ_?5;w011-D){O98}0o3TrhD4k$`PmADu)p>(Y@yp%!l=*L8e4G}%xgHyR zav(Y_A^w^yluuoEm*v!J0#o{4HYbU>n~;#Fd-jm3wHU;1u3c6LOwhkzKtiKtqy@)R z(&lNoyEtXo{g)e%z0_s#v3&kuBBig%wLfpiFkd^WMSovC_xSHr^4*WpZ{bk)ET@NQ z|12IMixI7z7Qvgi$pDMgP)$C|=E2tu2y0oQJF7rCM&+#AjqV!9=K&J`!=H$x+nn=< zAW`SF_TMUbt@^HYOp#r83rM7WHMTRO+x%}D(mRfRI2hJyf9w3+`%ze_5~~~Nuss$u zZvYwfZMlb-FvLU(I*x@~{;52BF{{07Y7XU2yB!-3p%4n}8L}=_BkEHA=AXo_q!hhs zz%?B3zN;v7D@Sg7+gFqa|J^9W!jia_g2oxV^ja72WLA5pNtgd&X>Pv<(_?RX0Y4@3 zp_Q^wTt^B~S7jzMsqIq0ez$P#)geT{fS03{1@)t#CdZq2B*Mfg$GzHEL!kVd0N5Vd z8ahb-zW9H~UD9Z#0A}nDgwEn^%uM$XzOoSPX=k(nXh5y>R4m~rq>rhD- z(x4Oq>dTdzuPxs4z0&;L8$`i76R`07C zuNNx3?(Uvmu3yR>jO(FhzaQ&qm_o;ax9<&=l_Pm`Tb>E-BJQB}yPhCN>(!2?6?5tb zWs{QwzekPLfKku*h|IfQYo|u(`mdTcHfHrZW3|v7gz3cWgN+NfXmeshRBI~((T)jj zR4f{35Ic82?uDPcBQ^ye)5tJ$2A|K#C_~d?!O2Z3$bsmrBYxfKPU?m8XWT`r=HX51 z!^vmfjP1>^mcM7AK?({;-={JCsk+v{-90^t$**#I7kus-oAg0t9?w2iWqVd`^>nR< zrqkp2)luJ0`P~>#&)Eckx5O&ao&Zyet{r5C8%@)>rrzqSM;}e|0f_l}f@~g}|52o& z`NQCfr61();#)}bpH-hLfWh*g(*JiE30DxP%NqW`4Bj|97b`2G0qFi3FK^Et5i9-}Zb`Mh}^C0MoYjJMovz~vhS#UbbGe0{QH}M+;&N-DpB7c|Mi=!2N0SzU|r3zVnrX99u?z* z&--+XAucp6g@Z>yd2b|CoKzo{0N5$Tsqa>zwIsQ*QW;x%iST9iYW*%G^LURe*kK{H zJGQ$U_o2g(?|p@mY3(({uS=-P|D?z7M(XowGqw5@r#Is7ikai8H<|j>wR!u^!Hi$d zht%WP{3iE|ozfwtQ3Nf)yXb=9^P31eOD)DvVe#?bXt2qt-$*TWRUm_N-gw`-DhPHk z>wNmX#rOloQDy4Ny6Uob(*HDbP7K)%3&gMrDHZ&fm~9gImv}4$9XN>wqZJSTC;BF; zgR(@UfwK_!VaEfN@3a3iq`V`L+?8{5a86r{nMrO2P#m_?jbo2o+Fq!Xc0zM~+Vej* zEIMkZ5VgNBQK#-(c!5$++3cc_}*kgTbD@#>lBPCB&V^~6A zpN5q#=o_*C5Pp?8#+b$j0G*oK_|W)lQ4Hhg>YGUTt@moT9U3>^NxzbF5|(Ujl64zd zNTZa4C(NfZP&OaeuT$|`p`m)QirnX^*7*Hhx3n0n^dH>{8h&AlpVfFJXPYwR$YheT zn&Eu{gjesFwYNzFUG~}UI{W(i>=%|=fv{7LuKa`abW5`wxt9{QcBl_F7v(P|e-vBy> zm|N7?z+k%t4q`g4k;8w!825U{4j(>0oDAe3?(xyRZ!bjsu9s%&Eq6vSJmNOkk8yhU z(0j3)Nu819@}nTey=z^uMo;+QJD4 zM9T`^v|pM3F;%cKxOwq;c~*!S7tajPjESVA5V#p&WW97b8%(rnUWBuWxhUuLP|9=f`_fnCxh~$52t_5mue7=#cWOP>6sW~ z_r|55RL8qh@*;UT?PcAxW6{m;-v~?IZ9yo83@#PD}Tq1Qr3;h@El=b`8u|*+t;>lol#wJ=iW8`*D43m~GO?ixVH9QR?k*e7V~7 zdx@os`MdlQV{S2aHY!-s%Sikx)rVl)1+pF{XVEixWt7>-wtDz2rssIY-!VRdE!>i5 znkKwB^CJbY0l+qYAs1}4*6MS?TVRlF-TKS+gTWKK1$ss}{^+}pzX5;9<}Vdk)h${d zdlN8Y((qxpaX1}6VHDN#n4?sFDU7A_#qqC$pzsQgo3B>Ii@4% zY6qCK?=a(jqlDZspkG_Hyv;+5nT38tEi(7Pcm2)3nLKN}lr34#(VkMW;oFv)yJ8v- zs&}c!GvFPt`Igf404b$rui;=9q?sZ>H@N({H(!fF(MspTz~d3QhbQg)NtIPqb$Yyh z@i7-~em;S7?4>nn)^TCJTazI;-$mYva=0U5Zf{R|Hcy;{`i*5TU5b#_6#2PRW1@W|yIrwW21vnh*@(-ibRmmR%% zr_sQ6=nTR8vJzSBZkBafCQW08wt?@TQ`EK%jAi5nTe z6QS1bpM4mG8MAQ*lYak(6qGO!(sjxuveaMklhS{=%!6>QZ=;}~p!3uiWBeDG*Q|b+ z=BqCkb37o%35J1GxkQK#I1MHepkrHfV}Q<2)aqDSqXVVu2$w9KICiLu2|G>QmLm#l zFl5O}$V-GUjnmx;%lR7l!W`b#l)w6eiJiq~6%2DZ)PEh8GF5v4YUC@PncZNoO!^S^ zGmPUn!!EZgTdoj08C!=ciiC@K8)*LQw5b%vcVYCa#u;1-94usSOnf3~a*fgVO&79?Wt>zabrGESiQAD> zP%qN9({4xrnxG)Q)F%v}nDORJ9IA=1&9o#$bca$Xby-5L+TDH-^8yzy^6Bo+%T=t$ zeKKblKlJm;DnFk=D{j(_fOhX0?HiX-eBEQspXz45}_v3v#xTDU2rXS;2>mF1of*Z+=iW!4zFf1~fE7hwX_S50i4K?{XKYs#glD--Ge->jr zfKrh|jcLR(Q=0fe+}K;%?XM?!!*He0T4J1`qp|-a=MYMq#!DCKB0a>%)GkGHB zxITr5m};}0wgIN9EaRz3{TCfdU&|8Y+6r|aIm2Dy@tUm|Rd3aqcLPg5TCK}vmw7r>=}P1K(xBWk6ClX6-khsFRa2OFTcvr|0Y1 za*9`vf9T>8(nXreJpAHLorRQ+VI0D=Iz64f#QhsWX5Eb`;<6n!`yMb(*+l$?_i|3e zRM*@i`?LIcgB$}fH4a>(b)(>c;lVXW*TK5_X4P&r-fA$$N+1t2K7C2od^J4_XNzho z`S5Vcz3LOGD$WnfnL=W=K{#beBkfd?iYkSNWy$)9i!{t*gG-xq@ApD}#8?4Zy~Vb> zbCSmo!L@)UEmd+LR(js-wBhPBrre5eO@9em5*O)?61@yJ?|o5=@G(k3_ooGZE>C*) zDnjF%FSMW>G9LvWxl=~#fjB&T+6U?MaA!gPsmsFUm)-Ax%@n6iz13;myj=ViD(b1M z1FWsr{+Cr|lHRv`k&ZTwj#T>6?@X)Y4RjC!2F4qnG5e%C=0B>|YFA0pMiOtenjb#Z zo?Tu1b$Lk639(rm3LJ9c6xo$ANtBiS;%VU1ilvUKdm2P`+i&WlO1#^5FCrISOBJ-= zwuQ5PVx>qzNI0H)SWwHQJ+>=j5qj$p+-(|wSg9&MTm1=`6z7`~=)H!Q)6bXpUaOHf zCprc5cz%m(3fuStYBgvw==}-E**TZE%6XCwim1tX;s9#f;+d?yPIKQ+KxVjQY2Jjf z*LuLwQFly#Ptb+kK4YN*H%hr=Qqevb7BLkhfTA>wDVIhm^y1%vF)I!jnlvdquxPtkaVKpW6ZZ5JL_pA`OG?Sm_AkkOBUrdwHeJLp zWg}%2eh4v#S^{{#6^`S^0bf{0cTmcycD$`>?YSeo(BEHyoPHuaz}qZZ0r3E zK`3@rL=1DuoKh8?A8=jHL{{oo8I!di*jqD6GIZ0F!?$F3j}Lx<^{tfcWF}gw=9fb8kN2+Y?a}L|$qwCxJfNIZz|BXZS z8@wO(7e?>@6XweYBJ9uwg>Zx3VLve_AE7Do6aCXgmwC^V?J-8}KQVUn7}W8;Ar}l+ zfvs?3gprH#8Mpq0NhGmj>iKjJ#M4P*m0DT7O`Vk0F77;dd_oRKh6_aDRAY_|<>N32{w|9pQB-neWy3N@Ry;RDrmtDF9)`4MT6B?sGA z(}=mK+oc{m-Tv=wjnHo`6{Me;tBqqh}TL>)zPd2nkA zYG0XhEnE~-TbDYD)I<0$zni}%E{$ih`n!KtXLKxgkl(567yJ>*eolU``RkmR;oAro zy$&%&51DSn>Y87|H$*hqMkIYTs6EpC@`@w^xP{&Vry-ZoJ_$*~G-u|i!U|fp$7FH} z1M+KYzzY@S!#L3d@le59R@E(>lst{e3U9tj0{`%x!+md>5`64YWw-~>;ovvO->jnn zR;qU)EppcxQbsee35Ef1~mq(QCPGUmf8!uybA? z`=3a@$G1%GR>*U>WsbF{)p;+HhT-!G#afx%7K4-AbAJ5>_Q(y7x#wq%cA`^2M zJ2%|30h8EQU%`XGoSYY9RPtT`s`>p?p@@-`gUPD5Qm#7xFx;7f0#2B&9{Z!^$cek1 zgB>I0tj;*z-k1F*L$#3aS4U(NPS6#S=ap-hjX%(o6WT7-ZIB;-O18)3Zzj+KFj5fD z5yNJ(q41$m_(GcAeJJR2+2@7~biXYQg=*dhFh&5a!u&2{u}V5(PdXmrhAQ&&yGr^P5sV0sB>S$w?pBC22GZO?N;FBbiX`~a^nLS7M4m4+G{pZi7o}_Z=#2r$<_6528 zi``5ewDBgA5NwzEp}>kQlT-_`y(_EFqxblxjNi2LJXT<~z<&NCVRC9!L;0@S1}z<0J_$ z(l>cc@;LHCWwl8HjWO}LxlI+Fc*3E<3&tUa#`)(3E$WcOl9B;r1JKI#@q7Cnm&m)G zo>%z2{fY#C<-l$EnUo#jWwzl^`kB#F&zMh;hhe9I1P2S7!U>7&gK0$jjqy58*bG{& z+D=MVrD3(?eRm>&CV+*R{aXMo#kuzKNVd4Ua`B#iN?ly|ZalOdH+-Y}#4%nP3i^4Z z)o-Tl8$?_RMfwq@Jb~-R)3suFYwXBQ*`BD}ncj!j&voLq76xy^iRI@x4tTBf$QjEjkgO(b0A*@k6NXgl;LE10aKG;@tC zy*zWx^Cw7{8d@0o+N@D^5@B~z;q}W&;9deeBrKmMZbk;!?!y>9dKr8<&J=lhT-bD6 z-+c5R2xhK^lt?aO=BKl_>QVzatug?~fQRqk?YHU< z1vAR=DT>AU_u>@|N972+oShFBZy72bMrMO~-1p1f4+Xd9!#|K=^T=#B`eA)b76H9e z>)KX2|6t~`ND18dZjcv@l4|fEMl3z+^%J{#jJH{7t<)aIv`%WE<>l4%M*@1iwa}>f z>2Hp!ZQjpeE_vK3{!k8>7Xqb{<(Er)qgV5q!nT3~i)tZo6z}2fvsgbLcC5=&4;`Ta z_cqz`ZUE>aE5`gggar$@(efH5gcnajIF*YwC9z_sO2arJVE>7oAQ!8kEd@&*7AmoW zwf~qF9aEz@#E>KxK}$Yy_~K^Di!O4D2(cg5w}QK2?XZbr{UNvYyP(=R8(8%}RdJe6 z_VLl=ovMxiltgeV6p2TS9?mYO*#B}dV~kH1=TV6whJHb`-{HaTRK9hj2pY%NruY7M$u_#pa7$^S3Xye?~Mn z)=`4YrWza(OH>uvPI}zp>hH-Ul7sZLee^P0g2Oo2X0BmhOSZSK>e2L($VVo>p-kZ4 z*5^Hq4uzOy}vs~VX~#4Wve!A6)=)1$*!K{kV#eV+#Do= zr+l}%qLaF%k4<;`gwA3stcr_9q7*TX?BeBBmxC}%>%<77Oc)Xb5r8W8vYJb>8jRyk zIxM90RKMrf-N~ML7}bS}sh$jLUM*^%E2_OFO<$WSC74Nl-TC!ekdne^L@esWwhqoJ zGJ5j$lU>!zWHG)Kv)jh+_vpv=vFT>QqyQu}ji!&RXGsq-LrE-1M{!Kn$w_E@lZ{*) z>|AP5svKOuvMNEpoAG0D@Z)+k)hyT7y(P?(uE%&^d((9KXP+7SQ9fC(%#9c;-IKR+ zsbiiQJv;7Z+rUCQ_!@VW+6hFtzCIn>3v8b4!cOT(cSG**0vWD$Z!S4I7wqImT)zEmgG#SlEhKZf!LQZ5sZkV{Z zrdFnYuXLtnD`m6?0&;J*k{gC=_gZ`?t^FWfTlZQ1>$_7t7V$X73DdhytIxMysb2{7 zf#?zJ8)=q&AY^1)HO)b7F_Cv%ym~6(sMWe5{ek+XISo0J)QJ4I{S{Q1Bv=_XQ;aHZg+R_=5Y| z=bk5~XFoVRy>_i?_osc#|L)qb2zY#&H2HgNzn`$)9dvPh9C}vgdUlK6b}nj95Wh_% zheRUQHJy;q^6n=|j487E8rosCX<}0{_KEfoYy~?8AwOKfiItok{1~Dq07RAHU@NZP zRB&0M`2wlEmi3cn&ydR5D1h~s5bA(bRLbFlF(hS882x?i>|h?*i!Lq{_d7Ns#F^Rr zK)hpIN7MCv@SE6*TVCf&DxW%tA1!)6cE=Mfo21-UJ;&$AFc@JnWqXTUY)A;E2BG^j zd~~rgFn+NME4aTJe$Dk-O(6_%XoHPvYC}d=7|9IW(o4bo%EezxudtKi^5^wcimw1B zw>UG}GvFQd&n)+Z>#rZq_X@o+z1cTWbYs(zFkr0edn+^@>T576D%;VY$_R$0PEXz0VYXZFrzzz}RafPeX1=JL{VJW+Ad5h+&5%_MzSLYs94P>!M__ z(Kj_~+@ZR7=duBjJ_BT<5_3CRBdKm5>3PLk3R^IoJA7zSvR>UO>XGeLaB`Th+YA&# zUq`_b&U$&7l;WnJ{I5fjnBs%AK#X6tywgJ)PIbiOUqQoFlFK?`jidq{5W{gHDQ3UE zz7n0_5yvH<;PYax`>W+3d7jcj~U z)msb*{Np!1RpBHbg19bcRVL$MF`BEXTy%BZUk5);J;x8I2F}#3qW> zq$HOD*HMwOugrE==qTJWEzE|&rnRGqEtf9IOhUw`+>6B#K$l~0IV{KsdYb&g@&X^N;)Y?w$Y{^ zNKB@nO-x9}l9~`qEe0(ss}ty|cxlDR{*pcITK!URlEx~p>53a=@Wr?n{ogzb`w3@< zI@|B>%U7Y#IewPROB1>0T2)pi{TPwpOb`5wr@1}0r~3ajW$|A2*mGkMY@6^FPEKM9 zU{cGvg$$!5j98Sr8Qj%1_Q!i&7<|f>=Z|tu6-7A^nDY1O2P@E)@oQq4z^w%`?l$@* z8dP9Qum35(G~flJ;TaJVL*THX6dOt2JDSW!yIP#Lsr_k^CEH6I1&VC8!vW8REvPYX zzyJKh(YHrTOqhQeo|3ttJ+GcU7q^c=f!kH>8emvEdPsxbi$r~DRZSE>EjfyvyhAjI zqJy1O)&aIn2v&iGq^+R4LF$tFfv?4XU57+l%c_I8zarDJ@~OrSbA_F8TEm9LfbuhW zvKMatro0s0tJ1-I#!QTS=5kgoQR{7YJKzoNOJ>yc4h%8yP|IY1;Y<$NyV2kqjg7Ld zNO5K|rOcYA=mr|aQGyULNp|8fDKhtxjF>XkxD zmY%4%I=$-p+kwXh2L&~4$l)ATtQBAA%Wb}=0hTN&(fCuHnOVpPwNubwkIGVUX_UOf zTz!5`#)Y(C0+5Q%0%O`( z^at#50Q#cLASN@P3^H@6tnPmEU_g1^_u?OiMK5c2mak_;H(1l}iQ(B;1rfY;1I9nr zvSvD|>FA_Ur1!OTr#BI&??W%5^CF#zJ}Nr!S9wzM!BYSX{Q*mVq+U0-iVJ_9+szBm z7%&|}6=D?<@`%kb_*AZassx#slr_tN_~lUWyIYNL4iC$_q2h#=A;A^CGjg4FXXaBw z6OobWD|tUZ&S1BAC`p&G->Sm2<=X+9x~inv!CD-*y3?Yp>hObbVbBhowH4_?eFNb$ z;X)3S*480`?B)pDlaWClR-zO|`5{+%?t%;1D^DE!i5q_oWuc_FyDd9JZ+Nl<(TwZq zO8%|7yK@>kI=->gKqV=xU>t_(MW|`*7{yqHtm=9>F$qaUxpMUTQ}_rO05yDY4ykfx zg9Y?1c)nxg$udbV*N)?rA<7eH6CJm7pRP=0>#p{JQ9=C>##I*Afp7hn!9 zt{b!-4a9)GZ9|sYLcv$*s5_r*x@IF5qb0dZL1}@Ph7JT}vVBo>IagKDDnJQTRnGr9 zu^<+dIk(|J2z#Ut=YUIcd)0<1)y5daSxiu5l=V{Z$colNNNO8o-kA%<8NrYY0g5== zVK7}|Y#=FMTUdR)c%YZQCL|CKOoQfe3`ly@v@G6}l9ZIRp&met_mlbL2%s+FurEhizx_}TkAyZxGv<8~i62H~Oejtv zUcE=xNGwUIEe>c&9c11Ay;>9LGr#q$+`@GaG}Fbs3^7sFB-QNYZYtZcTP{G$0LiSbFcK5t4#ZsVGP zWm1uQiECUu)H%lQvD}{gGVT85aN}e#cA|=991#Jwc6KKlg*Auva<6Ts*CK!{8knnJ1!;CPM$p#5y1GO>g2I zTk@2o%}#n=p#spLIK2k#L-<5#y#GEk{CMkvAo&>V zFz3ZwvQgJz1nU>Rqti9#-S#Fg$9{Zb z#t7^!XP`Yzvb0If^M{k0h|f9a0+{0A7SiuAv$QQ#j9x_4|2E zJby@PsQD2noNW2)Shn%cNF<{)K5!kA$9S3sg#ptX?g&9Cd(V6s$Ti}9ih?bZIEbFB zjN>&nqLNirgD3)*smV$A@leRnIbfzuj;{@6KebgCX0OgKwr#G=UW`4;o!zkGm&GD$raCf0=>e*gV_Kk`ZE>zAoSv zWH3vtVv)?M(!%QI6V%bq+W_WyUD2?OPN9`ZgQO?~y^vybUUrJx{)|L~BIm@w+wW+u zy`T2rZxgYrz|i8B@ikfG;A=oLQzJnBf?Yi)<-h}ZX9iIRJ&*&hDcc9X!W#nK zOx7_{3@u+w(z5X><0F$)lQCb;u;CdLW}Xc~Z87T+*0;su^2F(Ok~dLedEMLFO4>;Y z6twC^EZ#?`)q(5*NV6jR-1odmSD{F z;o9%!#p}T(z;TF6Bo(C)<*=$Bco||LmjSmAS5AFKqGm-AfjCJ?sY0k$ z0OkmY|EARmW`x<$&;$Sc9B^~^TQU5)MS+jzBf6y{J3G!2(|gguie>XBGS#C*8>j?$ zqR07M)W8_HQKG*rg!h#Y6>6eJRt+MwIPtKk! zLb`R|WG6|i~eep4GE#+||2KaY10QlB?bp6wjT5ati$ag3Va1}M^ zxTC&Y`5qZM{&$oaq;H?*x{mo7(9;)qOl(6~0v#skU%8YiE&?K5+rJ~_i#dPgxqUm0 zXzWO9OA7eP_NXk~9B;&5a3r8Pxc$v<>=HV^N z*X_WPg0-NHbDGYEyXUhrPi~wGeZ&lp*{3Xf@ZGir4uuJ>_cFl2!MuTtsGgW1&(2$D zw&S~7*fsWXV>>_|0?n(ezREBD7qqB}`qzWzHPA~oF+a1fFanpi6d2ujUaxJZgvEzI zA(K9J;9829#4ba56wuJ(Hm=|e%LjzSI{q>IgSo>E(A+tNhy&`~#SIE3Z!|qz@r*TS zT1#;{D}a-iz-~HUZu~fK-kWAq-1lXR*xxzGB?4aQo9$MO3`R0xSEZS2@cSfsW~XxT zP9+|z87HJ08b{Z3JbqE0l~OUw{{d94@bf9AQ}PT8?wEDCEtz)g%*TC9QV_J`HI^^p zXN zTPbD*6IFA|?zM5rS1Du3Uq^>-MPY_Y0Ndx0Ex?BFgzk?i(IEJt|?STY!r9`RLYzQ(h9kdo1T&&iN{SCK(V8Ob0 z2N3vEklrL#bMyiYJTVXhT+rBkOPaoqPX_X(+6uerF)^!-pMu=UttnJ~em1u>Ud0Z0 z0Ur>*iSU3BWUaFUe}qlS0JR8lzygR&Iiv;n z!Ww+%|IsXGu|Ocf`h!jYEp!Mx@l}P{K?dYk2KaTf7FIs#K%;t742)Czc=1B~OMhJx zNM2q27xvsv+8YLYXW}?pX+(?LGOLVI2r0$S3B`u+WXUWf8kB0>4t3n;@_LFXPHJ#PmFVemD?s`L zJr#0~Ve#j(4m`WCcNUde?q)=%3k?oafWy%&m;9zSi>#TzJiDy2lzOvhU|OP01*!oL6L{iYP_EFshEO1$Idt6uh+kh1FN_%ZEH%>+c|wp=Pu zDd4v5R_#z91quWOKyoO6s5~n@VsH*VI^MtcA}@Yt!WHe8mtnC9`aV(ARYbSq!P5I% zZ07VIC&eX#s5>h#_irwKD#fRM@6Dxj*uL{CQO^)CHJX=l{uwZL|4j(YW4j-|cGXLw zsqDC%;C47$++}xJf-?i~9vgV;U zp8nG!EE+(7+>|oMZ_%q~#>5)C7s|XxLJho-C@GXRSLzFULi&6#F(+uJZt+poLkMZH z5UIkDO1~u$1DNh?qsPod{A!xN1oX>V7K>wPF&x!aiBNncG&@!KU z$uN?A(D7TJ$)?uVfOtq1%ydEh{QSIshsf`eL0nwiAlKP1F^eIu?bv0d^=B*Ugx233 z8jhS@8mHBD7^`ys+k(ri_n;;Jb4I@cyy20O8r;#XKfFyC|AIO3kCC4+aLeAAX@J%n z-c#7`3sn8|Xy=ZiC?s=F%4}%OhF-C&Le%pi~YIZBs=4AnP z9+`^5yBv|)xj9JX()7$xEZ^s3=HQjB&ErKnm23DlBhTf_1!CtOD}3gcFw>?EgxoUx z2)2VVh`I7Zd4ImGQ}1G^(Vjm}iAv&bf1-bY z7dGa4u{U8N?e8RX6)tNxcqm!tyPGqtiq9Q?X4PV=EA>7YH8|ub4N&_qi6&4@SBDs$`1GVZQ{nZP&8b#%v7o2!x{GK47ni0cm^p2WmJy_Qb}bY)9Q}$eUDgR z8a~A~dZnx!2cMq##V$!QfTEjf4E*+XO_!+MO5stW*-n-J+8+==(0SCYb`=^77?(B4 z{cXPt_d(`qsEVMdYR~=$Q9m&hUQunL!&im|=h{t>tYNt&Ev~nyb{>J)vEzDwrJsbX zan|ZCXK?FKES(mnP+GVl>+()0^p5aWs^P=ls0Vv^d;D2}Ua&ePg;`ZRq1CPeV+a$| zw?Jj9O|r}^>Vq|w{R-dPay?93sfsfoZQqqT2T<@Ua?QGHHQtNVKh6Q`m}3sR%U(2F z19%y!m)n>X+Wu~|z~fS3%|O?_e)a&7Hjh-pBs1hivh9ko5}iZ>lo@@;^Qd!9IEQP;w&0%kF&;>MJUvLxg$W!?b6=T0x(PAc&Z! zDdVipUd!Ud|9burc9&=K7)z zTZ^WcIz2s2SWpDWD3SY)O}!K9_uNoa6t;rYqpUh?p3baPgQaU4;o!@@ez0&ALXbeQ z4tqvn1!ur5=9D%88wbrhw`ZHDDX6HXHd$Y6?=nh%x_7TrHyuVsZtf+h!0Sy&g8#r{7{W@5e4)#dnp+E z^)%(|AKO9y(HMy-`K-+u&Gu|qfH0j4+N5th&41#`L29+K-b4xhSr70@p``qi2W=cu zyM1@wV7d?2d6^0^k?J#*>kiwje&1svVp(n3qX5JD8K^ep0re+9&q(9o+c!WX&hL6)~M-K8P48BKo=LMquado%bPtajs^KAkZ z3H1$hgBZm+CGI;{O3HTxIX!u@OpT|WDxQgPpy6Y#?P$jK^oJ=J3gBLWE6cd9$X4as zYm%R8qs-1@gu<#Y(z%8rf9QKNNJt1}_1ap8{?lnzSS+I3#ePvKZrnhaOF89(9H`P+ zC3e#hNxyS*e%>+89Td-R4J&UlZ};{5)q5z8JlZv4f%E3E0ls&$IQiuD12B#787#Y{AeK4AL&QbYKd512v! z%g15()5Qa)I6Kh9UUY;hsU*GqUR9e*1A>fhe$;?xKA4J!5AauQIy={Dn5}mE;E>=S(U>4%Vd7&34|=j}4^Q!V z+2(Ie@#KA_+T=cW{xsjfv646dG)8Ngq^YW9`}7GZqjoEF!6ua>g3k!FhZoj%4j`6J zF<{>$ zHB9@Z-&u&C)&vrTG{}sw%qA)dlyyuJq)?)y(TQWyPJ|&` zPb0lRnN9X(2uDwxtWK{zEL0XRD|>2TpD@50wsK~8P09A+LHiHj*7VP;0^p;K*)L-R zFlz?%%nOzRehYZ0wVg71l2^b&r3eM4(*EP20>~&hM^{(-N32KhU!k&Uoetqxo86dv z{ujuH9Aq6(zshhi??{WCyT%_s=O0q%qOp}C$afqe-w-%N@6Dt+r;=#s@^O3hlg@WF ziiGViwXxuG{8Ply`0HZl z(ykF-71W@)p;isk#+Tbyx3YYiN?^)nkIwB>lQut6!?LRWewnMCp^_!Fencz`rq&(y zBDp70wX`JZpB!QkqGS5Gnixl04(Q=pi3Lj{M931RH}}8>BiW#i+=ue)c9YjRXpz4? zmpBJ6e4KdJ?q3VBZVHIBbP~tE@XYCvetzZ?lNBf`LuZ2 zN+s!LUOt%CabEgf&J;)GYX@Q-e*$k~%DD+iF*o}q*^8@9n2P%4ohiI>b`v25jMqX& z!3lqb9*#pZBT7Vwu-zeiMTGDhdi3*9a$F3mOUN*|3hw|nUP& zL^6*$Z+9zD=`msg8AQueU(mhB>x#^cYr2M;C6#NuIxQ6G+w1k028t0o{f1;anAtGX8ZWacUwV#qk^dsKA zC2XeWAJuZrfJO40oq9ljBAYJDSkop(IMGgDx^3>FSsFWk-Xa}U*^#Vkmc#jac!z3}yQ=gwshUi*mJ=}xlipNH zhYm4}5A%Ol$P^>$G*R}8tm)C#%V2%X{i}GhW^o#s3M+BOKsC%_@CX{CqK^81G+lQ%+|Sq5+bU64 z??kj!LUhr42w~MFL@!Z-)q9IBL<=HX)YUB(t9OasdoR&j5Z?X%p6C5*|NG2+W@cyZ zJ@=e5AP9R^OUqr~^wb>26)b>S4z^AJ(0emgkOy~GHbs$hv2_XQp4Ev&a4=VW@(7JR zCqt@&-0xK|$#Lb0;Vo@d<0o{?qn>tHReoqd<%p^dJs+Zx&Y>TbkV7>evhs^$gB9t3 zyGvBwKaPc?u>H&SU$@MyNFxgfjmpYtj`QrCx|6cAQ)D@w;4obmViERJWohJG;S~f6 zD1{pm(zUT+usXU4#@_-KXFI(*~SF1u#E2Cr2)J-i7Ohn3=SfM;6FpreX;ZF0Uc43=zjw#j$7>524{G zg$6S#?53N<89BG3EtCA}(2sO-zJeD^bb(UMoDAgU>Pr5v{;m3_< zi8vxE6~0@|lAl>wm<>3c$Ugk}xJovz(R+2C%(e!Yc1Y>JRi5nx8k-CTu~nyVXs9O{ zBr=L|VSN-Eb}f8h=j=mM4URp>um}Zv-SG(sGv%jBFEKg8k!UwV(aj|R3*o_ zzumk`JUkpdn?iH8UTy7__%c`cWRArrF^O8AR{E!5JE7< z1_q>OgjgJgsi;YMkMgWQ|8oYxiQ;aruX(9Bq2fZXrEY!X>|RQbc% z4^pL0@PGU}%n|u{IC_eWt)o=vlSfAQ;QFU6rQbBy)ETSI@ufs`Frx6gu@TL5LtOY8 z5i763XR@=y0n!Q=`Wk3uRYA=9f!La7fjgt#i}#`#2k$cKX?$qVcob(TAxRGG7DpJS z`<(nkfO7?&;N)q{afX#gKdi|9{6)<*kzr$+h;I^U&sdNDbT)9DwHV@gz1``px89)} zI8+cQfBL;;-3$8PAtDQQ4MDZ($huX{82sdfw6XZXJ`IKmbhyJ5v!YcrOf|;?q#L&8 zvDxUGo1iXBd`?==4B9FjTf5=qiwi*p#n}?>kwzn1L>zBltY`3j@~4#CT*jz;jlxJN ze`mTzEi&r;tNZD4bTDjH7!vqlXXFftYISt-JW*g<2b)tgC8k3ub?Yut&K1_KSVX=6 zD~rD?yxatGMsd{FsdZwbj*`!*jbw!prSI{JBiD$Qx2AO~C5tgetPa^`{vslVNFA1B z`_80fh?ODyvpy^|5(@;e%rUp&{O|f89U9gBn-7Dh7i(PzhxRXs8#C3zJhbnHd&$-3VYyb7& z`snYk{-l)aNwZ7U%zN`iVRmmi<>&K9DENve-Q6lzj=UT0Fxwknn%dPBOk{6W7f7{j zO>HXLK?~E$X03aB1e?o!mm(9X_ka~z3*_#madhK8qSdE*T6$64j zK!5SF*H&-tww&kY?V@@kgByi!{85?HvcXsk^+7J;|)D#utmq~oHxBT8>in3%W)PC`U%c7#PcF6di^#-kA?s-;e zE;~Phm33p|Rz(p7jgT^d`kE3R6~5*&!>WoA>X4Nk)p;p>{nZbWk!l>M+ep_0A{D)FaE>AxsU zEpj*^HIJkljevpI&TlHG*=b6N=lznV zsvLhwt19&lZ0Lm-fhCscBY~K5Yf4%h)+g58sDJP@ikdKCqlpusRGq!rMXqKxK0XfX zAU3zN3NLv2U1+_r&^Ve|7xHSvYjjJ3t)_>pf?ge7mi{|e8R=QPfKO=Od|C_ALuvB2 zBYG;bY6_mJ+n}?_qb`5$GW<&x9mrE;za2Slhiskg5w}(~)5jg=#rM+m%Vp0?Vya77hG~%3i2=PW>n+*A7`!$3A`bq&^J*ckg`CYcYtiQe3rpNx{o7z!qdbtB3&~VNiyKLyDhTV)SC8T-QkXJ#hpOBhXOk>O zQ8S?}@zycq#F5|8+H&45Q}bD7zxg~kloE{oy-Q~ZA09zv%s75=hwxcnzu0N3BKFko zN{>)?pl32KfQYMNV3d_j-P4e7`Y`>QGWe1m*b>OD6zTPTYXFc6^zB+?T1?PTV}Y%D zT4S&UmmR(N6qO48+yxMd7sPV{Y)*F{$R4qDym>mdefTv%hnh5?^J!UmHtNnOh<&H; z28l5uLs1PJRJ`Xt_Y^7!HK#uDYCj7BHn|lNnT57gYpNxyA>?UbI+?(@j^Me&i+85w z^WJskxOQo&-xyz*t1~=HN}XcUM^}#f`%Cu3Yr4EY+Q{NuUipd*^o`MH=Q2}x6$1bL z<9s0j)zfl%i__!z9Bmj>B0)Z!$_f(Gk1=dF*B+1G%4$&;$ihrmtD!Dhpu> z3uD@jmesii@Ig6jtkb@dmEXsoVML%1P+X=AgeEGl%icsTS_T8ZP?NmZG#@C{%l`qA z`UdN889o^}m<|loH9>!W=z}S6nC0w%rfF{)(B1?PNUUMD=?ujdiP?3VTE9WC<==dPeSw_S#yeIZS!r}_rACQZ>v^fh)Cbd&L3?7&wF5cdcWSfw(y~GA00HeC41W%gYsCH7 zl=|zneFlOt?6x7mkE;)4|2EPct&fcO3zbvvPweJCr`siX4H4vn9CEG(-S)wr zSWaJ=QOIXpQb^67bzjp5y-I>nX`oCW%}Lc@CW-g^)mm5pP52L1f(pr>j{U@P$x>(N z*nM+fzrNudd*qn)5|vG{REA{~F6u69ZwoTI4ibqFAvV;s`aH8-Lo(sM&-5$`&1G9O z^-fmW{-palV}-P#kpK)eh9f?LzJn;>aHL(}PyQht{+^;OCsZG~|jFD?i)RU6*WzZ@(Z%?3NKu)Ouh z2uTaA=i=uU@OvUFx*C0E6}|8-AUn-@sSjFnsy~o5PvDF`a)G3{F=c!iS)P#AlV<}D zp0U4^Nx|?HFnwJf1+QvmGIu@IqRi+wtZ`!#vn2=agd$5L#%poRo62*4Z(P0nJiOCP zKuWDYPfj;~8+(3tci0|a4FPafVcWhY;F#huMrhVmP1_H+8x;^t9;mO|rd{y}(2bwvXwQd!Vy$|^$kox;)?nLGaAWm3H3R%Ko z{aRXTkK0ueRQl<`%t92xh)K+ME8lRX1>__dd8hpoZ^YPy)o1A@`JRQj54~i%O(<~$Tvjn;chF*a=}hdv;UMLj!0=>$#YOGx3loDLR5(Z zSM2I^9mOZf3q!7?8n0Rs0MNmNb`?43&Bw~I=kAr_6uPEpn(qpH*{`e-cg6^@D)Xm1 zP5fWtVxYkh1G%-{vK))>?eiq34>g|5onNndM>QdmP89goJ#*X$r+*sXfM{Q!=?|J{%rT3ZKCWSYb)Jf&SNq~~*SVtVz^%2jk1I)hvD@JThIWX1 z+(n#VB&((Wa8g9UQq{|MYEhhRg`;ul;xCI3p|45QT;+RbR9Ox)$m**`w(0DJURmwwR$^#cg2)nSA#zylswi7(GIUf z8{7Pu3~vs4x;Kr#;C_K_$>wT6e9a4%>u+S1;LT4~K>m{A+ z_}+{My7@c*_Ey2;Z$RXmFv*J2_m4>gago<$+&d(tNW3i?W)Ol6n{qCD=W-Z8XAaF5 z=yUCzUO9g|t3m>yIF#KwiyZ{m^Cba9L?K@hDHtz6`-HDeW4?#8ysWeskL6^M6t(jD zsnZiD0H{+qjg*I^0^7{C**IZD7#S&Ug?Nw`s|6Jm==@t(-Zke>gjhKE{c$wHa0TAi z7D=uY6cl)nHWptN*r|A{ml1;5Mb0u~KNL{T_$b1lV}|RvdCA^&5II1C-(B?E5~EJD zGfmMkY9dkBw4RLe_B&b_!prH{r9#5rFh3ejMl?W3Q~lpAR%HHlixcu5ORqfH)O6J` zc^R%Sr3sm|9YKQ#+dSL^0U!zl1i@grPI7HL$oHzie`wT!haHDnmYh+7eE<;rm)p$k z?9|k)-@;o4pM%npT#m8wECwf1YNjC4NxB8Q`QT+9J_$^?&>zfs-kG43G)g-Zq@Dfh zqfJ^jgHZ1v=i(rmrcJ0qE#-MY}el_kwNUu>Q7=xpEew z0FlmUw{yokmJT#|X*q68&J^ zP*Rw%oEMENFr;DnNigMFf79rEwM4t0E$3a9`1(e<_T`=&I78XOTQCSdd>JM1F2QJ> zE%Ao>LdW7%r!_pDQk=)2VxWc@F+C4J>R4{g+cKo<;Nre8PAzd?ga?ILP2_$$K9aE8 zqc_a5X8+2~K*a8fyW5~pY*#~6&%AJ-jz<0LelOe0jio^GL32xc=|sCH(u|FmrTSG5 zlXO7V1%~mNNCiVK>KFYJnzC2hU(P}r3vZ+KVRCXkco(qhyUzt-kR-qCuT~y+WQB(A zfXOc*3#=%mUu9|JzQCb1rrg@3F&SKw$J!Ib30$yW6>TQm7 zqX>>vwZ+cphu1@ENRJ|i#jDt&zzcqAt2p3=t5vI<7tU`jGKi=*uR0z{=AKe~ z1f7k9FpaYo_t{}({L%X2cjCUAs+skxTRUe_`CuZbVm88E=AO`XJGd|W75gtsuV{F# zuR)9uBb&>a$w7K~120HIJh zt7{IEA0Oiq657YD`Xhk79MO-xP2@yF8b*?_6(+j}+r=ZF6N4;IX=*{HWWnDNa~?!~~xKqTk@I zMr(8&rhz>@9A_BD*sl-&MD)mMa4JKxgd$KPHxele<&IUty$%n7a&;r5>0N;;qZci| zapB7-4F1kT6UTKAA}ky9g1fu>)u*wJf#KMdc<|Y(Gv9uSbWpt0#XMz%ie0kR9cgy; z$57C#%#-69zfx&sCj?&S7_sn2!gG?WU3aw!TNC%G!(sfm79HbXLJ)1Wd7S?t-{0Wh zhB3&MAfdC1a@cTPc5|RqLlFmLhmz@6W5*>KHqN}@@7lY9K;hsE4AU^&`{P7bpWvaD zVF3Iq@B7Eo#g9zY*FTXDug#hHUP4(*0T$B#Uiu5dw6j-*=@`3qZVCf}g^NA1<&NOU z72A{JGrwl(H|9s@;u|?jp1@@2-}^i-n$1vzd!v6bMCX1+PvBkP0Uv11_bUdQfjeG( z=r>yHVT@HVe>g?^)}D1VT^Ron!NJBby!$(gJ|`7Z-dCG*U;Ckoy*;Dx*M}2DLXB6i zI`Wa#vJR&fH;-J6X}m*+%BHSzRUzY>f?$ zOY6<6_LobXiGTn>h{ukA1=jO#_z(P$ci}a)0RByfv1z`>m-Oz?DXQ~xneBk<`4Xr! z!4@S#Z0wK3%SPVQX1ZT$l$&W$qes70RY_;n-g=&0CKdj0vs8^mB}x5s)Dd$W2wcBHUC?>Plk14{GN zfn{u`{giT%CEHQbbttsk>m2znW$I=Xh=B=NVzfO8Mvd@*1xn23{fRnJS{<0SF&Hmn z(Eb>yQmzMh?+k#j&5e&58()}H-S(}!M+9+TE06YEDWcm%n*nl}sX*#+<0(*dey9OX zwHWC0;Va3Y&2rf?;fW1IOHsHF5IZ6QJ0_Fw+o2%_+=fQOM%rRG0y)A<>WKJR34E+t z${QFgFBvM&=m)vDU?$u|C{Q479ka<9--z+Yq&rS`{|6WQ$nlMeuX(@T`|;(4zfr>4 z8eGE#9*0c_9C*ah!nQbjBR$g+~z~VqHu;uv=l~lCy zT8%IaEeI;QR#|Q)3qi=5onDu2uz|V`H_11RVBnhZ`Q2ga>?e~Ks6N{v&!O)R7&}CT z8pd{v*b#=X`}C1syB_l3#MiXKq#mjLqK!X^p(&V0vyvbG4iFlF`%bs{VvDzReSQ>( zOEbF%xGcflI^)qsb#RAyfPAbkrg%-Qsz))}buEyGR4=iV?HhC;P?OVBJ9)PdBJKjg zm?1}3x_@tx+(ESFZ5Scw{A)_-<}8y$r%`o`Xr`&Nm;2YH6-rKXgsJVdD8OW>jET`` z6k2IB6!M?%Pz=KK{xgT4`4VscwSs|{_EzzWR}dM;__(z5x^(`(zxTG{Upt0m)#>=m zSGm7Lo1*Tf#x&Q0teEJ$U!hWuJqw+5o~hV_vbqZTtS|q#Z$dd&jk(gA`^AK>|NJ%- zyCR-p0fC2aW^E?{D+TaB-s*F0V=c*dq?~BKwCV!|iCZaugPr#J%-?^eOyq+8yqId;Jb6_1;lloJAqnl4 z;{Io>A&E>~lPx76)76r^QJ_iU*>R|MM{NnOCM1v;q8;Z$a)duN9DAm*(U5TA%Pg^y zLCN^$OSc?-=U9#;?#I5P-)*u4dfoPx4Jg;nAs(d-Z2W(`Dvj%F+r=QM@7l6jh2!Za zx3xEvCQm@R535w8ug93Wl7${wHv(Hjm}>Hfwi?!Iycska;V7q6X+au`c%O~78&28I zD-I0mg5C=v*T{OZtJiBBETAnBsE4NDI7t`6sp8TJ=@lZukXO%AQP=9 z<>_e$A=uOc4d;lkL_jaHd4^=UJG~BO_|PxX5uFHR7#h8!MPl-a5qUR-;8i0-zIhGR!EkDswz(BwZs~^)%uh*EvzLJNG@ocfx$wi@6!OSG*9U9wzyV zPbC%$_GN-6JpOzQ03PsgQM!?K`S4{XAapDVs<@EqG4owO4qJf5hh83;B^DBMLhKcs zwwL#bq0P$;>ojTNSljsAu+7#_d?{gOAy!?ly5qoQ&j9v#dj+A)3v zpta$dzP8=d(2iKbtJ~}f6m=R3m~L#~uERNUbWHfLP~U{B3yJCM{1^n|w8@)PkhKY- z_)}lZR^7Nm-B2UoO#?E2+1U;J!^0TjC^Gn%lmx|U>}$D+ z&@-1xeEwmKp04Ws_VyYe=bw{#2oe$z)$;OG{Mp>>=`$X2ZE^$}_&4&2?&hi`7ZQAZ zSMcKpVc5Ro(HtJPK;F&syKPWfsk-NZ<7k%U7>~FumI-0}73C!ORE8L_H6$#`ROeqH zF`uQ@9LroO?lDEDE?0&Q?|N;;UFF?zH)ObmyqanDP>v6GJYH(XBV5vU)zSGrET7?$ zfAq9MtJ@w8fOInRNO__R1rt6;>(<^io@f`E^Bcqs508U2U0U{wZ&o`7i~UtDYJNKL znzt{_?AxcJ(nnFv-m5vVc?hU}Rdi$0V;2cjZ;H=|MdIM#k`NNta=u&`6Hvo8l$_jb z$383Q=B%l)sr!nPp%_Tr$za*9`K!GmYf$4459bMHW%dzddEWnMm;s73V-iDS9Ond4 z%b{0co4?Q}&>k(x!>FSDoq>uU+1xmMy&c%dt8#4ahVqrDY3`yjX<2n2{x0YB5_TyR z2jNK-#l4s2T0HeAWJ^w#%ECn4ntiOtLtWTw)z#5VA?XD$YOEbEh(vZ`0HHHUDwCK1 z??|;@H|!Zuz)$mkwM76ot(_RZr+oL_-wL|A+FF;(IajT3co>yQA<4(V02$mD`0G48 zzRO|8?Y7v-_SV+(RHSTHmVF8FgP(Zc0_G=He>>UAv1g>Q%G~W0)YR07CmJI&2H~c# z=axjGGJejVm`dVLvR``@HcVi;0m!;Bw;Op~Lu%Hh!JTXiTh>$l|3sjQ`VhI9@IDUf zb!TmTDhX>242&wZFTtHXF=Qa5$j=JGkTfH~+v037n4j;Jap%WOIeT=_U&hoAi=~2A z_9KI-_LvpCHgp~DBGHmNlarM-3p3Tj+AXb^M0}JKvDhkqMn~g02G09PlUjXxfIm+5 zgE^RqcyNtz>Hj&w0ME~#!> zS#S?qmr0xMxn>XD<2RBpx$iKR;DNR0O?x zRB~`fjR5wP2=@*tuN``P5^v7Kg)AkP4=DA%q*m>eq{ZP9jRxwxESeDPHMs*TXbLr8 zi3qoM@`!L`myVB&EZ&}{%sz-FkP{cj&-hP%%F2pe&H8KbCBQKY40|$mJozW(C%Y?- zi@5y9m*n9u)wPcJWn0-W3-S9XTKX>-J-pmsHv_Hj`Uu^E2T9Iaxv=20(JOQR^P{f0 z)Yy11^*KK-D9$na0m4v_;z=&S{;mtS>B! ziqLo;Ayl&cJ6Hdp7?JRm4>G$v$&+#d@w2Ub(`_ckzLM8-U0?)Rsai71Ue%l!Vnu0D zp?grSt6d#Lj4>Qe;=kNUCv&p!YX>!-u@kVIYg&z~Z9O2$vQL6PDLu2*0v)PYz8U$) z_8xz3@72)t;sFYujhzzt{!Q064x*B%o6;Gq@Ab!CkTNqh_0w)n-k)S-^lY!D%5#eC zNOO0KUmus|GB3zbt5LEOEc;EtegY~$Hk1h5puxdrZaBCjXC#mDBn}E4C~J1%>RKURPASkt;f7<(l)U zX+&JRF*v88+P+tEW<1Q9wq)Sy9_wok2$Nv)utF{A$uxe0MP~zOu*$3z2i{I`oRmgZ zt@mB1ioX+3hz(~4bsMIUGUkqtvnnHujae-%S&faIAQ{d_FOwEphrCWIYLZ0^YNg=M zp*3&7{AVP#eiYK+ka9h1;A6hjz?Z9=4$P{Gyr(7e@;+xV(XNKPc$O;QDs_Cda(XEn zD*#A#PyUZ8iTtet977LJO zNZby1Ke4nX<|F+tna)MN7{P6X4o`AXP1crtD>hX~GhP2)Gy5Q5qH7#YbJKcrW98Wu zc7?+TC2EVgLya3p)&^ps0oODbsE8oE3H?VpC*d38!y$mS{GuxH10=4GAOIXiT<&^O zrcWg>0k*LCe)}UKJw8V=tyYhXQ49M$fQ;DnGhW;kfXnA(OT}umHOUdQwH_J$kN=W+;n!cfx=EAwhfQ|zq zqXjhAC<$I)V_oxo2e#2Vza)x;(P%IldBn;sw21t(b5Dzc!)@V3&n4TWP!{Qo7)hPg2`OTu=& z^2I-=1hBF73Li38Z47al@)7hH4|23Q=8L_yaUPY#r&faf%IHz|Fkg7{R&1-~O?~~7 zWR%H1z3rv%l|!HYnGPF*?fDF;n-Sv8!ePuC90^N)GG!b;)Bay zZ(m6Yl(av~EQ7JYXZn1f5vJ9(`0bt{%MTB|m*kKcJk>AsJ|8iWW!hz(C4V9BzMW}& zj{!`%Yakl&V5__hB!1`N9Ca5rX&G2udzirxxPKLdpMK$2YqW1BEEL-No)hQwk7;{< z1!IMuhlOpqdxaG$`rwj9M%)kYt*n65oEH~8=LGK{9c+{L^K;!d?;mQJ6del= z_#j3`K_|!YhY4#I-=`^kmh(wXFsWWUK65sG?RdVG@$Fkr`}=_BCMM5G`6nrmftND0 zpwBCYnH|??yA4MB#rBwC78EX-(VA#`@>p&dE~G#)kd;#hFS~WB&GMj+R$hXw-k)OD z-eW{4$;+Uh--btE@~lBjHxX4N=NU%)j+~uORu7?QGpLc@i}#8+`exdIR7Ej9dx^}$ zIkm?^xM=dI=YhwReLB@|Z@SSPhV{Y!F z_gWeY*4O0sNy10bH%^fJf&B!)*Y+gdeL(eFG^@$ynyc}M(_xok2H!u9>K)^TQtIi; zB{xjmBZ+(E@RPd3^}ZI*R9(C-gH zuM)Y`ST}OxmBH~*O3@3af7`OFfRR4%3>(H6gVglL5Qqg23-l;8@|NT%$7(L$)#1PW zgKg!6KIuy~`IqbOPj`QVGip=pKmIg7*a`|pv+Q;vd8*m|ILJ88l;aUXjqYy`E?!9a zo2`vH0aMXzsHcm(S6)o`JWhQDm_A;MV23P!cw)hPyBv~S?l^0~*uPh!)rO`IgIHTz zx6M$HzNQbU87xLx@ARm6#lQ23L9@9y$oK1JOaZt4>23rVii`7!R@&xazY9JrKXNk4 zpGR2ukW<^oIX8*|Z05F<5s|ZnOlf|E6yyTTef03LoH~>IZ*Yw5PTnvzi#mU8aLf6R zZ|(K*;2D(e-NC-4C@;*O0?Wb*} zKd{OQAU66LrgXcZG9109G=!0dcjAI)*6Jm#LL&{pv=IN|fd@9Uj0;N_$OZ?JyeEKp z;Wjd%>0oKHM>!oDpc7V5xmsVmAyOCldQybe*o4pC$Mo;O0z=eaY=i8NPOzh<;JPjr z&dJc#@DI{i-?bu4J=gK7og@!L*~a;Kl*?hvEroEnz-E7e=avrBLBgQ)2S;IQ<>Agw z^^k&6r_n5-@ad^{1_p~gS+{6$TEq%5O3KP!7Q7>YW{l>|E9lrXRZv}UfY1GetJ4+|CFBZ4N!PZ->IaWwy-BC8G)4NF*Nq*ZVA~hnagicz{ zU(TP{x_dCYyU6}{R?-SYq?flhG8snJ8z1EZ&m{uxUY0aK&n(AciKJT}j&|CRvAV>A z02B@9vh!VdZCUO>xo~E0EQ2gD(&1}O))fJ0CvrIRlI~L+`y9e2@|dbEYd7F$*UN$* zzUe-Ra<0bje}RvC)NyE6;8-wr)r7D|n3S~Mxz$%o~0*vOSZl@!7~ z)^&!Pe~prq)a~XkM72T-L2q)4R`pIO2gqh%=Yog}UA74!j=KmI{FJvOqs~^bnq^N1 zP>ee}h-_1RI}TQ`R0ta_-$sUDCJ0S6d+gQ%0g#dso9@MMsN8P}K)Wkf6Q@KKc587$ z)zA0t7rUc~^1#`n`9!i>6{Z~N?A_Y_c{dC%m-)L*U9+BH%5;e*krS8Svp#4u<>?qB zyS_irwBLn_=5e`+2Mfjw<5v`7$y?x`XPJC=`8({!;3nGjdDjHtV&>-mpanjlr{IJ9 z{i`~%iNL1=Qv-x z`IFx@YPceQ;}pE)Dmc?%0(~X^gxv`+TmO5}M&JyQnIX@wm%>LqpB9%%#UGy@Iy~sO zriUF|9t<7LG5LYQt`n3EA3q+ZXreW`teSjW&Yf^*?a}gmAiIj+NEeKr68_kYWOZ}o<#{ZT5E2VZW-t3WfcEz_O8-^Tk?T&5*ut_p$dh9-PB3X zBoYV4fY%bT+G8b4q%lFPy*ZM+&OTxw#AYg(&W8pY(>tAl;)u~O7^#j0VgbRF4ey#P z2jn);fv3j(#(4@3CsF+4)aL=OS?MEkyS*wji-RueKcSBmfq>E25`f$dMGK=EH2co>1UUk zUylWrALYB#tw{vZ4+u*7N{e_OFO_(m;6O|*BB$SvmuOdcXuN(%4~PaVzekM55@miy zIn+mjG)t6}v8DZ?$W3=C{gP6O|lq>0(8 zVePfSX@pDhNCJXf3-XQpSz}gL_vgXWWcI_`rWZT*xr@TY(^O1+2mmr1s|$d7+BAT zw1n>IB?_JKqv63|H-BB-hZajb3yKc3*=>lPnz>#HF||;z&$w=kn(?=}M06)uqu@KD z33t)C5goU)ANUJrm7nj~g7&5#lG7;JLewB)#>MD$eTU8uc^^^Ecy@1nw1c;m#?ANc zuXa|FTsyYEU*G^9n8o2^~_iFUyc;LHyo@yPi8UJH|k zj)4IfWr!S_;V1nWTGM2th~;17BX`7&2^o(K0UM>NigT^6!6h~j*3t9 zLv1KVluS)6RBl^ZmQ0;BkO4^_*eex$dV$Z|8jkt4BFL-;0LcaabFTzKt!tp|^F?h%ou5sZJc6_lG=@dV2?+RH-&TC@ zp$)>`)FenFo_@4Z75XA&Lc+^wkDjCPiiyr( zXFOjz4flifszeCt2?*_?3$``Ts-k1teijT@7f zI3gci=$`~}@-D4_W}j&Rl9vC=?#X1o=0Sl-*D(){salc)T9xO=O21n^#`fUZZ8!^> z1KNiAcaH`!5Nn@3Dd;&rG2t_Px*B@m!3Zh>#LofO&ISj4`0mpWY4yx{A`HuMGfGo_ z@~_gqh`&488?jlx5>=56s@(jgLR9R61rVdn0oOj-fVZ?Zd9_+n!0>)ZvmW< zY8`MvghqvnvX%*lLFCQn^)HwPl`St!wy{&!N_ro|KkZ=QB+rwt3|dhqENh~JpKHT= zV{0BB{BJ~Q&8>n73!E8rHCY;CKOmN*7$^=qTgDe3dk+&Quc)Uv_1T);9VZpAiX6f85Z6^+j zCODMa@clBOkUgtB@Jw*>w7wAj2|&Q1B-AlWG(ZA~o?dF^czlj!IMEVtf87g&Ek?l^ zi=Dz6W71KyUd{@tzbM-!)u;r0#bG88ra=DJ^8O#@;i|LbkBP<;7ugd4x^`Xxb4F!#z(s| zc?+n?b{-h*v<&Ah3&=g%_u>2#!8vsONBIi;Jfhh-4sP8|y?Zju|>SS{Nck ziS`N#6bM{1RUH+}ctD#S?YEZXhGxq__I&g|D;YnQUHNGDoV@x`6+9b)H~ToIO7Gy) zF^_=JVMhwi+VRmSLk@MU4((v(!}*wH=;?x9Z%>U!^=C;pXCG7;lM%U2y-WsdRfZ$4 zEQz`?B0u)ODUfXN?&A-f9upCRJ@|tmw;qkbMu=MT(uxWxw~e$R+fR_-at+|EOnkg3 zhz`u{aTe7VG_5q{+;Uje+CKLIv$dcHqW#W2D*o`ML*-3wG$yV;nn&-p*D{`~GuDd= z;n6O}nL?CbPgeuUa1=pbsUA9Yag=~7gwMxwEmN6t_o}eNpV8Afm|PK3(=+5N6wG1w zsU+}EPR+1e+IC(<`VnuighnxnY6Ru#j^qmka}e;*L34gqj5I-KAdN|24LbV>2$n-jJc)+re%5; z6&J5~rX#VDN8Vq)W5XHA5kkSU!q3sDumw6}J?)p;rD5EoFPlD|q%@hGT9!iKx)|nv z=YPE^DHK>II%H+lG56#!#|uIK-b~tq4_6vVyCUkbzBoreqEe=hK=N9i zsCabO0OK@eADslTHtP+8^L#086<;>o=&uoQJDh~_*rj=;U8rlB^@bD`=QjAy;>bC6 zTO~?2%AJSKMdZZJa|w(ANJ5>LavO7sJjbD={6DPMg|z50&djq#)0{q+m&dl2Z&hg# z{0}49rR?-kTB3rq*!ZeK7VP0NPds;>(_i85EK`{lRu@&;W|8O0o~PP@KT2rVbCl`+ zw7GfGGE|vi2W6C@)%n!e)2Tw3`K#@v&`w6X|>)CJZa*MIGb6yZveF_;kx*!QyYaohvnW zQ@-Z6*B@40yhx(7RF-Y@b*j$S7m;E-8F8K9POK9xLf6oTdgdv^lc?YdcSNMXExKP) z7em4@i*#2G8DeS{-X)!9KG?=6XCn2AH)`_?)NnfJzc{R&F~BnO4vlxJYyVz)I+t}9 zrAo@nXUQeZG1wl@24eR`2GU3f@&R{_mq^lUYVA6 zwW-Gln9A@#i>h>Wn~x`p-ON?tNno5v{>un7!XH2qi;BR4fg%S7q@o1uW*Id0ZIH=}dQWprxq$Y{VOxDS5|oAij%X&zk|-ro2T zChr*{x#V&+v@7Py0z)UG-qpWRQd>52Xz%QvHC-7Ioh7h1Md(A^BN!C)YcPpUJ#y#RLr!`;>ZkshTCtWJL*Ip7Oay zpfzoqlrjiIm&(A0FZ+iFV2bge&>PR<*t%)qPs{^aW6#_@ks-h&_?XyZ^&h71b(gX% zDJXyKC)ZH(hPwtU(|0X0_3iU2#YhoJQBwT{*Wu1*o`#UxHv5?C#hQ16dEIaJ#A*); zL_DM!RDWe_4^E-zu9x9UPK#5&weqg;^=iU+Fus-z$@p{u%;9EY|MKDV+&%hm8USv z92zbq9LBM03!cs$0p9eLAPx`NxIO-F?9j08epc@BekRpHNzR(3!=ytv8uT#6Cy!av zDh)U{B=Y#i4WdD^(Dh{I>hU3Fv2<=d`EKSeEmk6XQc`_i$ZK%>Rio%55dj%)tXlOF zt_I1b%B815+TTL>W~2fM>}hG;hqFkTvUyk-KFa^b`+r)1%xq?5JA%ed9D0%t21-+w zy{TTswGUfJFLs;U?~U6Uh-(_c!InA)Uu*{kw%Qb8CB@W44mW8w=!FxLsTN_Xy(dzL z_g9E5b3x2UYctKkLU|2Z3I8Wh^-r1nnAzwpL)-m4Af`?|xpBE+aIpIg4j5!3;JPY^ zN!_y<7jL}Z*DBwn^2?2;&NXiSi;4`-$le5F z%P5-~r1j5$68Il&PN#<0C}waGOQr^^bWL|oCdDEZC&0L%zHfOk7XEnFhs+K{U1Mm24p#zGHR$VwDPA3)nHG*lL{Jvo0&7~i5wdk@!Il{QY z>z{O5;OOx-Ee(y4tOC!E2`Hw}J`&d~3Xaa)4>M^{ip9tML;$5PNbCSX$>a6ZI*^AwnK6)FDueA{PD!Bs+3xyMEQgO(3^ptN5#qwh}!vQhSb*K zJbo%)#~WmsM|$+G-zj+9_V3_4%@_9 zB_bw1!y$l}N#!ZA+C&=aj&D|4)H!`lE61;{ z{8N~$%yqB$6Q5tF3|UV3Gi902*EwtMZ*jpmbkqzC)I^PkC_{l`oapZcTa5I_=I!2V zUB;KbETRqs`Ox||3CgxZANnW86))%9cP3+bCnueH9PDlW_Mr=0!T!)n?G4YnatHvQHkE?_>Ahki*`D5{FC$@#&g;iJbXB*uy`FN#bqQR+d)^7o8Bte+x&yfShFcL_Rxz>HSSk zH63*Q?3vD*#6_{D0#1Y0^MzwFp`4YGOWw*Xq zQv*UTw4*`m>!@_^Ts65H;jJ!QwEscw7;%Iaq+ehIm5A1Stp{n9JLhRk0CG-YQh}6{ zJ5MbSlpJc-Uf#>7=ou?p=+b5z5Mh0k|HTk1(iL5rl-AjCSL&ZfxSk;F8$(*W=^ZbG zh{WE|zJwD%65sZ($`xT&eOi8GuQX?N)|abo8jo2U4Lo9tL^e#bsVDrVwIP2_;8*e7 z?!)`P2uRjBM(xI$b&5e*>W^!Q(Xg=_Z7nFE&1yLr2&AINHsHNU==b$a%zmnu$oktR zaP0;TLKGFbr&OM$XJjgB^SS>(S^Z@|SZsB@(t>I46rjmKLpk8`A&uQ#H_f{N@LFEg(3@9d8!;lHEYe)9Bwf|(EY#E&@sRM|Ta zMHFnJqN3X&OqFEFO7FPR3Y&7k#)?_p^ClJs0w>3JzSF`LQiFUqK^$h*^;y_3!3#?n zYjH_${nHiVuFC!C~A3@wgV=xZTdXUzd z1ZF;4?^j;K!0n;wXmqU=fU=)gU#HGZe;J960P)_}TwSKEA1|k-UY;~As$iC_)#DGH z&i?f_zv5z~p$KKXA#d8qqYrl07GDp-4^(TBh&3W>c_Zh=E=g}JHS$%DunKh*Wp=6z zi7eD|8d>lCz|`jg{O4uVUWk3x*Yt)cZqLV-4ZImW>2~R^EjmXtzaDdj_5Ulbh%$T! zowaP%kC+3UD3$v(RMq{?Ip4%YW)u}0LuinPrW!*pZEQrA?Cj)Q^mJrga=weX4t+Qg zd-L3P=tDrCetdiU%ZlJvou%$nvFlUb&)O+@$exgrv>RpuhAJo?lxHvQI$g+#xw-5HM^VmZ5{GP@b@1J8odF>c%XD6QW`^+6W|8tpuJmB4yh@swGEag}>Ci+cz-C}@c;L?z& zI=nt!N+_hJ`kj^pKv)!e>P{-HS~Z2)k-;bw*fChl)(<-yc)gE1D4&7s(X@Yj)m`;p zH($rFI*^g5XSr4yOg@_dQy<*llxDb~tH(Oz zKkT;F8F=uLlh#NhX37Q3Yev8-xjy0l9vHx6)dL%aF3$Q9OT(}Ie;+UU&cj?wG=2)0 z)!F@-qLrZhGqyS8dx#UKRGPz|i23+E?!4UqztKHyR(Qw8Cs;%_Bo#&|I{KM@W~N8A z=FA*dub%{yLWk&LK9L<=VQw`|oG}S8c1s1#kihl*SFuEyX0=D8>$*CD;)#>`w5uN= zd3yk71V_Q|F?F?8HZ{}X3#p@>AoO%c#~ATKro!4%Yp<%t0{`p_OJ-TWg9KDTuwKqN zhIydk7q)FPJi9OgEVmePtUjl@M>y_3KLj|r>~fg@cngTG?t(LIbvbFLQnOD{{r58~ zkj0v({wuQYf_o&+-%8GSL+`Qk*G`Qwg-|XPiRkL}KKptq+yb9wrhyC;n;;4gEwp$_ zB6iET*&425dCx4CxrRYJdU(g-k104f{E~jXtM_T%TmmhFaLTW%wOFTwp6HugQAfNl z#EWY*q_fH4hh~lT^F^=avX1J=q2wf)C>k4gra3PwQ!jhdewRK^4%^A}7n9A%khRlr zfxo$GIYq1WuCZ*wEFe}Do0+F9MWFS00<5;64OHGks)CQw?E*FAl!)TvV&KmoV%bTs zK&{B=a_rixxGR4o|M@UR^%Dil&Wp)d)+q1?d_l4En3q;ZiL*a1#$QLcF$gy>ajd4J zqJT7=oLry9usuJX$Ld*z2iS2-1+P@?r0-D@PId!G#qnB^U$avwaAJ@gj}OLjm53l4Urvo!W$b#~^58ip zin&dqRdXgbz}DvdWmY?Ug08>mhYcoxgcBKrI9oDJ5lmyNa%B6ff(*?HgWWTGvj|45tmW!mul9{IS9+RF)Kr z1NJdkGtMYz_au3b5ZQy^IS?Q795cX76LO{&NmvX9h@sD)S#CVD5p#!3F&>(Ulh%cf zLv6Qso^Xods8jB$(jKq(gk4lw*W6DdHoB2u-Hw6?tCs>Y z16HgWsaq7Q?#2|!-97%b9eHY)-XiYP#uqYBWF#4=zK+NDYjW}%;#uw5K$a(>T^p_H z^cy#^OnhuTzQuXGtV=`5>4XYt2`y}=n@~)o1IJmU(o2R{7!@OT`^P6xYkybX)xUti zkSO6W+jAnqct2gE$ktO?b`efT;>1fuW`#G6_OBkvk=83)e*Fr-9BC~mnr|5?zO_{l zG~xst!tv=QSLU03AZ||bTVnVr6~F$;!ihQEwSNSnKjwJp@Xj9sLDSbvAM~5{_}XNT zJq9o-u&04NFhYQ_MIWhi+OtmBliV61(M`E{qd&nzhj~{-Xx8r zUmqIAwsGU#VFM7`a_~%c#Kn79L>iFZ{kQF?zqxsG-A=@PK0(SCBJ~0Op64TI^q3IVY4>XOCJMq5PeoNAQ;?< ze#G&IKNNG43Sjo}QXmyiWnF&t_8wu_d~p9P^#P@{sFt84<6TgnDq( z&@)C3V+`jH4-tTl6Fgo9*5F#6h5h*VaC@}laDyQIT(z{r{>10PGtBr(ultW&nh74u zRqEQwJY=shbz&mbsRQu$Ras&^hOqIeS^KFI9=CemV-dIux2kr+8cxYR(l^~?a;6SM zFz)yxv?c-i3x$cF`1aWVFia7aUlOr0e}SauD;E!QX)bKlM?HLt>rq1jx3B7*u$&S1 z7n}X)o(a-7kdAc}8lvMX#E%Wxn?FXwZq*{g!_i z&s+#9u;UrLEubZWQt+<*`3ls}oIHA>PNp&E+ekTg!k<@~w)$+stBypFBTdUAv;V?* z0c}+XbCOmzy@xf%+|6K4aWHR)LJRf^FAsvbhd&=%Cp>3m^&7tYr@tj7AwTEnEPIF} zLjNg*L@$J&`eFOzIC0KQ9>nu8@c%nWp+oSE?Ey-EMj6r3pYTi{Xs?~yyHR516?r>s zgwl!f?CjwobbWn^5ApU>_%d@S_es~UV9Z3KDqH66M_MBf3M@et-@M`~o=EN88x+EY zc+dX4wzOT)f;3k8++H$)mcE=2zw9BNh2QKsIIiN}ie!1yg|M&IOBOrIe+*CFFz7pv273^(o@em7}YN+t(}h-SnIZFuM423ZF@{S2-0_O{Cn2w8t7* z<+g7t1o`CzeS9GDAwCeaQ^5-rgX0w>7S8B~te2$5&paNJlfG zZozpOFq(GCbT5U7S(YX&QwQ!2pdy;RcAS%vkBY~@e;d-#PgbSxv?Z4Nm%>U57G%I1IUuc(ZE;iBHTcZd}ebj8sbL0 z8A#OtkeSWZx;ssfQz|TKmgBH7*l&y1$79m#(}!vLFKiZ9m$^T= zKb?l=c1^HSA<9Vl-fNWf^~K)A#9RUyHt{PSze@?JV;&?X#n_b=467&r<-$ zXhWhmPX%~X7yiJvUZE)HcW^N~8v}n;VWgR6j7avlJUS(vjsR^jeoDE`bGW#cMx9+F zAEIWtH{7u;d-mFdMAAO(??%_WayuSqBe?Wz;#G_xV_~oU zUOQ@hhyO^>rWfJh8+y{W#7>e;J;0px0lCdNIYjJMg70I;?;15_VO1{uV?DYJZlQBdZ1JG)m|)|4^j1KHZZ6tzu|u?nvGX8(KX2m*>mS4-H-Z7xr^K zMjzc5+GQJhI*8vhY`F9uBK(-d+c5&`=RoSlzuX;V!>K=H4*A(8aBt(o>3T3#Ev&@Y;`Bz``7Ys;8(w zfcerl48JgeiqXZFelL5Sc2_8RzW(^j?X4SfGw@MF=$Jna-`@C-?$?Zp*Q?aFtZaza z1N_vlL-EzE+wF4(V8i_H=_P)I4#IxoPSBqcIZByl{CsR`^ot&Q~4-6wh7&=aN$#6ALNpQ=LFcy@I3 zWVLUL@UG+o<$47eBN_<09)V^qW@S#*>o7VOIn#~%DL{_s!lPzu7sM?g9T5Iedz`j^ zVFoeS7W~AI?v>y%|9W^Ipx95>>r!W9_&=~<)g9Tj54+sd{*j}0p%kJp_Vrz9pOcc; zNCW+i4OauEA$b(8a@6xiYcbZZ^tlQuPEOxRF{5b!$W&hfvHATk4x0P=twuz7!if z31RPk>IT)RvX@Kcu;Ow>z)_Gmf>hYtHvtXv#uSEu5Q<#Klco`5Bi?3iv&Ik|iYeX3 zz}bxpti<#d(+&^m*+ zyXKz-k`#xi`-Y^up42Ni&hGPm8NFQItL(8%xEKC3TJ%90w=;pA#w< z6F`2X1BaIP`f3mZIs}gdGoD$#T)^S425zb|%8sGs5K{eS=)q_`kB5c!1PJV2flj;% z{P+fjQ~0%bRg~=NdKOXxflGnL5>6$G{s?Kw-fzaLq|u4 z^s4x6=CGC6C#<%b+w{BZp-ld$nN0rhW@{;3QU!X{zN?Y-FDVjqH9MOsxF>q~7$J#> zs^ICOr8PzyS>^-jSw|0dXY87Z*V}3Uq^44%v<_#Dw8?>F)3N(yU?j@jtQK(NGB7Jy zSz0os#Cp-V_3sgyTUv8*P4SD?){G2Zb>jB7kYmNq^n=`Ha1K*we{K|?daa1{km67p4)TQ_#j*x40APZ-ixL#Zec z6K#!#ef=v{C%KOUM++3RHzbz-e96jox~3W<)~6M04>VAKjAPh-zkeI5@Hr~DigIB= zJTe0LM{l-RCP1(+T&MLMzKY+@DYK$INIC49MaL;c645Vc%RrJ zBQDL`V3$Nk^*Wz1bH?PrlC#?d1{BEXWov^s zSTj-Te^6yI$t|y5dX@>Up?*`CG4TTvj7Jw2UbBc=ZZ95xCrzkNxb&;jgi4{BvJh;~ z8sk{W+5&}u`3=XCCfxQpRN{Rz=@R1l$3@oCT@-Q48xab zK$e63%XB3lJ|d76<>ZD+HIYGfbIA+QeWU~L1uCrKHcsqbkM2(Q5PK7v^B@L$_~)rI zDIedn2W!)>V|k8rsqw2d+x&|}tKG_J8Ag0H_nF$9xp*#hq_8m0x^)xI$WXxfI&C0V zOe|EoheuwDh(E4o!}aZPEBq|#xb%#H3%X`Vy1AExjf+^d>#Uj+I%h{rKOHG+GXJeJ zF*I^l`E#}yBhljaw}v2O6;IGo1*Fg`ZmF603+WD#6s9$$cDva6C(>;G7?4fKVV(63 zH)M<@6*YX73NnA>42=bM4)gz4%V_8Ie8_sTsRn76R#a>^yI}0w4YD&z@o~R!g_(xX zIWgp1`HRAVUGTu(&ThWHvE`H$eL;K%f=MIF@q~i3kL$f{U)V374heIDNli11b2ooP zn=|JtD8F5Z`*rNIQ^5Vqc02&z9|)U6y>uuJN$46LAekQZCbv$9i-k~vCH)Dr<8#z= zKfuYsUi4f&RqgGNi6XDmOfIH%2VMk*t~g8Pr&2ob8K`3w{k99V-rHw^qU+4J%A0*C zRFAnAS_B9FK}v(={UW^A{q=-VW2i=op8t*v1KpjlOP>MU>cgo%$bBLvROYuD8v3z>F;Aqtd{ZsKBpR>&bwd&2eye8yBf-h;>^`l<1O=VVPF%a}&}Fy$V^4tia#Y z7ToqP;mEM?j8oeY*3LrNZ0^HMRrNP7ZV56?U0mY&U{pJdB-ZY(m+5FUmtjLE$f;#; zi7`9iH_ZQ$Gj8LDe2e05yq1LYr}Kp9AZB->!s zs)D@Dw1$+_ps9gW6J$Gm1eRsQek498c-tTE$*SmcGF_s?M9I~Yq~UEqvFB&WUTBr{ zk-$3Au%V$LGE+`yHmD|mcGiPr&)CpO{{+WpQcoC@Rv6qQemVAnXdj>qohnjAqltDLJ&?dSD( zP4RsgCjIG9T_A4Y85LsDlnI1?nc2HXi3{KCeGHOT3B=|8bt)SU$(U^(V=~j+@zDJn zBZ)9^ooPn-MLxTlf=07}1w$lO7WNt1-zLoiWAH9k<+8Qb8{9!FTH!RtF4D0XG=6`K(gHoleWy?zn5XL-j_viza1fTo z$XQZiV&VGhFR1Fj*B7ROZOJhx**{3x_?Cz*l4w*MyoU=)8<3pBie24@2JI3W`jjs6 zft{~_agSd^*DTFaS>-gTH8*ib+nE*I?1C+91wX%v4z>|Xo-*E8tN54cCUep9YGW$l zU8F8b9VP(Tb=aBcU5puWt~{>iXI#S&ke9cdXk|%8bz#z5ZZtm3@#YaiwVdhM8&ux= zLy`cvn%#8Ec=Dr+iI3PnHXToHT7+5A@+_Hf0jXO8YhRXW_hb_5jq=x$VdhoIV=hoo zO|Jnhi}>$ne0WOInu0v2kgtK#5N08~J37I;^gX5svc{p9m61T-kSG9%FDXc|EC3DLb6xtrh#Aq$w%fHsidF z$HZ=>pH~HaoP++zrggPO$9DfE- zp)45P3uPBnEO~0@LAG?bwnqo86jMuOd;`%=7-$FJk@fW~AunR?bpZ#;q zV+-Le`2Ygvv=)<5(X~5#zSr(uZr?0=A&YZSyb0c%`&@UB&KcIUl;G5IX z*+Z$m|#fApl z_@>7FkUnuvAq>giZhh%^r1gQ5c$hgLb}^=~eGW;JwDy~a^K!bHor2rSY7zm)bJ(+a zHwXb&5*@JK9DA;+h8Tk zVx)SvA_^9GKO(wC_j?F)WI@l$3q9i)&% zM4NRdo?kXdY?oAK6|+3|po>7Ocj04+4UHm(NAoq^cgEfzVJwuzv$NKQBX;k8E2b4f zaTSsB>VIX_lHh1#_gucZqIaA^c`kB1Aw}cg9FyJFl%;HH`h!@T=w_?vqhOED)?(9U zjpO&eSVP{P$v3I_P3vaNzVB~A$2JnC=s;u!laAR1xnKQyHnjNi1;zsm*j~$CJzM}R zC(4=GJdn2Q3p&x8APVh+*YmBIT>b(D1I)$w^nbn#(m+td7C`Pu;)X0e6aW5d=WY>( zlJ79!Onx2McC4XbI2z84c|7k<4>XP*76SB}qlni{6FqZ*a+?U83MbXs*%ASRXf~4@ zC0)dZBGX>KF$vT`@i;hgiNy1p8`Jv-rBuVF%#NibpKU9u)eODhrNM? zZ5{Xd>eHMVmOAh#sK?lQK8hOcif%k7i4bgremkf=DQ@!ghCKh6R41$wh-vSe!3uHo z*aQZ@8w?22C{sPX`o1dIDF5G6M|37J19dO8U(}PYT_%x;*H~CC3fa-W*aKB~!OO%< zDgSqseUjis8=I6l#I^Su#w72Z5Snr#99aJ^xajS#v7Jsr4lxXq zu`_73sv}p9rpH7zA{v!y>T~A}9QoGm!OndHq|B%@(p7(Dpy&DZeL{mQy@k~%ZdN20 z#LW}>HM%Nc-iLmj>U-JK;Sqeh$Lb~X2W?95%fgq<39H)z^%J~5qV=yoarr>5rR-yH zoP=3X@)T{}_p@>D-_HaLc^~JlzB2{vF0&)dGF0+*k)jV++-7>usu$Ozk%3%95ra)w zIjbaUuUb}9Q&w884&EKQA{s;2hm@wqwwpkE|E~j_GzMKRldqsCgj^+)=M(kKHgBUE`DZY?|Ta}nbB)vEvUb!E?d*w9sE*W=V&O$aM5Y--q4!(;{ zCVZ1jjRiVZHkxLF>;6uIE41X3vc2tqNzEFBTEE?6<0Iw!F>iM`&CG|C|xi&a2G zjFv{+EKRW<*23kg%hhw9V15j?mx*tE7^Xzebsv#4GY7Ze9L1;|E5Gp4>1=WT0Nye zZnpRdBNKe6N?}Daugp}yKMj=%0`jRud4*>d){Xl-^FV?;425TQ+jMw};Q(_QzasYZ*ddmU7O;@xfJmaR6nCFzl& zx%N%#+VR%l=yy~v^Q?Je1nX?V04?41M~kcufs=H5L^|d9%WYyyu7N#d@}Bg`E}hCp zKOf5homGa@N-hKj9=i-dYp)M;S&*?F6*pa#O_Jhu`C8)&xZezAVW*UDbIGl z!yC=}CW!j5q+b$$PD5a2ltd?}wG7V_^t6tGMjGn{=Q_c|l8wqRLVXJ}{0 zU~~ikz|R)uF-#$e#=82t)m7eQO#D-^ zn~18!oihxQH+L&{N&C_=5!w>r z1wW<=kfPs1=5x|H=x9ohqxtS)|BD%VmjGK?k=$MrvG)r=Ml0?gm!g$JwE}*N30m&G zNpYFx>T;t3nHDx{FhAGdDc&yLo$d+=5}-14$@#R4Iz2Tynw`llD6jSObY1i-l4$Xi z-MLsBH?RhNDSR1l1PvZqT4-$VhcYvKZzjfF2t$V!DXgRl{pD+9Y2!X#kQfm`vg$ua zmtF4s-b^27Rn?hMo+gAX#j00~eQOmdyfiUNWc|9;|IgG=h`2$Q3?v#<6_>D%qN25N zKc1w`n?26;Bc{*VIo+}k7KUZLGcA}TfVhY0KO(z2(PlR1-mX;f`5u{-3bxK5pX6dG zKlXp285@Vr{^C!0>KOUgSlF@!BNiAZ1}^{y5t;Q1h2ABkH)K?o0k`>o z_^w1?H`_cV^1)61?%~$%ylE}LiP7O|cfE0D9)E^IaBSqJhRoE+8xrxkHu)7`Dqcfc ze?rZxoYu4O(}jn8Ip#1w!!#|<-Vs$%>4hqc{F`HjB>f@u$EXrwS6WG?#8=vUd;IyL zogMP%5f!{CwbeUrx9ochfcuW3hl8}AO@p3eV;AaOGY-CnkBFg^Aj72l*p%6%pm$;e_^1&&hsXW{5q*XkR2kB zq^;zC%HeVE^l(?8@U5M|R7w$h;n|7};qtA{j9dh?Y=gqRRq@lCRNa~mzs&|#Di9|B zBhj8mZuu6B^`ba2S@RaBt(-nqR4CEE`-J^oWZwVtxs6|JW}Z9G8ku?Z|-5=J>ojkxTt~f8?NTpU;1W@kCPVw05k-KoR*z zpEVR6V&bE(rH6mM3k>~ovYrMU#xnF3bGYvvrP}%8MAV*@+!iPq1Zaq^!^{U#YZu6qDmmfjdt2S0wo(h+C(*x13^MS$EQD{4_ zz0)#A_if?Rm^6dpKiE)?btkN^iN`m(OkN~JN>zsbKf9`M_s3;Z6YPiL0kuWk@$ zX2%2dfu8iRCmd40>vZ|xP)FlH>#TW3X2dj-&{9+46MXr|!L_SJ$o;_qn7l7r;=7e)e~2+C!tAIE#<%{7M++*r{h?z8dd}JSx>H_f>4^4<>JY+9<Z zqcKjQFj~XzkdSJ0FXD~mNIKj*#zb_57V1wByWmL&whz7bCW8eMMna49@1ysP8V@}9 z_#|=h@x7uLOMg)pPN^om@zA=wb`MV9Y1!KH>iB-!(&1QxEi7Qy_gvaZlDZ$Fssn@I zkzB8;m}>@a)ug^KsB;=+*rDdITqSl?Vmy*)gc@8$CDsX%HFQ@H=|C_^47(Uc$T}Id z0Yd8C-G4@c|6PMW(xpJ439U%9tRJa!;KJy-G0UKj;4?dp2x#*8pKj>A;|lK|G(VB^ zns^>j;(?t1?)SgT@6zDeH(hQz%IV{uKJm=DVGI5E86k2xQ4+v?&AIK?6t~hz*)MSx z+6W*{{<}iv zf7CPFfJ}K!WTSEEcv{Y6>5|*P2?kQOzyD?V`%yEI(6X!&BLmoYixP-Z;?^mOd5wG6ONlNsf!tPr&r7w>_ zf8+r=l+d&qrtmKU33?xc*Vr$wJ5OD0^Fsu-Pfx!ze@oMpU^x5f4CftNF6mJjaJvuc zQv3MZ2cpE3hDv6VsXwAQI3APKf>3ly`fn;7{|vs>BU}KLonhIWXN27@JZx><`(TR+ z0ST4=!XBgPhs})}hhW+7Kxf~4iwiegS*Hv_)n<&I66idQtobT7At1)jg)46d`_nai zgZNPy2-_;H;AvCPrU{yS%+iFQGf#=ORGjgsA{Y^8VD`bl0Z=Gx&`4H+9VAljoJkhz z;h3N$^p-_AWdP%cz#(}goM%5uOrV)$Vy%fwr%PG6%X>+5WtTEA5?IOkz!-fYe>7N) zooir1e7mWcy?&;@3%?;G{a#52M$e(>onBPik@80bOM+8!rqa`=QXQM;2Jfo(W}RiA zxcZ>!1=$(}ZId8V7gOR56l<07mQ^+F9_u2MA_Z<=yi6oaVUyW-uERvyG< z1%sAOn|a*&sq5HR?`v-%tDH+Rpv%aFi~!-!w0N!X-sb+5K>P^wf%SY}#MI{Hg7o;ba#D$o0ZBYMQ`l0B_UbSM=f}CtcEhf+C0G}>84wJ29Wv$ z2rNlGcAeX{AQ6m`kKcyw2i@S9W1V)av`P68l>AYd0F{-zZ~yDH*k@~~<{cO)eLRnY z`qC|f_$^#0JVgBm7t%%hb{jQ7{tVcWx?}ad_4Be~@CiR_h#Nxy79B1cIIq9N>ibDn z$s?~?s=J5w*iXl2onmMMHJt-ze+m<}Mz+t-D>D#c=ag zUHPkQ`10lIAtL-m^U`nR>mN_P(Q259Qxd=H|2($5Vb!OjY3n=|*KGVY%95kMm%K(b zT0#@5a0Q~@Taz`UqPJ*0zlp+~_4%L=Sm8zi9e1=L9+oLh?Ta*J4Cq!RHD?|?hT<>BHUsnyUVo{4ViRxOFNJF6u*lnUfb0%2o#MBM-m+Zo;y|*vr8@;?< z(a5UEV+cIE)O?h5lV@UIx+VwU0SISc^)bBNjwydwLH0k&zAo%h=d`=zZS7dM`l)yk z>7S>?-p-6Dn{KyGjx>@(N6Lq@AKHCGYKxd}wUjU>wx`oE(z zj&hsbX2lNzEp2{Nf{r8LYDD~k5;+R-e0l6FM>e-A zu_((x|2lPRbd?=7Nyen4$4lsSv9Xz1+)Zv-qC#@5CZe;@dz9$nuhN?eazU#>g%XHA zi<#>meoOYgf^bEyt?MedP^P6_Sk-Q8mmXzR%O&pxq@_kWd(;0hmPxoMK_bniE^I^! z+0}e$>FS|HB-1ZdY7Q@Oe&18-6f27m8qguOW}Xj7Ee1<^v3PS>ELm@D$qC|^w*_F= zS+(tRD+SiA0{Tr)OnZ%FrgYSe8G4aSpy@)bM_Ndep&-2d%y_ja@|U6ZVo3W?0V_ea zy<%@Lf3l=t(8b>)9$}+7v2pM4SOC@JvD6yzE9kDw$ZMc%y3UWM=sVjCe+VUl0+()A zC%L^f0721sQ-!*_Bg>aYC~x(+=cRB$etzi`u#7T&=_!w2iJ$exa&1jDv0RBa z%5*SBN7+Vm4_B1+wryoMq}2BTxV7xlJK^@PHmN?P{gSnKDm2-()%7uE(7c7*tJ^C0@vkv6Fn)3)4?_b9yx$)HHGWd)MWFV72E9qkSM>11!pI72% z_4SV%1#}XBz2Ms93QYSyDe1yO`_E5bi!$u>=e@cbgO@cm162f%WT8%u_i9xd&y+i+ zR~E2zV&%Tl_)lzPz)7r&QjE=8l4stAm()PRps&W#p2Sho6A2g#tU3l?V${(-5)K7hEx#`Mg1xh%wW0t96ZyuOlEQt#IkR=!H(5@qdSV zH>-1B7Y^I>lUoi&o|Dqs%kA;+hHj^LWWVTBcOT`s); zdtj`Nai{ey2Pgc(*1q?8q4EaCMvkRYfBhw%<7RK-qH)kwNlCP{DhSB;>ud0Sx8ggF zDm^Wk-zT8ML!HGyZM6u{?|io$glLGBCV~h{}xr% z?{#Yh!3+_mSRndU(a}gLti-R!*8$_E5X14Yd|pcD30f145+?cIC)DkCe;?fpXUcE8 zbwh#=nruUk2mdSk4a@a#NQ!4Ci&50O71Q5<`M9YE1h@^LKkb{nMa84PV`5^wArH=U zwPU6T-3`=Jql9+l(0r?;#){w_A0Xcz1{zf z-&#CK1joO2Maw%&gOM62f^53>)7H)SV}IgQe_~cYMv+jM!$^YH(MtX8Fg7qxAv^DIlI~Z(#wB-{yG`v8G6#nZ_P(? zNCdHZq8%>%mH5PNYfAwxxEH_WOGSTtqYm_soy_Oo0(-b_C$WCO6Pvq_bR^n`EB{hV z68iXV6pycWQjFrZWYR$xXTwznk){?P=mi4@F@t9MNP=g(Y zFqSQJ;5fC@`)}FIZ!9&gjQ7wlk7JrqE_C8OKbu83ZC3mqwi#iw#D(QdCyq$sJ%a{FE;d%2Rvg5aGQ^ALole)h?)jXU!0=Z2} z7AeYH3Y#eJ$rnk4jZ-OPihP@Ds5cov!7n51Gc8*u{;>Wv=Ov^>=!a^2rNwBSS34#8 zN^8~~9R8Mn;PU2eES*sFEOyo_6rQHd6nlD?UNY@9`^?5O+sEq1L3eXirU4wns}d$# zxM*>p;5fzlsE@uo`BFVKk_DRR8vS8{xWsC1-Y&2`X0O?1`8Zp56NHu+F^7)HmR_=b zifg~e4;m5chX)$S8tbqTx)nAuG_X<9d3`ZW&cq%BoHa1h^t3HguU z(31q%0!_b9@0!&v7am}&V~`|4aD9L)MWTUB;MLoIOBzIS&{O(`y%W-;$GT3&pMW<* z8G`#y%w^&q+cPTqs)~xkg%*auP(7e3p%d;vo$~o-pv-9Mm3>dm<*K)VaWCY5_Jyhz zyb-(CwjE)WCW_%e2CL6#>J*{>doMmU7K2|#Lydt}M)ThEJ6rpk>~^L#6z!%W*1ym(DiMA) z)I`oN783ziSwg{u~ZN!}PEo*QxKag*WFS88=9GzAfpxpFuwJ^W5t@X^DFYBu8ruL^F^-=GEzGj|?@4sFt3ajI9~!-f zt!^15O@D+2fzB$`T>t&e*XFSBjx%dEpn5pjz`h#C0EE;xe^VNk$pe-jDXx6O7M1a)!?#%A}YwzKP|EC2Y zR=0sdiG&fy#uofH6r@!}~!{Xtuh%wUZr}Qjy=IT>cQ5Yvt#4RM(?})fC?6ulP ztx+zjvCbl96*=a41F2039QOf7*5~qT*tR>jy5t(UtCU0upA!!A5>$>pDOH}``lm?9 zZI1kMc>9_lQcmS-ui>;sN3)25ch@46N85OopESlCa0Wd>bXrc|f#QEIUO%+*RsN+L z@2OZpS?7x?YLa|d%*JTfk&?)qM!q1G!a`(6%#!$cqe-7xTJE4_x46{|e*f!!l(4=j zNDu*%09D&!iHcPGOb>0QI3th}$9$YHzkI-g%tS+wogI2}BK~E&j!h4s^Q-Os*PGOO zO`tiMzl#`!27-2JPJP-|C6wSyH?#*Btr8Pu$VhCHaK%kUHUR;q?dg{gD-42LrBM$3r0B|MHSN zJ-Di27&3$9c*BY&-!>trk`VUc?G*+(7#*ANv}XmKExtfupm;D$;9H)m%~!4BiqC7+ z=$NnWZhVti<3T;31$g?NJPRujQUfgC(7wYD)x=ZAJ4KiU)B>CDZ4L_w`OPERMuG}S z0vP&~D|z~m-jAEJ64%Gy^Oy-IAw~h40>yZbX|CDMZE$B?fSa1V6UD^$vlkX}dkf#> zIxYfpSd#03F@8)-x_nk^02gs>tB%KrKTtYVDruq-mxUqAJT1K#5xwKu5-8n@mZ83fmP?^zsY^(kHE z(L6gwrV(CdQp2k*>@)y@z+2oLXR=YFYyN`TddP9wd?obv$;mWuTApFnReYu1c4b8c zZfHHF)33$JKp&2!W$}5Ph%Mc4*p9<}3YU^r1o92j`;ePJ;2#=%;pD=CY`wfbC{a)j za`*Y0HBmB*+Z_f|6859=L{DP?;v*%*U+C&7ZHnMX1hcWw zYG2*@G^@fYC7qSwaLNu%a>O}*P6M+iy zKL5YG9~=#iZ#(+V5OO}jfZgtJ3A=Y;H+zv05QVz#Xgf@Z!9PBcuW*2oQQ@-|M5vj^ z!eBMr))ej>g2OKA@= zhRD5@ctujwwQq}9a4T;AF`pK|F4}H_7UvTmiYGxW?)Za0yqxTbSW;$uRSkJ>BqvUk znnJC*n5C6rJBa+EZ^$>!mvaLMwZSKfy-4A^QSRektAF_%T$eUKPi7(V1Uw(9VMHT5 z62y(GAuj`2ZQ%hYk!U=g5+Va|df8Ec1F!>#a|PU(7fQ~jvUWbz7rSHGoi-TC*oV>9 zImu+^f$+Rt!1yx^7kX^&yLwApi~ZM9;TO^UVgoEM15x%^$F5$;p(?zFc1yk-2!o6t zg{Us-Bs#28exm5ft+2CxRM0w9F=F*oZ0?uO=HtfQ3~2`%n6>{NNmU1y>#_)Fz&`xu z`UAI!{rv1bmUikRhev)G8K`FcXMEhRn+~6_g%~SUWdz=IglV55e?D-02wVWbdM$GWET{WVEs`OQ zw1Fu3AKlIc!6;o|wEd=?uUggqDn~*pCHmI8hAW0BySlD;)GBT8Nv%wYsWX+QSx{eZ zuP<(0^mkHrPd+=t_ByT!*t0#{U2jmg4%%0{*xH0h;DRRBLq zk4a)s1PZ#CbEjN7Zm;I^AvoWr&06e1??+LIz>{zsV0{J6Czcl>S)icCnfC0U>+ZBn zCHgAQZaOp(;1y?%{(K+hk+D)bAG`7sYBQg57ZIW86LclpAB8SwK!7S&Tsw{jnk0Jg zWlQk_;%(>EW7U=!tG|aTsytgC@g@UGB3(d0j4Y(%QW?O4&W^~fUwJk_zx8<^iDPzKsBKOH`e|U* zzP+(CGgeq{bG*lFC0S2qW;#wBs7MSP4ieGVMO)Nh&eVenZN11A{!3+1jw)tyP?4*Z zLSD>?o7Yn$W4$mBDFV*#Fu7KDd9dSeyAqHoOme4r!VB;Sm-a8j=MideP7Fy{s`?F6 zi-$54BT=orzR#mj2TNP??Ky6e5Zruh603G(Eb}EVNnRlFK*dR7YUZq(wHqHu_^`Ne z79V?GN0H!tzz;y3g|@i|G~h_!`!;1SWdV$snrte0a8DF4XnNyjta1-{rxD><&KbfY zKDRj#C3qe8{F^m1OT)TWTxWJSw1V6zam~Y`;1``h#uUzkr z)khBcA6^xf0gIISAaeewy1aCyNce+)b{XW2uN_LYP`fH^ehR&8#pBL{B3Shf2Q~51 zUCbaTNV~&8!_CHd?#pJMaRCtP=YSp4a#Y98RwkJQcVoXKSA+fN$;Y$O*w`cX!|jYd z3ns!zY^+}y!&zYnyY#iG(^l=MyTo0j@Wwfj_!IcM?l%a@%A&i5nj{Rjcq1{53{|jX z=tyAEa9OGS-s{b>5fSBrKr^pQ^tRS;EGaY6bprj%mqXQg$#6H>sOmOr_3;Tu`tx+{ z=v{MDXtPgWONhygR_X_j;oMeG$vs%OWxmz{{l#s~&#juUC(qj2Lbta@Zzh^BpXtWG zR#<2&R}Rl%_*Xkwwks)r`l8O&w5Xo^WS9-DQIyLlVe;bXUN}fnfs|cILBiYVpz|I1 zb7EXD=)f;WewkW-N09=%cCj$pS38&bikQs&c@BM@6q1MAUI58)LA%R343R>$C50-tI!709=sK#m+PQ%zbOD_P#N3Uhur8%Iu~Lf&Xs zvH?ro5%~akC%uw$1U1<^sG-G}vR^)Cr}*6yIByYh+Z_lPPu{1dDL)$^)>3$S+Ev0HOFpCgX8^vhBkiey72KibF1sU*Fv0MYz?}`O@4uCR7^mN&ax~9_bultY z$cSDfa;^&Emezmw7v9jOpp>7>&xveAm^&q7ghKGHWcp(cW{_G@64s?!(L3qsauN~@ zVIYXp7pUir1~BF$M1{i5`bw>#omjC#nL{)3~kOZu*?ng7i-l!p}MdIKkGL zEOWtagqDI=7B^-}5fSPd8tT9~oTZ9bHZ@y?h|SH-h)o5$`mN17DXPRKZ0DJRcg_Bl zDb%>N4*tCs7TPza82OQ=pYsSv^S~?%1wW940Kl0qlBE_(6{LO*WGgH&I{)4zrRG$6 zj}66PUsmv*ST8nnseFFEcn`-VZ0+Vq^Flsh1dDx7$5~Zs2Wy~1%h6ZU8?)FiSi)DZ zAmT^|6aFXgW^Qq}h^J3Cn~PTiC!j_j>k5rzV=CGT+@70Y+@!c{KlaOtuXsPpZ+1ES zP75q`#CO?HVa(SYnrBo0TS++jh9DkKV5Gh#_j73k0;aQTUa{ZcqwL#kT(w>Hg2E_P z^k18%7t@&3+WY+08}XatK^J6G%;%nF`y*|ZAj#!z6bTXuMP)8{@)2)SMx3i_ps&7d zc*8q4jemPNl6yBM^QDhK>|agaY3<2!{VNGEw#|E!srVkE&-y1ShUg&&hO zX29`xB7$jcZC}B^{_wfo4M1@jz6(!cu~Y$>?y*KI*f(zHS(|FqJDI6@d}5UI+#pGW zu00m~cD${~f#EzZ4SnaXFv(?G0ZRY-=iNi9deZzWmEc=ms_(&MTKrWXg6>2JvF+X= zZRKICA}OK4PH#>8bi}+Bjyl%D{3v2I2fD<9l!I*k)cfAPjTV^>*lUxnl+!le-MoiB z9pGabR3V(&K1_&`O0}QKXrdVm#HEiS^+3rOQYH!6H0mDv2OD3Vup`mHj%(16L47n| zze!-gjcr2?aws-f!8Z=aj-z&HUQivn9*`)SWzqJD8fl8&b$_93tIsp9JApAYJ^bOc zmHoIuj)KlI|MX#~2BW&g)^)cKNTdHLO&{@{WqXIc#1B0fp(2@r&BVUBk@ zrJspYd1=czEs*84>KPvbPufxn!P{A3VH(m53({(ernLyMP&y9n0X@7eQRqkw!C2iRiSILJ;z@+(+56bhBr&wAm-{4fm4g;96Hu$)qXUmeio=1pDW!io9iNAsEGz$}3rCz7EIr)W|1y8i%q7FO z?DsO@H2+I)C!>U~b?*BLR8vEc&Xnx0g#i z_UK4d_;So(PCMx9lKe%W{N)EcMAFtW+l4Y$FA`x}oMYbZ{6@dHSPl5kU0(7MZYR%v zslE){<9E3o->YZwEK(<+TjOZ|_(}G0R96T1W~JB?ZPV#hO?^>MoYwTkxw?S*vaK&! z+Un73r2uRyi)K`W*B%&M9cWXvv96(8kWg+=>VGdYT_F-q_2&tD|KTFN1wq6FAl?J`yl~60|0-bKy0i z?E6iT#__-hGLL^B#a?(5n;(jXNoMi3Cp{ zM4?KLqn2pHm?94)gR@>DCFiE5SPfI1g){=UWp3nOYMX=CLm#vj-VWUq7V28+9X5EF ztHSlbFh)e|Z=vl8;IHs~IH-dP-!^I4gk|CyyrE)?nobzwb39(pAzq z<-JiTr*^k5!zPyuLbS02#=h@)^^}}s)oQ~j09*;`Ydb@-IX+NyaZTZBCp_o_*RbTCLb4fj6pv7@^9eqb|0!3CgK2 ze(T}!L_)i?lsAQQZyJle3@{V9J9DlavKy%Ly6ZAQo8t`C+2*v(3`9!-AX-j^&`DgL z|0rwb;P7cU~n@hv3xqJy^b{kBM0{ke#VGvyc;TEuemPgcZ-Ik zO2SbTVeJgcF8Oe5^28?Jet%vGqe7KtWafeR1Uq{{f2}Q{58% z#w-Ro(bBXnixx~}Z|rh$S)zMKQ*kRjZq;kG=$VRlGF0j7+=~{uEF7oHh#n)OXsf3K z8|=jxD*huu6td@FX$Oeahc@WhQD%Uo9N0GFsBT13h(OXV4Qsl9_E?;%`w632TFW(l zC*;R#6THWI1X`27<|R^WYEq89(c0}poy_O{Y}Hun-P~k5{7N5j`8tE5<_5Sx>L?{; zJ>RyzxV^OD2A((|<{xL~qLWa(mes}nm{f~apcXT1_=S?t{*2ZI0%~KU=6Xa*bnwG5 zV{`r%8rZAO-x1|{z0q8E_dca_4ko>r3&;Ma^<4e5TogIP!sb=cio!3J_=du$e^|<($|i;D5gpiuW$drINm$X?;Sfs zmh2QiSOJ`LF_&TMwkimrgasxToR2WU?23}&VSck{d1Dp zJX#||+NI4gDN+*66W;yC3d z_)cKKcw(5h_gGQ&ig>d|HmA)@bA4ATR9=3^Vy&;K_A$OTET+LZDm~2{A5TuTAZdch z_GF{&fmjbr99o*sa@kQ&NsDanh@i3sNOwr<={F;i21A zNjUtzM^oZ(2_U}Blo&bSoM8e2<^AC2>#O~I?&RY;IlH41hl>TT^Is6A&W7?zsX9eO zd-F~S&gQq+t{2T~>%BquD%zg^75}GKshm3^`L1Q|8_|o#nf~jBg#CBvDyG>vH5+H)-!Ob%4#;IQfJr~eUFfG4B>w5v+?T;KWV3m~IUf`^3%c}TUYt18Bb=f{eC|M;};M!+L_BFwwU zpJ#5ECm0%2Rce_^?{OLpFL#Ue^8ejHc8ECpuN~!wPDtcDu~3 zINc&bk#&bE3k5yS0%Mvjh(W|b%H?*xDH;C-m z@#(0aWlL`1BzDj@adYkG)#Rfdb1SRUuK*u27`hM)hki|l(oj9?z7OqAU!p3_4xPV*3YrF>b?QfWj_pLB!hn&3GVo7npsIyUww6r)~8_9>fVE?a8 z(58jIXN|5MRlc((UyH%gp};979Z(%2g&u_fU;-;hFCALX5Xaf>2MQ9PhNVFaj?X4b3-Obc`|rVN&1w$8l-}0>OA7Z;RE{_`3>8*5D!HYPHgsL5c!2Im>+qy zltq7$f%72Gc&eDP%i4U%pkDgfEAKzJWzorKwlpH~VX06cY5(tG2v!Zhd0)Tx@m*J5 ziHz&aCss2bD*od~QBqhIVCGf|elN!%$XB$g9itk5lMm$@RH2tDi{*LlCw7}B(6Jc0 z5J_QEP<|-`8Kpc##Zrl0sEYVtFl#j)kC}+*N-1`uE2B9zSx>uQ5-)-*k3cA6^hTAp z(3*hq^G|N?bng4e;d75?>DXMP|4vbbm<&G3HBZr(JPd?t(kiGgh|57!+Z_E-c59k@ zS5+NF3h{^<7#?LK=BfetNjBY59{}&mxa*plh(Kr9g8Cvn?`UYeZoDr;rOXRUXM|`+ zE!{Fee3*Yi*q@xb=@a}krFsX7VLy9C=$O@>ENi~1S=SU4mh`BlTpLaxuQbXxT7 zL}2%iKv)cTdW|MA4aodENR&`M@j;{<8;}qB=#$fuQqg(KarS``YVIj9Z~?_`e)0-n zxE8ExV2XBtHVi|=wN{J$UfBM)oSm}JQJ4ZT?Vv0smdVO4g<{U zeFsW_Rw^f#8jZ1?=jmf@FYHpYCXSK{Ld9d}hUnIbP-C}pLhnJbeEUhwZI?1P>ZF{) z7->b^#k27BtlETX;GWVq1HkaaSKQRina>)Z&(;aS&tsoxZlnpI06fyx24$2O{GUWL zioIfE?zt1ovfe?sOgSGd$>xbrcDIks{zUIeB6(f(-c2B;iwZ|=B^e3v#KeixF8$qU z4!c!Q$ADP>Z#6P>YtH|gvQR0Csj}NcFFEb5e;b)Tii8x9WBgZdNq3A;CnB^q(!l)! zk?rYFd)GY$0K=6MmJ8n9?571xtTi!YQ}Jv=T-t3HCA!kE`B_(+$8IW*3t8-6%5QyQ z)IMyn2Rh{g{?4hXaf|;473aJ`+-y7N3{ukHsjiwy5BOTLC}^Q?75L$$SeROu$@>AGm6EmDe@OjRi>Aq>f)u>QN$ljM0_m?>w*1q?+BG+%_#%h61{tA7W z_))x<7{G-k#34h#@xVU|ZP6VtqCy|;Dlm=Ybp&Q#mWi2HYBE(~-TQ;e$g;E2w~r0; zSOHQs8&FsjtEl1Fd1=l4%r;o&uA?}_HP}vcGP}Q`f&mHj98W9BeUK8 zf7N^WJ+aS97g%|ON75&nkD=`3_`lHcQK>^I*N#+$KjSfzds=4J_@p=VHn|d%{{6GN z!JQH>I$Wl?KK4wP6He?3-0*(nsnF1mJ%#PZGL-?JkABuu3EYFNToZ{?QE^~aMAg_& zr00O diff --git a/public/images/items/pb_silver.png b/public/images/items/pb_silver.png new file mode 100644 index 0000000000000000000000000000000000000000..f60a8348a94de2e587a94a66e2d0b4336b25763d GIT binary patch literal 556 zcmV+{0@MA8P)Px$=1D|BR9J=Wmd{G#Kpe+E{iBGD(%MtqOM6a2)4_jWw)gr4>f6Ol_aguOTIA7OlIcy{p6R)B;d-G zt0BwN=Q{CT@tiIY09vK(U-z{yK0N*bAUPd!9M=he(2b)ZC{#NGbzcYK_E(v;WZZUa zZ$IYdT9*_E0`MU+&2c6NZ$Bo-03uVazH0gwgsQ5xA#7zp4{T1m-q-g*zp?@_?%r%_ zu1A}_ixw9WwzFV&54;Ky4Yi9IqFSjKZB$amH3g=GcpxCq4On!Csb0>foV~!fNOt%2uJLj)H z7V`c9cAihl`lomP@z86!>@@t)3%E1ZZ$Xq}(@q~tZb!%L{a6Pv74CM0a}_L8or}6S zR5KZdt6hxImEv~SXcV_9sEwLssV*q=9$Dv{RdLb(Fzpv}GoeqE?kk_+TiBkx-&UIb&JYavlC?UY0EQ#Z8z3`e9qoXIwH>x=pqd z%35Sq>qmzv^CTe%oUfV}4yqwAZUt~i>7)61sNM}8P;br=s$(ZrlZK(vjAsUWM38U+ zr&)QP->kiGTmg*mJkJS+I`+I}KRnc`C{xKEdPLC}N;Oxf^~q{$aPvAo1Q<7u@*{@)RdMQ0{*wIZpce-YC+`B66=Hm#rS9 z%i`PcuPXOyFj)A1^nUO+-SD3k=fyMMs?i6of6#jWc(z?l<@v@Jr5k>0zjZ1vyuJtA y#ewheyXsUbi|VYIH!R!g?3LP&(Df*KfBywl*SaLA^$+XOa?;Me&zBUz_3=2Cwf6C~&~@LKwZ?SJ zE(wE97~16d?B;4H%K^`)c55OY#&D;a_3RAJ4PYgU)7YFM01p}4=;Tsp4yDdsA_GOq zHj$VF9zG>>tstHXDJ3?7#^!Pg8o5$8Hjza&V;TW-x*eovM&aRY_k_*ADM*R)`H4*PKWf!0(48sQn?===)i~112hN$mX3T1 zNb3q!MRVcoU^0@=a@~8`qj7$9tK=(GH*ofDuGS$J!DH*KY9U?MJ4kNh9R#?q2=(Ox zNzfGNF1y`8%XW2_-5!@i-9byY%H4I)C89N=CYraJyuF3a)e5zv?x3~#Z`4wE(AL~V zXGd>`^r#Le$9#<-=Y5X`@BKC?AC;htZMXHmsR`xnlLY!?+^B&cr~V77S8({jvAJ@F d&VQ%0%pa{9Auo9qX&wLo002ovPDHLkV1n*=KDGb= literal 0 HcmV?d00001 diff --git a/public/locales b/public/locales index 3ccef8472dd..87615556d8a 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 3ccef8472dd7cc7c362538489954cb8fdad27e5f +Subproject commit 87615556d8a2bd7eef7abac818f84423a8a13b03 diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index c838f6b2c49..7d0d86fadbf 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -10,7 +10,7 @@ import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { TimeOfDay } from "#enums/time-of-day"; -import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier } from "#app/modifier/modifier"; +import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; import { SpeciesFormKey } from "#enums/species-form-key"; @@ -1652,11 +1652,11 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter + p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier - || m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG), + || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG), new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter + p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier - || m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG) + || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG) ] }; diff --git a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts index 8dc4eca25bf..1e20b73e351 100644 --- a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -1,4 +1,4 @@ -import { leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { generateModifierType, leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { modifierTypes } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; @@ -14,6 +14,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import i18next from "i18next"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/anOfferYouCantRefuse"; @@ -98,6 +99,8 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = } } + const shinyCharm = generateModifierType(scene, modifierTypes.SHINY_CHARM); + encounter.setDialogueToken("itemName", shinyCharm?.name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name")); encounter.setDialogueToken("liepardName", getPokemonSpecies(Species.LIEPARD).getName()); return true; @@ -123,7 +126,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = return true; }) .withOptionPhase(async (scene: BattleScene) => { - // Give the player a Shiny charm + // Give the player a Shiny Charm scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.SHINY_CHARM)); leaveEncounterWithoutBattle(scene, true); }) @@ -132,9 +135,11 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) - .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( - new MoveRequirement(EXTORTION_MOVES, true), - new AbilityRequirement(EXTORTION_ABILITIES, true)) + .withPrimaryPokemonRequirement( + CombinationPokemonRequirement.Some( + new MoveRequirement(EXTORTION_MOVES, true), + new AbilityRequirement(EXTORTION_ABILITIES, true) + ) ) .withDialogue({ buttonLabel: `${namespace}:option.2.label`, 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 d316ab14cde..e24eadb56c7 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -193,12 +193,14 @@ const WAVE_LEVEL_BREAKPOINTS = [ 30, 50, 70, 100, 120, 140, 160 ]; export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BUG_TYPE_SUPERFAN) .withEncounterTier(MysteryEncounterTier.GREAT) - .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( - // Must have at least 1 Bug type on team, OR have a bug item somewhere on the team - new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1), - new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1), - new TypeRequirement(Type.BUG, false, 1) - )) + .withPrimaryPokemonRequirement( + CombinationPokemonRequirement.Some( + // Must have at least 1 Bug type on team, OR have a bug item somewhere on the team + new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1), + new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1), + new TypeRequirement(Type.BUG, false, 1) + ) + ) .withMaxAllowedEncounters(1) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withIntroSpriteConfigs([]) // These are set in onInit() @@ -405,11 +407,13 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = .build()) .withOption(MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( - // Meets one or both of the below reqs - new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1), - new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1) - )) + .withPrimaryPokemonRequirement( + CombinationPokemonRequirement.Some( + // Meets one or both of the below reqs + new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1), + new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1) + ) + ) .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 57c8aa7a561..c4b03660bde 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -11,7 +11,7 @@ import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Abilities } from "#enums/abilities"; -import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { applyAbilityOverrideToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { Type } from "#app/data/type"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -425,17 +425,8 @@ function onYesAbilitySwap(scene: BattleScene, resolve) { const onPokemonSelected = (pokemon: PlayerPokemon) => { // Do ability swap const encounter = scene.currentBattle.mysteryEncounter!; - if (pokemon.isFusion()) { - if (!pokemon.fusionCustomPokemonData) { - pokemon.fusionCustomPokemonData = new CustomPokemonData(); - } - pokemon.fusionCustomPokemonData.ability = encounter.misc.ability; - } else { - if (!pokemon.customPokemonData) { - pokemon.customPokemonData = new CustomPokemonData(); - } - pokemon.customPokemonData.ability = encounter.misc.ability; - } + + applyAbilityOverrideToPokemon(pokemon, encounter.misc.ability); encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender()); scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true)); }; diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 2c13086ccb8..8a814b58248 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -14,6 +14,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Challenges } from "#enums/challenges"; /** i18n namespace for encounter */ const namespace = "mysteryEncounters/darkDeal"; @@ -141,6 +142,7 @@ export const DarkDealEncounter: MysteryEncounter = // Removes random pokemon (including fainted) from party and adds name to dialogue data tokens // Will never return last battle able mon and instead pick fainted/unable to battle const removedPokemon = getRandomPlayerPokemon(scene, true, false, true); + // Get all the pokemon's held items const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); scene.removePokemonFromPlayerParty(removedPokemon); @@ -160,7 +162,13 @@ export const DarkDealEncounter: MysteryEncounter = scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ROGUE_BALL)); // Start encounter with random legendary (7-10 starter strength) that has level additive - const bossTypes: Type[] = encounter.misc.removedTypes; + // If this is a mono-type challenge, always ensure the required type is filtered for + let bossTypes: Type[] = encounter.misc.removedTypes; + const singleTypeChallenges = scene.gameMode.challenges.filter(c => c.value && c.id === Challenges.SINGLE_TYPE); + if (scene.gameMode.isChallenge && singleTypeChallenges.length > 0) { + bossTypes = singleTypeChallenges.map(c => (c.value - 1) as Type); + } + const bossModifiers: PokemonHeldItemModifier[] = encounter.misc.modifiers; // Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+ const roll = randSeedInt(100); diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index 5686d0f6ce5..d5f9388b56c 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -45,10 +45,13 @@ export const DelibirdyEncounter: MysteryEncounter = .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneRequirement(new MoneyRequirement(0, DELIBIRDY_MONEY_PRICE_MULTIPLIER)) // Must have enough money for it to spawn at the very least - .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( // Must also have either option 2 or 3 available to spawn - new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), - new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true) - )) + .withPrimaryPokemonRequirement( + CombinationPokemonRequirement.Some( + // Must also have either option 2 or 3 available to spawn + new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), + new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true) + ) + ) .withIntroSpriteConfigs([ { spriteKey: "", @@ -196,7 +199,7 @@ export const DelibirdyEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const modifier: BerryModifier | HealingBoosterModifier = encounter.misc.chosenModifier; - // Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed + // Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed if (modifier instanceof BerryModifier) { // Check if the player has max stacks of that Candy Jar already const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier; @@ -211,8 +214,8 @@ export const DelibirdyEncounter: MysteryEncounter = scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR)); } } else { - // Check if the player has max stacks of that Healing Charm already - const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; + // Check if the player has max stacks of that Berry Pouch already + const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { // At max stacks, give the first party pokemon a Shell Bell instead @@ -221,7 +224,7 @@ export const DelibirdyEncounter: MysteryEncounter = scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); } } @@ -290,8 +293,8 @@ export const DelibirdyEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const modifier = encounter.misc.chosenModifier; - // Check if the player has max stacks of Berry Pouch already - const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; + // Check if the player has max stacks of Healing Charm already + const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { // At max stacks, give the first party pokemon a Shell Bell instead @@ -300,7 +303,7 @@ export const DelibirdyEncounter: MysteryEncounter = scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); } // Remove the modifier if its stacks go to 0 diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index d306206159a..5c16e5d8564 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -4,24 +4,30 @@ import { AttackTypeBoosterModifierType, modifierTypes, } from "#app/modifier/mod import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; -import { TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { AbilityRequirement, CombinationPokemonRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; 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 { BattlerIndex } from "#app/battle"; -import { PokemonMove } from "#app/field/pokemon"; +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 { isNullOrUndefined, randSeedInt } from "#app/utils"; import { StatusEffect } from "#app/data/status-effect"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { applyAbilityOverrideToPokemon, applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { EncounterAnim } from "#enums/encounter-anims"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { Stat } from "#enums/stat"; +import { Ability } from "#app/data/ability"; +import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/fieryFallout"; @@ -62,16 +68,24 @@ export const FieryFalloutEncounter: MysteryEncounter = { species: volcaronaSpecies, isBoss: false, - gender: Gender.MALE + gender: Gender.MALE, + tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF, Stat.SPD ], 1)); + } }, { species: volcaronaSpecies, isBoss: false, - gender: Gender.FEMALE + gender: Gender.FEMALE, + tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF, Stat.SPD ], 1)); + } } ], doubleBattle: true, - disableSwitch: true + disableSwitch: true, }; encounter.enemyPartyConfigs = [ config ]; @@ -139,7 +153,7 @@ export const FieryFalloutEncounter: MysteryEncounter = async (scene: BattleScene) => { // Pick battle const encounter = scene.currentBattle.mysteryEncounter!; - setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonCharcoal(scene)); + setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonAttackTypeBoostItem(scene)); encounter.startOfBattleEffects.push( { @@ -153,18 +167,6 @@ export const FieryFalloutEncounter: MysteryEncounter = targets: [ BattlerIndex.PLAYER_2 ], move: new PokemonMove(Moves.FIRE_SPIN), ignorePp: true - }, - { - sourceBattlerIndex: BattlerIndex.ENEMY, - targets: [ BattlerIndex.ENEMY ], - move: new PokemonMove(Moves.QUIVER_DANCE), - ignorePp: true - }, - { - sourceBattlerIndex: BattlerIndex.ENEMY_2, - targets: [ BattlerIndex.ENEMY_2 ], - move: new PokemonMove(Moves.QUIVER_DANCE), - ignorePp: true }); await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); } @@ -180,7 +182,7 @@ export const FieryFalloutEncounter: MysteryEncounter = ], }, async (scene: BattleScene) => { - // Damage non-fire types and burn 1 random non-fire type member + // 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)); @@ -198,7 +200,11 @@ export const FieryFalloutEncounter: MysteryEncounter = if (chosenPokemon.trySetStatus(StatusEffect.BURN)) { // Burn applied encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender()); + encounter.setDialogueToken("abilityName", new Ability(Abilities.HEATPROOF, 3).name); queueEncounterMessage(scene, `${namespace}:option.2.target_burned`); + + // Also permanently change the burned Pokemon's ability to Heatproof + applyAbilityOverrideToPokemon(chosenPokemon, Abilities.HEATPROOF); } } @@ -209,8 +215,12 @@ export const FieryFalloutEncounter: MysteryEncounter = .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) - .withPrimaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3PrimaryName dialogue token automatically - .withSecondaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3SecondaryName dialogue token automatically + .withPrimaryPokemonRequirement( + CombinationPokemonRequirement.Some( + new TypeRequirement(Type.FIRE, true, 1), + new AbilityRequirement(FIRE_RESISTANT_ABILITIES, true) + ) + ) // Will set option3PrimaryName dialogue token automatically .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, @@ -233,26 +243,32 @@ export const FieryFalloutEncounter: MysteryEncounter = { fillRemaining: true }, undefined, () => { - giveLeadPokemonCharcoal(scene); + giveLeadPokemonAttackTypeBoostItem(scene); }); const primary = encounter.options[2].primaryPokemon!; - const secondary = encounter.options[2].secondaryPokemon![0]; - setEncounterExp(scene, [ primary.id, secondary.id ], getPokemonSpecies(Species.VOLCARONA).baseExp * 2); + setEncounterExp(scene, [ primary.id ], getPokemonSpecies(Species.VOLCARONA).baseExp * 2); leaveEncounterWithoutBattle(scene); }) .build() ) .build(); -function giveLeadPokemonCharcoal(scene: BattleScene) { - // Give first party pokemon Charcoal for free at end of battle +function giveLeadPokemonAttackTypeBoostItem(scene: BattleScene) { + // Give first party pokemon attack type boost item for free at end of battle const leadPokemon = scene.getParty()?.[0]; if (leadPokemon) { - const charcoal = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.FIRE ]) as AttackTypeBoosterModifierType; - applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal); - scene.currentBattle.mysteryEncounter!.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); - queueEncounterMessage(scene, `${namespace}:found_charcoal`); + // Generate type booster held item, default to Charcoal if item fails to generate + let boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType; + if (!boosterModifierType) { + boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.FIRE ]) as AttackTypeBoosterModifierType; + } + applyModifierTypeToPlayerPokemon(scene, leadPokemon, boosterModifierType); + + const encounter = scene.currentBattle.mysteryEncounter!; + encounter.setDialogueToken("itemName", boosterModifierType.name); + encounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); + queueEncounterMessage(scene, `${namespace}:found_item`); } } diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts index f282064bb94..7fdd29d36a2 100644 --- a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -56,7 +56,13 @@ export const MysteriousChallengersEncounter: MysteryEncounter = // Hard difficulty trainer is another random trainer, but with AVERAGE_BALANCED config // Number of mons is based off wave: 1-20 is 2, 20-40 is 3, etc. capping at 6 after wave 100 - const hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); + let retries = 0; + let hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); + while (retries < 5 && hardTrainerType === normalTrainerType) { + // Will try to use a different trainer from the normal trainer type + hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); + retries++; + } const hardTemplate = new TrainerPartyCompoundTemplate( new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true), new TrainerPartyTemplate( 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 d30c97b27de..8dd730492b1 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -21,7 +21,7 @@ import i18next from "i18next"; const namespace = "mysteryEncounters/shadyVitaminDealer"; const VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER = 1.5; -const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 3.5; +const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 5; /** * Shady Vitamin Dealer encounter. 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 9c10d33d019..610209f8aad 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 @@ -222,7 +222,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = encounter.misc.chosenPokemon = pokemon1; encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender()); const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs); - setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene)); + setEncounterRewards(scene, + { guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true }, + eggOptions, + () => doPostEncounterCleanup(scene)); // Remove all Pokemon from the party except the chosen Pokemon removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1); @@ -271,7 +274,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = encounter.misc.chosenPokemon = pokemon2; encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender()); const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs); - setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene)); + setEncounterRewards(scene, + { guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true }, + eggOptions, + () => doPostEncounterCleanup(scene)); // Remove all Pokemon from the party except the chosen Pokemon removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2); @@ -320,7 +326,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = encounter.misc.chosenPokemon = pokemon3; encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender()); const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs); - setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene)); + setEncounterRewards(scene, + { guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true }, + eggOptions, + () => doPostEncounterCleanup(scene)); // Remove all Pokemon from the party except the chosen Pokemon removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3); @@ -454,12 +463,16 @@ function calculateEggRewardsForPokemon(pokemon: PlayerPokemon): [number, number] } // Maximum of 30 points - const totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30); + let totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30); - // 1 Rare egg for every 6 points - const numRares = Math.floor(totalPoints / 6); + // First 5 points go to Common eggs + let numCommons = Math.min(totalPoints, 5); + totalPoints -= numCommons; + + // Then, 1 Rare egg for every 4 points + const numRares = Math.floor(totalPoints / 4); // 1 Common egg for every point leftover - const numCommons = totalPoints % 6; + numCommons += totalPoints % 4; return [ numCommons, numRares ]; } diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 9f80bbbffde..03341a713f2 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -37,6 +37,7 @@ export const TrainingSessionEncounter: MysteryEncounter = .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party .withFleeAllowed(false) .withHideWildIntroMessage(true) + .withPreventGameStatsUpdates(true) // Do not count the Pokemon as seen or defeated since it is ours .withIntroSpriteConfigs([ { spriteKey: "training_session_gear", 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 2b3b38b2164..d3c16ce2122 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -71,7 +71,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ] }; const config: EnemyPartyConfig = { - levelAdditiveModifier: 1, + levelAdditiveModifier: 0.5, pokemonConfigs: [ pokemonConfig ], disableSwitch: true }; diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index b97a22dbe51..2ecba6ce658 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -4,23 +4,30 @@ 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 { leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils"; +import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { IntegerHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { achvs } from "#app/system/achv"; import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; import { getLevelTotalExp } from "#app/data/exp"; import { Stat } from "#enums/stat"; -import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { Challenges } from "#enums/challenges"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { PlayerGender } from "#enums/player-gender"; +import { TrainerType } from "#enums/trainer-type"; +import PokemonData from "#app/system/pokemon-data"; +import { Nature } from "#enums/nature"; +import HeldModifierConfig from "#app/interfaces/held-modifier-config"; +import { trainerConfigs, TrainerPartyTemplate } from "#app/data/trainer-config"; +import { PartyMemberStrength } from "#enums/party-member-strength"; /** i18n namespace for encounter */ const namespace = "mysteryEncounters/weirdDream"; @@ -80,10 +87,11 @@ const EXCLUDED_TRANSFORMATION_SPECIES = [ const SUPER_LEGENDARY_BST_THRESHOLD = 600; const NON_LEGENDARY_BST_THRESHOLD = 570; -const GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD = 450; + +const OLD_GATEAU_STATS_UP = 20; /** 0-100 */ -const PERCENT_LEVEL_LOSS_ON_REFUSE = 12.5; +const PERCENT_LEVEL_LOSS_ON_REFUSE = 10; /** * Value ranges of the resulting species BST transformations after adding values to original species @@ -105,7 +113,8 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM) .withEncounterTier(MysteryEncounterTier.ROGUE) .withDisallowedChallenges(Challenges.SINGLE_TYPE, Challenges.SINGLE_GENERATION) - .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + // TODO: should reset minimum wave to 10 when there are more Rogue tiers in pool. Matching Dark Deal minimum for now. + .withSceneWaveRangeRequirement(30, 140) .withIntroSpriteConfigs([ { spriteKey: "weird_dream_woman", @@ -131,6 +140,15 @@ export const WeirdDreamEncounter: MysteryEncounter = .withQuery(`${namespace}:query`) .withOnInit((scene: BattleScene) => { scene.loadBgm("mystery_encounter_weird_dream", "mystery_encounter_weird_dream.mp3"); + + // Calculate all the newly transformed Pokemon and begin asset load + const teamTransformations = getTeamTransformations(scene); + const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets()); + scene.currentBattle.mysteryEncounter!.misc = { + teamTransformations, + loadAssets + }; + return true; }) .withOnVisualsStart((scene: BattleScene) => { @@ -156,13 +174,10 @@ export const WeirdDreamEncounter: MysteryEncounter = doShowDreamBackground(scene); }); - // Calculate all the newly transformed Pokemon and begin asset load - const teamTransformations = getTeamTransformations(scene); - const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets()); - scene.currentBattle.mysteryEncounter!.misc = { - teamTransformations, - loadAssets - }; + for (const transformation of scene.currentBattle.mysteryEncounter!.misc.teamTransformations) { + scene.removePokemonFromPlayerParty(transformation.previousPokemon, false); + scene.getParty().push(transformation.newPokemon); + } }) .withOptionPhase(async (scene: BattleScene) => { // Starts cutscene dialogue, but does not await so that cutscene plays as player goes through dialogue @@ -193,7 +208,7 @@ export const WeirdDreamEncounter: MysteryEncounter = await showEncounterText(scene, `${namespace}:option.1.dream_complete`); await doNewTeamPostProcess(scene, transformations); - setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT ]}); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT, modifierTypes.MINT ], fillRemaining: false }); leaveEncounterWithoutBattle(scene, true); }) .build() @@ -209,7 +224,88 @@ export const WeirdDreamEncounter: MysteryEncounter = ], }, async (scene: BattleScene) => { - // Reduce party levels by 20% + // Battle your "future" team for some item rewards + const transformations: PokemonTransformation[] = scene.currentBattle.mysteryEncounter!.misc.teamTransformations; + + // Uses the pokemon that player's party would have transformed into + const enemyPokemonConfigs: EnemyPokemonConfig[] = []; + for (const transformation of transformations) { + const newPokemon = transformation.newPokemon; + const previousPokemon = transformation.previousPokemon; + + await postProcessTransformedPokemon(scene, previousPokemon, newPokemon, newPokemon.species.getRootSpeciesId(), true); + + const dataSource = new PokemonData(newPokemon); + dataSource.player = false; + + // Copy held items to new pokemon + const newPokemonHeldItemConfigs: HeldModifierConfig[] = []; + for (const item of transformation.heldItems) { + newPokemonHeldItemConfigs.push({ + modifier: item.clone() as PokemonHeldItemModifier, + stackCount: item.getStackCount(), + isTransferable: false + }); + } + // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats + if (shouldGetOldGateau(newPokemon)) { + const stats = getOldGateauBoostedStats(newPokemon); + newPokemonHeldItemConfigs.push({ + modifier: generateModifierType(scene, modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU, [ OLD_GATEAU_STATS_UP, stats ]) as PokemonHeldItemModifierType, + stackCount: 1, + isTransferable: false + }); + } + + const enemyConfig: EnemyPokemonConfig = { + species: transformation.newSpecies, + isBoss: newPokemon.getSpeciesForm().getBaseStatTotal() > NON_LEGENDARY_BST_THRESHOLD, + level: previousPokemon.level, + dataSource: dataSource, + modifierConfigs: newPokemonHeldItemConfigs + }; + + enemyPokemonConfigs.push(enemyConfig); + } + + const genderIndex = scene.gameData.gender ?? PlayerGender.UNSET; + const trainerConfig = trainerConfigs[genderIndex === PlayerGender.FEMALE ? TrainerType.FUTURE_SELF_F : TrainerType.FUTURE_SELF_M].clone(); + trainerConfig.setPartyTemplates(new TrainerPartyTemplate(transformations.length, PartyMemberStrength.STRONG)); + const enemyPartyConfig: EnemyPartyConfig = { + trainerConfig: trainerConfig, + pokemonConfigs: enemyPokemonConfigs, + female: genderIndex === PlayerGender.FEMALE + }; + + 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); + if (passiveDisabledPokemon?.length > 0) { + const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; + enablePassiveMon.passive = true; + enablePassiveMon.updateInfo(true); + } + }; + + setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT ], fillRemaining: false }, undefined, onBeforeRewards); + + await showEncounterText(scene, `${namespace}:option.2.selected_2`, null, undefined, true); + await initBattleWithEnemyConfig(scene, enemyPartyConfig); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}:option.3.label`, + buttonTooltip: `${namespace}:option.3.tooltip`, + selected: [ + { + text: `${namespace}:option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave, reduce party levels by 10% for (const pokemon of scene.getParty()) { pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1); pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate); @@ -235,7 +331,7 @@ interface PokemonTransformation { function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { const party = scene.getParty(); // Removes all pokemon from the party - const alreadyUsedSpecies: PokemonSpecies[] = []; + const alreadyUsedSpecies: PokemonSpecies[] = party.map(p => p.species); const pokemonTransformations: PokemonTransformation[] = party.map(p => { return { previousPokemon: p @@ -250,11 +346,11 @@ function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { // First, roll 2 of the party members to new Pokemon at a +90 to +110 BST difference // Then, roll the remainder of the party members at a +40 to +50 BST difference const numPokemon = party.length; + const removedPokemon = randSeedShuffle(party.slice(0)); for (let i = 0; i < numPokemon; i++) { - const removed = party[randSeedInt(party.length)]; + const removed = removedPokemon[i]; const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id); pokemonTransformations[index].heldItems = removed.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); - scene.removePokemonFromPlayerParty(removed, false); const bst = removed.calculateBaseStats().reduce((a, b) => a + b, 0); let newBstRange: [number, number]; @@ -276,14 +372,13 @@ function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { pokemonTransformations[index].newSpecies = newSpecies; + console.log("New species: " + JSON.stringify(newSpecies)); alreadyUsedSpecies.push(newSpecies); } for (const transformation of pokemonTransformations) { const newAbilityIndex = randSeedInt(transformation.newSpecies.getAbilityCount()); - const newPlayerPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined); - transformation.newPokemon = newPlayerPokemon; - scene.getParty().push(newPlayerPokemon); + transformation.newPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined); } return pokemonTransformations; @@ -296,109 +391,20 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon const newPokemon = transformation.newPokemon; const speciesRootForm = newPokemon.species.getRootSpeciesId(); - // Roll HA a second time - if (newPokemon.species.abilityHidden) { - const hiddenIndex = newPokemon.species.ability2 ? 2 : 1; - if (newPokemon.abilityIndex < hiddenIndex) { - const hiddenAbilityChance = new IntegerHolder(256); - scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); - - const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); - - if (hasHiddenAbility) { - newPokemon.abilityIndex = hiddenIndex; - } - } + if (await postProcessTransformedPokemon(scene, previousPokemon, newPokemon, speciesRootForm)) { + atLeastOneNewStarter = true; } - // Roll IVs a second time - newPokemon.ivs = newPokemon.ivs.map(iv => { - const newValue = randSeedInt(31); - return newValue > iv ? newValue : iv; - }); - - // For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it - if (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny()) { - if (newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1) { - scene.validateAchv(achvs.HIDDEN_ABILITY); - } - - if (newPokemon.species.subLegendary) { - scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); - } - - if (newPokemon.species.legendary) { - scene.validateAchv(achvs.CATCH_LEGENDARY); - } - - if (newPokemon.species.mythical) { - scene.validateAchv(achvs.CATCH_MYTHICAL); - } - - scene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs); - const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false); - if (newStarterUnlocked) { - atLeastOneNewStarter = true; - await showEncounterText(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() })); - } - } - - // If the previous pokemon had pokerus, transfer to new pokemon - newPokemon.pokerus = previousPokemon.pokerus; - - // Transfer previous Pokemon's luck value - newPokemon.luck = previousPokemon.getLuck(); - - // If the previous pokemon had higher IVs, override to those (after updating dex IVs > prevents perfect 31s on a new unlock) - newPokemon.ivs = newPokemon.ivs.map((iv, index) => { - return previousPokemon.ivs[index] > iv ? previousPokemon.ivs[index] : iv; - }); - - // For pokemon that the player owns (including ones just caught), gain a candy - if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) { - scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1); - } - - // Set the moveset of the new pokemon to be the same as previous, but with 1 egg move and 1 (attempted) STAB move of the new species - newPokemon.generateAndPopulateMoveset(); - // Store a copy of a "standard" generated moveset for the new pokemon, will be used later for finding a favored move - const newPokemonGeneratedMoveset = newPokemon.moveset; - - newPokemon.moveset = previousPokemon.moveset; - - const newEggMoveIndex = await addEggMoveToNewPokemonMoveset(scene, newPokemon, speciesRootForm); - - // Try to add a favored STAB move (might fail if Pokemon already knows a bunch of moves from newPokemonGeneratedMoveset) - addFavoredMoveToNewPokemonMoveset(newPokemon, newPokemonGeneratedMoveset, newEggMoveIndex); - - // Randomize the second type of the pokemon - // If the pokemon does not normally have a second type, it will gain 1 - const newTypes = [ newPokemon.getTypes()[0] ]; - let newType = randSeedInt(18) as Type; - while (newType === newTypes[0]) { - newType = randSeedInt(18) as Type; - } - newTypes.push(newType); - if (!newPokemon.customPokemonData) { - newPokemon.customPokemonData = new CustomPokemonData(); - } - newPokemon.customPokemonData.types = newTypes; - + // Copy old items to new pokemon for (const item of transformation.heldItems) { item.pokemonId = newPokemon.id; await scene.addModifier(item, false, false, false, true); } - - // Any pokemon that is at or below 450 BST gets +20 permanent BST to 3 stats: HP (halved, +10), lowest of Atk/SpAtk, and lowest of Def/SpDef - if (newPokemon.getSpeciesForm().getBaseStatTotal() <= GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD) { - const stats: Stat[] = [ Stat.HP ]; - const baseStats = newPokemon.getSpeciesForm().baseStats.slice(0); - // Attack or SpAtk - stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK); - // Def or SpDef - stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF); + // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats + if (shouldGetOldGateau(newPokemon)) { + const stats = getOldGateauBoostedStats(newPokemon); const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU() - .generateType(scene.getParty(), [ 20, stats ]) + .generateType(scene.getParty(), [ OLD_GATEAU_STATS_UP, stats ]) ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU); const modifier = modType?.newModifier(newPokemon); if (modifier) { @@ -406,9 +412,6 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon } } - // Enable passive if previous had it - newPokemon.passive = previousPokemon.passive; - newPokemon.calculateStats(); await newPokemon.updateInfo(); } @@ -427,6 +430,138 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon } } +/** + * Applies special changes to the newly transformed pokemon, such as passing previous moves, gaining egg moves, etc. + * Returns whether the transformed pokemon unlocks a new starter for the player. + * @param scene + * @param previousPokemon + * @param newPokemon + * @param speciesRootForm + * @param forBattle Default `false`. If false, will perform achievements and dex unlocks for the player. + */ +async function postProcessTransformedPokemon(scene: BattleScene, previousPokemon: PlayerPokemon, newPokemon: PlayerPokemon, speciesRootForm: Species, forBattle: boolean = false): Promise { + let isNewStarter = false; + // Roll HA a second time + if (newPokemon.species.abilityHidden) { + const hiddenIndex = newPokemon.species.ability2 ? 2 : 1; + if (newPokemon.abilityIndex < hiddenIndex) { + const hiddenAbilityChance = new IntegerHolder(256); + scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + + const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + + if (hasHiddenAbility) { + newPokemon.abilityIndex = hiddenIndex; + } + } + } + + // Roll IVs a second time + newPokemon.ivs = newPokemon.ivs.map(iv => { + const newValue = randSeedInt(31); + return newValue > iv ? newValue : iv; + }); + + // Roll a neutral nature + newPokemon.nature = [ Nature.HARDY, Nature.DOCILE, Nature.BASHFUL, Nature.QUIRKY, Nature.SERIOUS ][randSeedInt(5)]; + + // For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it + if (!forBattle && (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny())) { + if (newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1) { + scene.validateAchv(achvs.HIDDEN_ABILITY); + } + + if (newPokemon.species.subLegendary) { + scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); + } + + if (newPokemon.species.legendary) { + scene.validateAchv(achvs.CATCH_LEGENDARY); + } + + if (newPokemon.species.mythical) { + scene.validateAchv(achvs.CATCH_MYTHICAL); + } + + scene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs); + const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false); + if (newStarterUnlocked) { + isNewStarter = true; + await showEncounterText(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() })); + } + } + + // If the previous pokemon had pokerus, transfer to new pokemon + newPokemon.pokerus = previousPokemon.pokerus; + + // Transfer previous Pokemon's luck value + newPokemon.luck = previousPokemon.getLuck(); + + // If the previous pokemon had higher IVs, override to those (after updating dex IVs > prevents perfect 31s on a new unlock) + newPokemon.ivs = newPokemon.ivs.map((iv, index) => { + return previousPokemon.ivs[index] > iv ? previousPokemon.ivs[index] : iv; + }); + + // For pokemon that the player owns (including ones just caught), gain a candy + if (!forBattle && !!scene.gameData.dexData[speciesRootForm].caughtAttr) { + scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1); + } + + // Set the moveset of the new pokemon to be the same as previous, but with 1 egg move and 1 (attempted) STAB move of the new species + newPokemon.generateAndPopulateMoveset(); + // Store a copy of a "standard" generated moveset for the new pokemon, will be used later for finding a favored move + const newPokemonGeneratedMoveset = newPokemon.moveset; + + newPokemon.moveset = previousPokemon.moveset.slice(0); + + const newEggMoveIndex = await addEggMoveToNewPokemonMoveset(scene, newPokemon, speciesRootForm, forBattle); + + // Try to add a favored STAB move (might fail if Pokemon already knows a bunch of moves from newPokemonGeneratedMoveset) + addFavoredMoveToNewPokemonMoveset(newPokemon, newPokemonGeneratedMoveset, newEggMoveIndex); + + // Randomize the second type of the pokemon + // If the pokemon does not normally have a second type, it will gain 1 + const newTypes = [ newPokemon.getTypes()[0] ]; + let newType = randSeedInt(18) as Type; + while (newType === newTypes[0]) { + newType = randSeedInt(18) as Type; + } + newTypes.push(newType); + if (!newPokemon.customPokemonData) { + newPokemon.customPokemonData = new CustomPokemonData(); + } + newPokemon.customPokemonData.types = newTypes; + + // Enable passive if previous had it + newPokemon.passive = previousPokemon.passive; + + return isNewStarter; +} + +/** + * @returns `true` if a given Pokemon has valid BST to be given an Old Gateau + */ +function shouldGetOldGateau(pokemon: Pokemon): boolean { + return pokemon.getSpeciesForm().getBaseStatTotal() < NON_LEGENDARY_BST_THRESHOLD; +} + +/** + * Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef + * @returns Array of 3 {@linkcode Stat}s to boost + */ +function getOldGateauBoostedStats(pokemon: Pokemon): Stat[] { + const stats: Stat[] = []; + const baseStats = pokemon.getSpeciesForm().baseStats.slice(0); + // HP or Speed + stats.push(baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD); + // Attack or SpAtk + stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK); + // Def or SpDef + stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF); + return stats; +} + + function getTransformedSpecies(originalBst: number, bstSearchRange: [number, number], hasPokemonBstHigherThan600: boolean, hasPokemonBstBetween570And600: boolean, alreadyUsedSpecies: PokemonSpecies[]): PokemonSpecies { let newSpecies: PokemonSpecies | undefined; while (isNullOrUndefined(newSpecies)) { @@ -550,7 +685,7 @@ function doSideBySideTransformations(scene: BattleScene, transformations: Pokemo * @param newPokemon * @param speciesRootForm */ -async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: PlayerPokemon, speciesRootForm: Species): Promise { +async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: PlayerPokemon, speciesRootForm: Species, forBattle: boolean = false): Promise { let eggMoveIndex: null | number = null; const eggMoves = newPokemon.getEggMoves()?.slice(0); if (eggMoves) { @@ -576,7 +711,7 @@ async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: Pla } // For pokemon that the player owns (including ones just caught), unlock the egg move - if (!isNullOrUndefined(randomEggMoveIndex) && !!scene.gameData.dexData[speciesRootForm].caughtAttr) { + if (!forBattle && !isNullOrUndefined(randomEggMoveIndex) && !!scene.gameData.dexData[speciesRootForm].caughtAttr) { await scene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true); } } diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index a57cedc8fa3..91ea0c5be19 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -37,31 +37,58 @@ export abstract class EncounterSceneRequirement implements EncounterRequirement abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; } +/** + * Combination of multiple {@linkcode EncounterSceneRequirement | EncounterSceneRequirements} (OR/AND possible. See {@linkcode isAnd}) + */ export class CombinationSceneRequirement extends EncounterSceneRequirement { - orRequirements: EncounterSceneRequirement[]; + /** If `true`, all requirements must be met (AND). If `false`, any requirement must be met (OR) */ + private isAnd: boolean; + requirements: EncounterSceneRequirement[]; - constructor(... orRequirements: EncounterSceneRequirement[]) { + public static Some(...requirements: EncounterSceneRequirement[]): CombinationSceneRequirement { + return new CombinationSceneRequirement(false, ...requirements); + } + + public static Every(...requirements: EncounterSceneRequirement[]): CombinationSceneRequirement { + return new CombinationSceneRequirement(true, ...requirements); + } + + private constructor(isAnd: boolean, ...requirements: EncounterSceneRequirement[]) { super(); - this.orRequirements = orRequirements; + this.isAnd = isAnd; + this.requirements = requirements; } + /** + * Checks if all/any requirements are met (depends on {@linkcode isAnd}) + * @param scene The {@linkcode BattleScene} to check against + * @returns true if all/any requirements are met (depends on {@linkcode isAnd}) + */ override meetsRequirement(scene: BattleScene): boolean { - for (const req of this.orRequirements) { - if (req.meetsRequirement(scene)) { - return true; - } - } - return false; + return this.isAnd + ? this.requirements.every(req => req.meetsRequirement(scene)) + : this.requirements.some(req => req.meetsRequirement(scene)); } + /** + * Retrieves a dialogue token key/value pair for the given {@linkcode EncounterSceneRequirement | requirements}. + * @param scene The {@linkcode BattleScene} to check against + * @param pokemon The {@linkcode PlayerPokemon} to check against + * @returns A dialogue token key/value pair + * @throws An {@linkcode Error} if {@linkcode isAnd} is `true` (not supported) + */ override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - for (const req of this.orRequirements) { - if (req.meetsRequirement(scene)) { - return req.getDialogueToken(scene, pokemon); + if (this.isAnd) { + throw new Error("Not implemented (Sorry)"); + } else { + for (const req of this.requirements) { + if (req.meetsRequirement(scene)) { + return req.getDialogueToken(scene, pokemon); + } } - } - return this.orRequirements[0].getDialogueToken(scene, pokemon); + return this.requirements[0].getDialogueToken(scene, pokemon); + } } } @@ -90,44 +117,74 @@ export abstract class EncounterPokemonRequirement implements EncounterRequiremen abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; } +/** + * Combination of multiple {@linkcode EncounterPokemonRequirement | EncounterPokemonRequirements} (OR/AND possible. See {@linkcode isAnd}) + */ export class CombinationPokemonRequirement extends EncounterPokemonRequirement { - orRequirements: EncounterPokemonRequirement[]; + /** If `true`, all requirements must be met (AND). If `false`, any requirement must be met (OR) */ + private isAnd: boolean; + private requirements: EncounterPokemonRequirement[]; - constructor(...orRequirements: EncounterPokemonRequirement[]) { + public static Some(...requirements: EncounterPokemonRequirement[]): CombinationPokemonRequirement { + return new CombinationPokemonRequirement(false, ...requirements); + } + + public static Every(...requirements: EncounterPokemonRequirement[]): CombinationPokemonRequirement { + return new CombinationPokemonRequirement(true, ...requirements); + } + + private constructor(isAnd: boolean, ...requirements: EncounterPokemonRequirement[]) { super(); + this.isAnd = isAnd; this.invertQuery = false; this.minNumberOfPokemon = 1; - this.orRequirements = orRequirements; + this.requirements = requirements; } + /** + * Checks if all/any requirements are met (depends on {@linkcode isAnd}) + * @param scene The {@linkcode BattleScene} to check against + * @returns true if all/any requirements are met (depends on {@linkcode isAnd}) + */ override meetsRequirement(scene: BattleScene): boolean { - for (const req of this.orRequirements) { - if (req.meetsRequirement(scene)) { - return true; - } - } - return false; + return this.isAnd + ? this.requirements.every(req => req.meetsRequirement(scene)) + : this.requirements.some(req => req.meetsRequirement(scene)); } + /** + * Queries the players party for all party members that are compatible with all/any requirements (depends on {@linkcode isAnd}) + * @param partyPokemon The party of {@linkcode PlayerPokemon} + * @returns All party members that are compatible with all/any requirements (depends on {@linkcode isAnd}) + */ override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { - for (const req of this.orRequirements) { - const result = req.queryParty(partyPokemon); - if (result?.length > 0) { - return result; - } + if (this.isAnd) { + return this.requirements.reduce((relevantPokemon, req) => req.queryParty(relevantPokemon), partyPokemon); + } else { + const matchingRequirement = this.requirements.find(req => req.queryParty(partyPokemon).length > 0); + return matchingRequirement ? matchingRequirement.queryParty(partyPokemon) : []; } - - return []; } + /** + * Retrieves a dialogue token key/value pair for the given {@linkcode EncounterPokemonRequirement | requirements}. + * @param scene The {@linkcode BattleScene} to check against + * @param pokemon The {@linkcode PlayerPokemon} to check against + * @returns A dialogue token key/value pair + * @throws An {@linkcode Error} if {@linkcode isAnd} is `true` (not supported) + */ override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - for (const req of this.orRequirements) { - if (req.meetsRequirement(scene)) { - return req.getDialogueToken(scene, pokemon); + if (this.isAnd) { + throw new Error("Not implemented (Sorry)"); + } else { + for (const req of this.requirements) { + if (req.meetsRequirement(scene)) { + return req.getDialogueToken(scene, pokemon); + } } - } - return this.orRequirements[0].getDialogueToken(scene, pokemon); + return this.requirements[0].getDialogueToken(scene, pokemon); + } } } diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index ee9eb159e10..c045ee51bd7 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -53,6 +53,7 @@ export interface IMysteryEncounter { hasBattleAnimationsWithoutTargets: boolean; skipEnemyBattleTurns: boolean; skipToFightInput: boolean; + preventGameStatsUpdates: boolean; onInit?: (scene: BattleScene) => boolean; onVisualsStart?: (scene: BattleScene) => boolean; @@ -150,6 +151,10 @@ export default class MysteryEncounter implements IMysteryEncounter { * If true, will skip COMMAND input and go straight to FIGHT (move select) input menu */ skipToFightInput: boolean; + /** + * If true, will prevent updating {@linkcode GameStats} for encountering and/or defeating Pokemon + */ + preventGameStatsUpdates: boolean; // #region Event callback functions @@ -548,6 +553,7 @@ export class MysteryEncounterBuilder implements Partial { hasBattleAnimationsWithoutTargets: boolean = false; skipEnemyBattleTurns: boolean = false; skipToFightInput: boolean = false; + preventGameStatsUpdates: boolean = false; maxAllowedEncounters: number = 3; expMultiplier: number = 1; @@ -735,6 +741,14 @@ export class MysteryEncounterBuilder implements Partial { return Object.assign(this, { skipToFightInput }); } + /** + * If true, will prevent updating {@linkcode GameStats} for encountering and/or defeating Pokemon + * Default `false` + */ + withPreventGameStatsUpdates(preventGameStatsUpdates: boolean): this & Required> { + return Object.assign(this, { preventGameStatsUpdates }); + } + /** * Sets the maximum number of times that an encounter can spawn in a given Classic run * @param maxAllowedEncounters diff --git a/src/data/mystery-encounters/requirements/requirement-groups.ts b/src/data/mystery-encounters/requirements/requirement-groups.ts index 63c899fc5e9..76bbb8f03a7 100644 --- a/src/data/mystery-encounters/requirements/requirement-groups.ts +++ b/src/data/mystery-encounters/requirements/requirement-groups.ts @@ -118,3 +118,20 @@ export const EXTORTION_ABILITIES = [ Abilities.SUCTION_CUPS, Abilities.STICKY_HOLD ]; + +/** + * Abilities that signify resistance to fire + */ +export const FIRE_RESISTANT_ABILITIES = [ + Abilities.FLAME_BODY, + Abilities.FLASH_FIRE, + Abilities.WELL_BAKED_BODY, + Abilities.HEATPROOF, + Abilities.THERMAL_EXCHANGE, + Abilities.THICK_FAT, + Abilities.WATER_BUBBLE, + Abilities.MAGMA_ARMOR, + Abilities.WATER_VEIL, + Abilities.STEAM_ENGINE, + Abilities.PRIMORDIAL_SEA +]; diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 5fa8af95f4d..b1adc478ab0 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -21,6 +21,8 @@ import { Gender } from "#app/data/gender"; import { PermanentStat } from "#enums/stat"; 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"; /** Will give +1 level every 10 waves */ export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; @@ -833,3 +835,21 @@ export function isPokemonValidForEncounterOptionSelection(pokemon: Pokemon, scen return null; } + +/** + * Permanently overrides the ability (not passive) of a pokemon. + * If the pokemon is a fusion, instead overrides the fused pokemon's ability. + */ +export function applyAbilityOverrideToPokemon(pokemon: Pokemon, ability: Abilities) { + if (pokemon.isFusion()) { + if (!pokemon.fusionCustomPokemonData) { + pokemon.fusionCustomPokemonData = new CustomPokemonData(); + } + pokemon.fusionCustomPokemonData.ability = ability; + } else { + if (!pokemon.customPokemonData) { + pokemon.customPokemonData = new CustomPokemonData(); + } + pokemon.customPokemonData.ability = ability; + } +} diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index fcc13975270..bc69b611075 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -2500,6 +2500,22 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER) .setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)), [TrainerType.EXPERT_POKEMON_BREEDER]: new TrainerConfig(++t).setMoneyMultiplier(3).setEncounterBgm(TrainerType.ACE_TRAINER).setLocalizedName("Expert Pokemon Breeder") - .setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.STRONG)) + .setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE)), + [TrainerType.FUTURE_SELF_M]: new TrainerConfig(++t) + .setMoneyMultiplier(0) + .setEncounterBgm("mystery_encounter_weird_dream") + .setBattleBgm("mystery_encounter_weird_dream") + .setMixedBattleBgm("mystery_encounter_weird_dream") + .setVictoryBgm("mystery_encounter_weird_dream") + .setLocalizedName("Future Self M") + .setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG)), + [TrainerType.FUTURE_SELF_F]: new TrainerConfig(++t) + .setMoneyMultiplier(0) + .setEncounterBgm("mystery_encounter_weird_dream") + .setBattleBgm("mystery_encounter_weird_dream") + .setMixedBattleBgm("mystery_encounter_weird_dream") + .setVictoryBgm("mystery_encounter_weird_dream") + .setLocalizedName("Future Self F") + .setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG)) }; diff --git a/src/enums/trainer-type.ts b/src/enums/trainer-type.ts index cb7509067b5..708faf69196 100644 --- a/src/enums/trainer-type.ts +++ b/src/enums/trainer-type.ts @@ -116,6 +116,8 @@ export enum TrainerType { VITO, BUG_TYPE_SUPERFAN, EXPERT_POKEMON_BREEDER, + FUTURE_SELF_M, + FUTURE_SELF_F, BROCK = 200, MISTY, diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 6c4ae3b7ff9..d41c1f9eefa 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -93,7 +93,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public stats: integer[]; public ivs: integer[]; public nature: Nature; - public natureOverride: Nature | -1; public moveset: (PokemonMove | null)[]; public status: Status | null; public friendship: integer; @@ -4283,7 +4282,6 @@ export class PlayerPokemon extends Pokemon { if (newEvolution.condition?.predicate(this)) { const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, undefined, this.shiny, this.variant, this.ivs, this.nature); - newPokemon.natureOverride = this.natureOverride; newPokemon.passive = this.passive; newPokemon.moveset = this.moveset.slice(); newPokemon.moveset = this.copyMoveset(); diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 8e7853a41bb..3e475c62590 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -10,7 +10,9 @@ import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect import { Type } from "#app/data/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 } 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"; @@ -1561,6 +1563,7 @@ export const modifierTypes = { VOUCHER_PREMIUM: () => new AddVoucherModifierType(VoucherType.PREMIUM, 1), GOLDEN_POKEBALL: () => new ModifierType("modifierType:ModifierType.GOLDEN_POKEBALL", "pb_gold", (type, _args) => new ExtraModifierModifier(type), undefined, "se/pb_bounce_1"), + SILVER_POKEBALL: () => new ModifierType("modifierType:ModifierType.SILVER_POKEBALL", "pb_silver", (type, _args) => new TempExtraModifierModifier(type, 100), undefined, "se/pb_bounce_1"), ENEMY_DAMAGE_BOOSTER: () => new ModifierType("modifierType:ModifierType.ENEMY_DAMAGE_BOOSTER", "wl_item_drop", (type, _args) => new EnemyDamageBoosterModifier(type, 5)), ENEMY_DAMAGE_REDUCTION: () => new ModifierType("modifierType:ModifierType.ENEMY_DAMAGE_REDUCTION", "wl_guard_spec", (type, _args) => new EnemyDamageReducerModifier(type, 2.5)), @@ -1577,13 +1580,13 @@ export const modifierTypes = { if (pregenArgs) { return new PokemonBaseStatTotalModifierType(pregenArgs[0] as number); } - return new PokemonBaseStatTotalModifierType(randSeedInt(20)); + return new PokemonBaseStatTotalModifierType(randSeedInt(20, 1)); }), MYSTERY_ENCOUNTER_OLD_GATEAU: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs) { return new PokemonBaseStatFlatModifierType(pregenArgs[0] as number, pregenArgs[1] as Stat[]); } - return new PokemonBaseStatFlatModifierType(randSeedInt(20), [ Stat.HP, Stat.ATK, Stat.DEF ]); + return new PokemonBaseStatFlatModifierType(randSeedInt(20, 1), [ Stat.HP, Stat.ATK, Stat.DEF ]); }), MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs) { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index b699c2483c9..11f16f103a5 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -404,6 +404,14 @@ export abstract class LapsingPersistentModifier extends PersistentModifier { this.battleCount = this.maxBattles; } + /** + * Updates an existing modifier with a new `maxBattles` and `battleCount`. + */ + setNewBattleCount(count: number): void { + this.maxBattles = count; + this.battleCount = count; + } + getMaxBattles(): number { return this.maxBattles; } @@ -960,7 +968,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier { this.stackCount = pokemon ? pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length - + pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length + + pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length : this.stackCount; const text = scene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11); @@ -975,7 +983,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier { getMaxHeldItemCount(pokemon: Pokemon): number { this.stackCount = pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length - + pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length; + + pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length; return 999; } } @@ -3288,6 +3296,60 @@ export class ExtraModifierModifier extends PersistentModifier { } } +/** + * Modifier used for timed boosts to the player's shop item rewards. + * @extends LapsingPersistentModifier + * @see {@linkcode apply} + */ +export class TempExtraModifierModifier extends LapsingPersistentModifier { + constructor(type: ModifierType, maxBattles: number, battleCount?: number, stackCount?: number) { + super(type, maxBattles, battleCount, stackCount); + } + + /** + * Goes through existing modifiers for any that match Silver Pokeball, + * which will then add the max count of the new item to the existing count of the current item. + * If no existing Silver Pokeballs are found, will add a new one. + * @param modifiers {@linkcode PersistentModifier} array of the player's modifiers + * @param _virtual N/A + * @param scene + * @returns true if the modifier was successfully added or applied, false otherwise + */ + add(modifiers: PersistentModifier[], _virtual: boolean, scene: BattleScene): boolean { + for (const modifier of modifiers) { + if (this.match(modifier)) { + const modifierInstance = modifier as TempExtraModifierModifier; + const newBattleCount = this.getMaxBattles() + modifierInstance.getBattleCount(); + + modifierInstance.setNewBattleCount(newBattleCount); + scene.playSound("se/restore"); + return true; + } + } + + modifiers.push(this); + return true; + } + + clone() { + return new TempExtraModifierModifier(this.type, this.getMaxBattles(), this.getBattleCount(), this.stackCount); + } + + match(modifier: Modifier): boolean { + return (modifier instanceof TempExtraModifierModifier); + } + + /** + * Increases the current rewards in the battle by the `stackCount`. + * @returns `true` if the shop reward number modifier applies successfully + * @param count {@linkcode NumberHolder} that holds the resulting shop item reward count + */ + apply(count: NumberHolder): boolean { + count.value += this.getStackCount(); + return true; + } +} + export abstract class EnemyPersistentModifier extends PersistentModifier { constructor(type: ModifierType, stackCount?: number) { super(type, stackCount); diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 38d5cfb4a10..e5a60692bb4 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -1,7 +1,7 @@ import BattleScene from "#app/battle-scene"; import { ModifierTier } from "#app/modifier/modifier-tier"; import { regenerateModifierPoolThresholds, ModifierTypeOption, ModifierType, getPlayerShopModifierTypeOptionsForWave, PokemonModifierType, FusePokemonModifierType, PokemonMoveModifierType, TmModifierType, RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, ModifierPoolType, getPlayerModifierTypeOptions } from "#app/modifier/modifier-type"; -import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; import { Mode } from "#app/ui/ui"; @@ -45,6 +45,7 @@ export class SelectModifierPhase extends BattlePhase { const modifierCount = new Utils.IntegerHolder(3); if (this.isPlayer()) { this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); + this.scene.applyModifiers(TempExtraModifierModifier, true, modifierCount); } // If custom modifiers are specified, overrides default item count @@ -274,7 +275,13 @@ export class SelectModifierPhase extends BattlePhase { // Otherwise, continue with custom multiplier multiplier = this.customModifierSettings.rerollMultiplier; } - return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER); + + const baseMultiplier = Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * (2 ** this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER); + + // Apply Black Sludge to reroll cost + const modifiedRerollCost = new NumberHolder(baseMultiplier); + this.scene.applyModifier(HealShopCostModifier, true, modifiedRerollCost); + return modifiedRerollCost.value; } getPoolType(): ModifierPoolType { diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index e900ff97fc6..1faa31655df 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -25,12 +25,17 @@ export class VictoryPhase extends PokemonPhase { start() { super.start(); - this.scene.gameData.gameStats.pokemonDefeated++; + const isMysteryEncounter = this.scene.currentBattle.isBattleMysteryEncounter(); + + // update Pokemon defeated count except for MEs that disable it + if (!isMysteryEncounter || !this.scene.currentBattle.mysteryEncounter?.preventGameStatsUpdates) { + this.scene.gameData.gameStats.pokemonDefeated++; + } const expValue = this.getPokemon().getExpValue(); this.scene.applyPartyExp(expValue, true); - if (this.scene.currentBattle.isBattleMysteryEncounter()) { + if (isMysteryEncounter) { handleMysteryEncounterVictory(this.scene, false, this.isExpOnly); return this.end(); } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 5f9aad63408..c00159a7fd7 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1569,6 +1569,10 @@ export class GameData { } setPokemonSeen(pokemon: Pokemon, incrementCount: boolean = true, trainer: boolean = false): void { + // Some Mystery Encounters block updates to these stats + if (this.scene.currentBattle?.isBattleMysteryEncounter() && this.scene.currentBattle.mysteryEncounter?.preventGameStatsUpdates) { + return; + } const dexEntry = this.dexData[pokemon.species.speciesId]; dexEntry.seenAttr |= pokemon.getDexAttr(); if (incrementCount) { diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index e681c995b26..421739a9da1 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -92,7 +92,6 @@ export default class PokemonData { this.stats = source.stats; this.ivs = source.ivs; this.nature = source.nature !== undefined ? source.nature : 0 as Nature; - this.natureOverride = source.natureOverride !== undefined ? source.natureOverride : -1; this.friendship = source.friendship !== undefined ? source.friendship : getPokemonSpecies(this.species).baseFriendship; this.metLevel = source.metLevel || 5; this.metBiome = source.metBiome !== undefined ? source.metBiome : -1; @@ -117,6 +116,8 @@ export default class PokemonData { this.customPokemonData = new CustomPokemonData(source.customPokemonData); + // Deprecated, but needed for session data migration + this.natureOverride = source.natureOverride; this.mysteryEncounterPokemonData = new CustomPokemonData(source.mysteryEncounterPokemonData); this.fusionMysteryEncounterPokemonData = new CustomPokemonData(source.fusionMysteryEncounterPokemonData); diff --git a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index 69c0a114645..66d628ef82f 100644 --- a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -206,7 +206,7 @@ describe("Delibird-y - Mystery Encounter", () => { expect(candyJarAfter?.stackCount).toBe(1); }); - it("Should remove Reviver Seed and give the player a Healing Charm", async () => { + it("Should remove Reviver Seed and give the player a Berry Pouch", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 1 Reviver Seed on party lead @@ -220,11 +220,11 @@ describe("Delibird-y - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); - const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); + const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); expect(reviverSeedAfter).toBeUndefined(); - expect(healingCharmAfter).toBeDefined(); - expect(healingCharmAfter?.stackCount).toBe(1); + expect(berryPouchAfter).toBeDefined(); + expect(berryPouchAfter?.stackCount).toBe(1); }); it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => { @@ -256,13 +256,13 @@ describe("Delibird-y - Mystery Encounter", () => { expect(shellBellAfter?.stackCount).toBe(1); }); - it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => { + it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); - // 5 Healing Charms + // 3 Berry Pouches scene.modifiers = []; - const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier; - healingCharm.stackCount = 5; + const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier; + healingCharm.stackCount = 3; await scene.addModifier(healingCharm, true, false, false, true); // Set 1 Reviver Seed on party lead @@ -275,12 +275,12 @@ describe("Delibird-y - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); - const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); expect(reviverSeedAfter).toBeUndefined(); expect(healingCharmAfter).toBeDefined(); - expect(healingCharmAfter?.stackCount).toBe(5); + expect(healingCharmAfter?.stackCount).toBe(3); expect(shellBellAfter).toBeDefined(); expect(shellBellAfter?.stackCount).toBe(1); }); @@ -347,7 +347,7 @@ describe("Delibird-y - Mystery Encounter", () => { }); }); - it("Should decrease held item stacks and give the player a Berry Pouch", async () => { + it("Should decrease held item stacks and give the player a Healing Charm", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 2 Soul Dew on party lead @@ -361,14 +361,14 @@ describe("Delibird-y - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); - const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); expect(soulDewAfter?.stackCount).toBe(1); - expect(berryPouchAfter).toBeDefined(); - expect(berryPouchAfter?.stackCount).toBe(1); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter?.stackCount).toBe(1); }); - it("Should remove held item and give the player a Berry Pouch", async () => { + it("Should remove held item and give the player a Healing Charm", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 1 Soul Dew on party lead @@ -382,20 +382,20 @@ describe("Delibird-y - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); - const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); expect(soulDewAfter).toBeUndefined(); - expect(berryPouchAfter).toBeDefined(); - expect(berryPouchAfter?.stackCount).toBe(1); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter?.stackCount).toBe(1); }); - it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { + it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // 5 Healing Charms scene.modifiers = []; - const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier; - healingCharm.stackCount = 3; + const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier; + healingCharm.stackCount = 5; await scene.addModifier(healingCharm, true, false, false, true); // Set 1 Soul Dew on party lead @@ -408,12 +408,12 @@ describe("Delibird-y - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); - const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); expect(soulDewAfter).toBeUndefined(); - expect(berryPouchAfter).toBeDefined(); - expect(berryPouchAfter?.stackCount).toBe(3); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter?.stackCount).toBe(5); expect(shellBellAfter).toBeDefined(); expect(shellBellAfter?.stackCount).toBe(1); }); 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 a4f303d121f..5a270f1cbec 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -12,7 +12,7 @@ import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encount import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; import { Moves } from "#enums/moves"; import BattleScene from "#app/battle-scene"; -import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { AttackTypeBoosterModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { Type } from "#app/data/type"; import { Status, StatusEffect } from "#app/data/status-effect"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; @@ -22,6 +22,8 @@ import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { CommandPhase } from "#app/phases/command-phase"; import { MovePhase } from "#app/phases/move-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Abilities } from "#enums/abilities"; import i18next from "i18next"; const namespace = "mysteryEncounters/fieryFallout"; @@ -42,10 +44,11 @@ describe("Fiery Fallout - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override.mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves() + .moveset([ Moves.PAYBACK, Moves.THUNDERBOLT ]); // Required for attack type booster item generation vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ @@ -109,12 +112,16 @@ describe("Fiery Fallout - Mystery Encounter", () => { { species: getPokemonSpecies(Species.VOLCARONA), isBoss: false, - gender: Gender.MALE + gender: Gender.MALE, + tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ], + mysteryEncounterBattleEffects: expect.any(Function) }, { species: getPokemonSpecies(Species.VOLCARONA), isBoss: false, - gender: Gender.FEMALE + gender: Gender.FEMALE, + tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ], + mysteryEncounterBattleEffects: expect.any(Function) } ], doubleBattle: true, @@ -157,12 +164,11 @@ describe("Fiery Fallout - Mystery Encounter", () => { expect(enemyField[0].gender).not.toEqual(enemyField[1].gender); // Should be opposite gender const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); - expect(movePhases.length).toBe(4); + expect(movePhases.length).toBe(2); expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.FIRE_SPIN).length).toBe(2); // Fire spin used twice before battle - expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.QUIVER_DANCE).length).toBe(2); // Quiver Dance used twice before battle }); - it("should give charcoal to lead pokemon", async () => { + it("should give attack type boosting item to lead pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); @@ -172,8 +178,8 @@ describe("Fiery Fallout - Mystery Encounter", () => { const leadPokemonId = scene.getParty()?.[0].id; const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; - const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal"); - expect(charcoal).toBeDefined; + const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier); + expect(item).toBeDefined; }); }); @@ -193,7 +199,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { }); }); - it("should damage all non-fire party PKM by 20% and randomly burn 1", async () => { + 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(); @@ -210,7 +216,8 @@ describe("Fiery Fallout - Mystery Encounter", () => { burnablePokemon.forEach((pkm) => { expect(pkm.hp, `${pkm.name} should have received 20% damage: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp() - Math.floor(pkm.getMaxHp() * 0.2)); }); - expect(burnablePokemon.some(pkm => pkm?.status?.effect === StatusEffect.BURN)).toBeTruthy(); + expect(burnablePokemon.some(pkm => pkm.status?.effect === StatusEffect.BURN)).toBeTruthy(); + expect(burnablePokemon.some(pkm => pkm.customPokemonData.ability === Abilities.HEATPROOF)); notBurnablePokemon.forEach((pkm) => expect(pkm.hp, `${pkm.name} should be full hp: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp())); }); @@ -241,17 +248,15 @@ describe("Fiery Fallout - Mystery Encounter", () => { }); }); - it("should give charcoal to lead pokemon", async () => { + it("should give attack type boosting item to lead pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); await runMysteryEncounterToEnd(game, 3); await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - const leadPokemonId = scene.getParty()?.[0].id; - const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; - const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal"); - expect(charcoal).toBeDefined; + const leadPokemonItems = scene.getParty()?.[0].getHeldItems() as PokemonHeldItemModifier[]; + const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier); + expect(item).toBeDefined; }); it("should leave encounter without battle", async () => { @@ -264,7 +269,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { }); it("should be disabled if not enough FIRE types are in party", async () => { - await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, [ Species.MAGIKARP, Species.ARCANINE ]); + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, [ Species.MAGIKARP ]); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.getCurrentPhase(); diff --git a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts index d761f2d1b21..8286c6a694b 100644 --- a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -86,7 +86,7 @@ describe("Trash to Treasure - Mystery Encounter", () => { expect(TrashToTreasureEncounter.enemyPartyConfigs).toEqual([ { - levelAdditiveModifier: 1, + levelAdditiveModifier: 0.5, disableSwitch: true, pokemonConfigs: [ { 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 0d463655a52..c1fa6d83a18 100644 --- a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -5,7 +5,7 @@ import { Species } from "#app/enums/species"; import GameManager from "#app/test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; import BattleScene from "#app/battle-scene"; import { Mode } from "#app/ui/ui"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; @@ -15,6 +15,8 @@ import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/weird-dream-encounter"; import * as EncounterTransformationSequence from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { CommandPhase } from "#app/phases/command-phase"; +import { ModifierTier } from "#app/modifier/modifier-tier"; const namespace = "mysteryEncounters/weirdDream"; const defaultParty = [ Species.MAGBY, Species.HAUNTER, Species.ABRA ]; @@ -70,7 +72,7 @@ describe("Weird Dream - Mystery Encounter", () => { expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}:description`); expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}:query`); - expect(WeirdDreamEncounter.options.length).toBe(2); + expect(WeirdDreamEncounter.options.length).toBe(3); }); it("should initialize fully", async () => { @@ -132,7 +134,7 @@ describe("Weird Dream - Mystery Encounter", () => { expect(plus40To50.length).toBe(1); }); - it("should have 1 Memory Mushroom, 5 Rogue Balls, and 2 Mints in rewards", async () => { + it("should have 1 Memory Mushroom, 5 Rogue Balls, and 3 Mints in rewards", async () => { await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); await runMysteryEncounterToEnd(game, 1); await game.phaseInterceptor.to(SelectModifierPhase, false); @@ -141,11 +143,12 @@ describe("Weird Dream - Mystery Encounter", () => { expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(4); + expect(modifierSelectHandler.options.length).toEqual(5); expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("ROGUE_BALL"); expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("MINT"); expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT"); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT"); }); it("should leave encounter without battle", async () => { @@ -158,7 +161,7 @@ describe("Weird Dream - Mystery Encounter", () => { }); }); - describe("Option 2 - Leave", () => { + describe("Option 2 - Battle Future Self", () => { it("should have the correct properties", () => { const option = WeirdDreamEncounter.options[1]; expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); @@ -174,17 +177,63 @@ describe("Weird Dream - Mystery Encounter", () => { }); }); - it("should reduce party levels by 12.5%", async () => { + it("should start a battle against the player's transformation team", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + + 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); + }); + + it("should have 2 Rogue/2 Ultra/2 Great items in rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(6); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[4].modifierTypeOption.type.tier - modifierSelectHandler.options[4].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT); + expect(modifierSelectHandler.options[5].modifierTypeOption.type.tier - modifierSelectHandler.options[5].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT); + }); + }); + + describe("Option 3 - Leave", () => { + it("should have the correct properties", () => { + const option = WeirdDreamEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option.3.label`, + buttonTooltip: `${namespace}:option.3.tooltip`, + selected: [ + { + text: `${namespace}:option.3.selected`, + }, + ], + }); + }); + + it("should reduce party levels by 10%", async () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); const levelsPrior = scene.getParty().map(p => p.level); - await runMysteryEncounterToEnd(game, 2); + await runMysteryEncounterToEnd(game, 3); const levelsAfter = scene.getParty().map(p => p.level); for (let i = 0; i < levelsPrior.length; i++) { - expect(Math.max(Math.ceil(0.8875 * levelsPrior[i]), 1)).toBe(levelsAfter[i]); + expect(Math.max(Math.ceil(0.9 * levelsPrior[i]), 1)).toBe(levelsAfter[i]); expect(scene.getParty()[i].levelExp).toBe(0); } @@ -195,7 +244,7 @@ describe("Weird Dream - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); - await runMysteryEncounterToEnd(game, 2); + await runMysteryEncounterToEnd(game, 3); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); From a2419c4fc3d8556cc359edba4d7eb4761e7877a3 Mon Sep 17 00:00:00 2001 From: Opaque02 <66582645+Opaque02@users.noreply.github.com> Date: Thu, 24 Oct 2024 06:00:07 +1000 Subject: [PATCH 16/17] [Misc] Add admin for (un)linking 3rd party accounts (#4198) * Updated admin panel to allow the concept of unlinking accounts * Don't look too hard at this commit, nothing to see here * Admin stuff * Fixed linking and unlinking and updated menu options * Undid some changes and cleaned up some code * Updated some logic and added some comments * Updates to admin panel logic * Stupid promises everyone hates them and they deserver to die * Promise stuff still * Promises working thanks to Ydarissep on discord - pushing with debug code before it decides to stop working again * Removed debugging code * All discord functionality seems to be working here?? Not sure what happened but yay * Fixed up some bugs and code * Added registered date to the panel * Fixed and updated some minor error message related stuff * Minor changes * Fixed some minor bugs, made the save related errors have error codes, and added updated icons * Updated search field error * Missed a couple of things to push * Fixed linting and doc errors * Revert dev related code and clean up dev comments * Reverting utils * Updating front end to match back end from Pancakes' comments * make getFields and getInputFieldConfigs a single function of FormUiHandler * remove outdated doc * Apply suggestions from code review Moka review changes Co-authored-by: MokaStitcher <54149968+MokaStitcher@users.noreply.github.com> * Added docs * eslint fixes * Fixed error not showing up in certain conditions --------- Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: MokaStitcher Co-authored-by: MokaStitcher <54149968+MokaStitcher@users.noreply.github.com> Co-authored-by: innerthunder --- public/images/ui/legacy/link_icon.png | Bin 0 -> 209 bytes public/images/ui/legacy/unlink_icon.png | Bin 0 -> 219 bytes public/images/ui/link_icon.png | Bin 0 -> 209 bytes public/images/ui/unlink_icon.png | Bin 0 -> 219 bytes src/loading-scene.ts | 2 + src/ui/admin-ui-handler.ts | 357 +++++++++++++++++++++--- src/ui/form-modal-ui-handler.ts | 57 ++-- src/ui/login-form-ui-handler.ts | 25 +- src/ui/menu-ui-handler.ts | 44 ++- src/ui/modal-ui-handler.ts | 3 + src/ui/registration-form-ui-handler.ts | 14 +- src/ui/rename-form-ui-handler.ts | 10 +- src/ui/test-dialogue-ui-handler.ts | 13 +- 13 files changed, 437 insertions(+), 88 deletions(-) create mode 100644 public/images/ui/legacy/link_icon.png create mode 100644 public/images/ui/legacy/unlink_icon.png create mode 100644 public/images/ui/link_icon.png create mode 100644 public/images/ui/unlink_icon.png diff --git a/public/images/ui/legacy/link_icon.png b/public/images/ui/legacy/link_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..56081261b9c699571cd9faf1e242b708031a2188 GIT binary patch literal 209 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~(Ey(iS0F7S zBBGF@8~)z!6O!-hwX9tpn8k_Jk!mjw9*GyDgGrS;2x1BJppT^vIsrl$4>@*Pm% zaninU@BfKwmd1~MJyrHlcqDW3Lvre_D{FF2N-+OwVOYVqdCPK>IBtJ6t?$y}>T-NB yd;YT(r&d;#F$)^F-d(vS#eCYwSud6APOEbkF@Ah05PJ(~1%s!npUXO@geCwUuu3cd literal 0 HcmV?d00001 diff --git a/public/images/ui/legacy/unlink_icon.png b/public/images/ui/legacy/unlink_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f0da5f8e3eda4fdf27c6e07b11fffa17af715976 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~p#Yx{S0JsU zsp;?U-__N%VZ(+;j~-2EH(3D`Wh)8t3ugEa0#%g{{sBc&JzX3_B&O!}ALKn?z`@LW zk*)tNn$K+b3IboFyt I=akR{02#wdng9R* literal 0 HcmV?d00001 diff --git a/public/images/ui/link_icon.png b/public/images/ui/link_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..56081261b9c699571cd9faf1e242b708031a2188 GIT binary patch literal 209 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~(Ey(iS0F7S zBBGF@8~)z!6O!-hwX9tpn8k_Jk!mjw9*GyDgGrS;2x1BJppT^vIsrl$4>@*Pm% zaninU@BfKwmd1~MJyrHlcqDW3Lvre_D{FF2N-+OwVOYVqdCPK>IBtJ6t?$y}>T-NB yd;YT(r&d;#F$)^F-d(vS#eCYwSud6APOEbkF@Ah05PJ(~1%s!npUXO@geCwUuu3cd literal 0 HcmV?d00001 diff --git a/public/images/ui/unlink_icon.png b/public/images/ui/unlink_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f0da5f8e3eda4fdf27c6e07b11fffa17af715976 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~p#Yx{S0JsU zsp;?U-__N%VZ(+;j~-2EH(3D`Wh)8t3ugEa0#%g{{sBc&JzX3_B&O!}ALKn?z`@LW zk*)tNn$K+b3IboFyt I=akR{02#wdng9R* literal 0 HcmV?d00001 diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 4f673fd2cfc..26936bcdaad 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -165,6 +165,8 @@ export class LoadingScene extends SceneBase { this.loadImage("discord", "ui"); this.loadImage("google", "ui"); this.loadImage("settings_icon", "ui"); + this.loadImage("link_icon", "ui"); + this.loadImage("unlink_icon", "ui"); this.loadImage("default_bg", "arenas"); // Load arena images diff --git a/src/ui/admin-ui-handler.ts b/src/ui/admin-ui-handler.ts index c73c02e66c2..6249e54d8c3 100644 --- a/src/ui/admin-ui-handler.ts +++ b/src/ui/admin-ui-handler.ts @@ -2,37 +2,83 @@ import BattleScene from "#app/battle-scene"; import { ModalConfig } from "./modal-ui-handler"; import { Mode } from "./ui"; import * as Utils from "../utils"; -import { FormModalUiHandler } from "./form-modal-ui-handler"; +import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; import { Button } from "#app/enums/buttons"; +import { TextStyle } from "./text"; export default class AdminUiHandler extends FormModalUiHandler { + private adminMode: AdminMode; + private adminResult: AdminSearchInfo; + private config: ModalConfig; + + private readonly buttonGap = 10; + // http response from the server when a username isn't found in the server + private readonly httpUserNotFoundErrorCode: number = 404; + private readonly ERR_REQUIRED_FIELD = (field: string) => { + if (field === "username") { + return `${Utils.formatText(field)} is required`; + } else { + return `${Utils.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); } - setup(): void { - super.setup(); - } - - getModalTitle(config?: ModalConfig): string { + override getModalTitle(): string { return "Admin panel"; } - getFields(config?: ModalConfig): string[] { - return [ "Username", "Discord ID" ]; + override getWidth(): number { + return this.adminMode === AdminMode.ADMIN ? 180 : 160; } - getWidth(config?: ModalConfig): number { - return 160; + override getMargin(): [number, number, number, number] { + return [ 0, 0, 0, 0 ]; } - getMargin(config?: ModalConfig): [number, number, number, number] { - return [ 0, 0, 48, 0 ]; + override getButtonLabels(): string[] { + switch (this.adminMode) { + case AdminMode.LINK: + return [ "Link Account", "Cancel" ]; + case AdminMode.SEARCH: + return [ "Find account", "Cancel" ]; + case AdminMode.ADMIN: + return [ "Back to search", "Cancel" ]; + default: + return [ "Activate ADMIN", "Cancel" ]; + } } - getButtonLabels(config?: ModalConfig): string[] { - return [ "Link account", "Cancel" ]; + override getInputFieldConfigs(): InputFieldConfig[] { + const inputFieldConfigs: InputFieldConfig[] = []; + switch (this.adminMode) { + case AdminMode.LINK: + inputFieldConfigs.push( { label: "Username" }); + inputFieldConfigs.push( { label: "Discord ID" }); + break; + case AdminMode.SEARCH: + inputFieldConfigs.push( { label: "Username" }); + break; + case AdminMode.ADMIN: + const adminResult = this.adminResult ?? { username: "", discordId: "", googleId: "", lastLoggedIn: "", registered: "" }; + // Discord and Google ID fields that are not empty get locked, other fields are all locked + inputFieldConfigs.push( { label: "Username", isReadOnly: true }); + inputFieldConfigs.push( { label: "Discord ID", isReadOnly: adminResult.discordId !== "" }); + inputFieldConfigs.push( { label: "Google ID", isReadOnly: adminResult.googleId !== "" }); + inputFieldConfigs.push( { label: "Last played", isReadOnly: true }); + inputFieldConfigs.push( { label: "Registered", isReadOnly: true }); + break; + } + return inputFieldConfigs; } processInput(button: Button): boolean { @@ -45,44 +91,281 @@ export default class AdminUiHandler extends FormModalUiHandler { } show(args: any[]): boolean { + this.config = args[0] as ModalConfig; // config + this.adminMode = args[1] as AdminMode; // admin mode + this.adminResult = args[2] ?? { username: "", discordId: "", googleId: "", lastLoggedIn: "", registered: "" }; // admin result, if any + const isMessageError = args[3]; // is the message shown a success or error + + const fields = this.getInputFieldConfigs(); + const hasTitle = !!this.getModalTitle(); + + this.updateFields(fields, hasTitle); + this.updateContainer(this.config); + + const labels = this.getButtonLabels(); + for (let i = 0; i < labels.length; i++) { + this.buttonLabels[i].setText(labels[i]); // sets the label text + } + + this.errorMessage.setPosition(10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin()); // sets the position of the message dynamically + if (isMessageError) { + this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK)); + this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true)); + } else { + this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_GREEN)); + this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_GREEN, true)); + } + if (super.show(args)) { - const config = args[0] as ModalConfig; + this.populateFields(this.adminMode, this.adminResult); const originalSubmitAction = this.submitAction; this.submitAction = (_) => { this.submitAction = originalSubmitAction; + const adminSearchResult: AdminSearchInfo = this.convertInputsToAdmin(); // this converts the input texts into a single object for use later + const validFields = this.areFieldsValid(this.adminMode); + if (validFields.error) { + 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 ?? "", adminSearchResult, true); + } this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); - const onFail = error => { - this.scene.ui.setMode(Mode.ADMIN, Object.assign(config, { errorMessage: error?.trim() })); - this.scene.ui.playError(); - }; - if (!this.inputs[0].text) { - return onFail("Username is required"); + if (this.adminMode === AdminMode.LINK) { + this.adminLinkUnlink(adminSearchResult, "discord", "Link") // calls server to link discord + .then(response => { + if (response.error) { + return this.showMessage(response.errorType, adminSearchResult, true); // error or some kind + } else { + return this.showMessage(this.SUCCESS_SERVICE_MODE("discord", "link"), adminSearchResult, false); // success + } + }); + } else if (this.adminMode === AdminMode.SEARCH) { + this.adminSearch(adminSearchResult) // admin search for username + .then(response => { + if (response.error) { + return this.showMessage(response.errorType, adminSearchResult, true); // failure + } + this.updateAdminPanelInfo(response.adminSearchResult ?? adminSearchResult); // success + }); + } else if (this.adminMode === AdminMode.ADMIN) { + this.updateAdminPanelInfo(adminSearchResult, AdminMode.SEARCH); } - if (!this.inputs[1].text) { - return onFail("Discord Id is required"); - } - Utils.apiPost("admin/account/discord-link", `username=${encodeURIComponent(this.inputs[0].text)}&discordId=${encodeURIComponent(this.inputs[1].text)}`, "application/x-www-form-urlencoded", true) - .then(response => { - if (!response.ok) { - console.error(response); - } - this.inputs[0].setText(""); - this.inputs[1].setText(""); - this.scene.ui.revertMode(); - }) - .catch((err) => { - console.error(err); - this.scene.ui.revertMode(); - }); return false; }; return true; } return false; + } + showMessage(message: string, adminResult: AdminSearchInfo, isError: boolean) { + this.scene.ui.setMode(Mode.ADMIN, Object.assign(this.config, { errorMessage: message?.trim() }), this.adminMode, adminResult, isError); + if (isError) { + this.scene.ui.playError(); + } else { + this.scene.ui.playSelect(); + } + } + + /** + * This is used to update the fields' text when loading in a new admin ui handler. It uses the {@linkcode adminResult} + * to update the input text based on the {@linkcode adminMode}. For a linking adminMode, it sets the username and discord. + * For a search adminMode, it sets the username. For an admin adminMode, it sets all the info from adminResult in the + * appropriate text boxes, and also sets the link/unlink icons for discord/google depending on the result + */ + private populateFields(adminMode: AdminMode, adminResult: AdminSearchInfo) { + switch (adminMode) { + case AdminMode.LINK: + this.inputs[0].setText(adminResult.username); + this.inputs[1].setText(adminResult.discordId); + break; + case AdminMode.SEARCH: + this.inputs[0].setText(adminResult.username); + break; + case AdminMode.ADMIN: + Object.keys(adminResult).forEach((aR, i) => { + this.inputs[i].setText(adminResult[aR]); + if (aR === "discordId" || aR === "googleId") { // this is here to add the icons for linking/unlinking of google/discord IDs + const nineSlice = this.inputContainers[i].list.find(iC => iC.type === "NineSlice"); + const img = this.scene.add.image(this.inputContainers[i].x + nineSlice!.width + this.buttonGap, this.inputContainers[i].y + (Math.floor(nineSlice!.height / 2)), adminResult[aR] === "" ? "link_icon" : "unlink_icon"); + img.setName(`adminBtn_${aR}`); + img.setOrigin(0.5, 0.5); + img.setInteractive(); + img.on("pointerdown", () => { + const service = aR.toLowerCase().replace("id", ""); // this takes our key (discordId or googleId) and removes the "Id" at the end to make it more url friendly + const mode = adminResult[aR] === "" ? "Link" : "Unlink"; // this figures out if we're linking or unlinking a service + const validFields = this.areFieldsValid(this.adminMode, service); + if (validFields.error) { + 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 + if (response.error) { + this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); + return this.showMessage(response.errorType, adminResult, true); // fail + } else { // success, reload panel with new results + this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); + this.adminSearch(adminResult) + .then(response => { + if (response.error) { + return this.showMessage(response.errorType, adminResult, true); + } + return this.showMessage(this.SUCCESS_SERVICE_MODE(service, mode), response.adminSearchResult ?? adminResult, false); + }); + } + }); + }); + this.addInteractionHoverEffect(img); + this.modalContainer.add(img); + } + }); + break; + } + } + + private areFieldsValid(adminMode: AdminMode, service?: string): { error: boolean; errorMessage?: string; } { + switch (adminMode) { + case AdminMode.LINK: + if (!this.inputs[0].text) { // username missing from link panel + return { + error: true, + errorMessage: this.ERR_REQUIRED_FIELD("username") + }; + } + if (!this.inputs[1].text) { // discordId missing from linking panel + return { + error: true, + errorMessage: this.ERR_REQUIRED_FIELD("discord") + }; + } + break; + case AdminMode.SEARCH: + if (!this.inputs[0].text) { // username missing from search panel + return { + error: true, + errorMessage: this.ERR_REQUIRED_FIELD("username") + }; + } + break; + case AdminMode.ADMIN: + if (!this.inputs[1].text && service === "discord") { // discordId missing from admin panel + return { + error: true, + errorMessage: this.ERR_REQUIRED_FIELD(service) + }; + } + if (!this.inputs[2].text && service === "google") { // googleId missing from admin panel + return { + error: true, + errorMessage: this.ERR_REQUIRED_FIELD(service) + }; + } + break; + } + return { + error: false + }; + } + + private convertInputsToAdmin(): AdminSearchInfo { + return { + username: this.inputs[0]?.node ? this.inputs[0].text : "", + discordId: this.inputs[1]?.node ? this.inputs[1]?.text : "", + googleId: this.inputs[2]?.node ? this.inputs[2]?.text : "", + lastLoggedIn: this.inputs[3]?.node ? this.inputs[3]?.text : "", + registered: this.inputs[4]?.node ? this.inputs[4]?.text : "" + }; + } + + 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 }; + } else { // success + const adminInfoJson: AdminSearchInfo = await adminInfo.json(); + return { adminSearchResult: adminInfoJson, error: false }; + } + } catch (err) { + console.error(err); + return { error: true, errorType: err }; + } + } + + private async adminLinkUnlink(adminSearchResult: AdminSearchInfo, service: string, mode: string) { + 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! + return { adminSearchResult: adminSearchResult, error: false }; + } + } catch (err) { + console.error(err); + return { error: true, errorType: err }; + } + } + + private updateAdminPanelInfo(adminSearchResult: AdminSearchInfo, mode?: AdminMode) { + mode = mode ?? AdminMode.ADMIN; + this.scene.ui.setMode(Mode.ADMIN, { + buttonActions: [ + // we double revert here and below to go back 2 layers of menus + () => { + this.scene.ui.revertMode(); + this.scene.ui.revertMode(); + }, + () => { + this.scene.ui.revertMode(); + this.scene.ui.revertMode(); + } + ] + }, mode, adminSearchResult); } clear(): void { super.clear(); + + // this is used to remove the existing fields on the admin panel so they can be updated + + const itemsToRemove: string[] = [ "formLabel", "adminBtn" ]; // this is the start of the names for each element we want to remove + const removeArray: any[] = []; + const mC = this.modalContainer.list; + for (let i = mC.length - 1; i >= 0; i--) { + /* This code looks for a few things before destroying the specific field; first it looks to see if the name of the element is %like% the itemsToRemove labels + * this means that anything with, for example, "formLabel", will be true. + * It then also checks for any containers that are within this.modalContainer, and checks if any of its child elements are of type rexInputText + * and if either of these conditions are met, the element is destroyed. + */ + if (itemsToRemove.some(iTR => mC[i].name.includes(iTR)) || (mC[i].type === "Container" && (mC[i] as Phaser.GameObjects.Container).list.find(m => m.type === "rexInputText"))) { + removeArray.push(mC[i]); + } + } + + while (removeArray.length > 0) { + this.modalContainer.remove(removeArray.pop(), true); + } } } + +export enum AdminMode { + LINK, + SEARCH, + ADMIN +} + +export function getAdminModeName(adminMode: AdminMode): string { + switch (adminMode) { + case AdminMode.LINK: + return "Link"; + case AdminMode.SEARCH: + return "Search"; + default: + return ""; + } +} + +interface AdminSearchInfo { + username: string; + discordId: string; + googleId: string; + lastLoggedIn: string; + registered: string; +} diff --git a/src/ui/form-modal-ui-handler.ts b/src/ui/form-modal-ui-handler.ts index 331154263ad..65ee9f2db10 100644 --- a/src/ui/form-modal-ui-handler.ts +++ b/src/ui/form-modal-ui-handler.ts @@ -5,7 +5,6 @@ import { TextStyle, addTextInputObject, addTextObject } from "./text"; import { WindowVariant, addWindow } from "./ui-theme"; import InputText from "phaser3-rex-plugins/plugins/inputtext"; import * as Utils from "../utils"; -import i18next from "i18next"; import { Button } from "#enums/buttons"; export interface FormModalConfig extends ModalConfig { @@ -19,6 +18,7 @@ export abstract class FormModalUiHandler extends ModalUiHandler { protected errorMessage: Phaser.GameObjects.Text; protected submitAction: Function | null; protected tween: Phaser.Tweens.Tween; + protected formLabels: Phaser.GameObjects.Text[]; constructor(scene: BattleScene, mode: Mode | null = null) { super(scene, mode); @@ -26,12 +26,18 @@ export abstract class FormModalUiHandler extends ModalUiHandler { this.editing = false; this.inputContainers = []; this.inputs = []; + this.formLabels = []; } - abstract getFields(): string[]; + /** + * Get configuration for all fields that should be part of the modal + * Gets used by {@linkcode updateFields} to add the proper text inputs and labels to the view + * @returns array of {@linkcode InputFieldConfig} + */ + abstract getInputFieldConfigs(): InputFieldConfig[]; getHeight(config?: ModalConfig): number { - return 20 * this.getFields().length + (this.getModalTitle() ? 26 : 0) + ((config as FormModalConfig)?.errorMessage ? 12 : 0) + this.getButtonTopMargin() + 28; + return 20 * this.getInputFieldConfigs().length + (this.getModalTitle() ? 26 : 0) + ((config as FormModalConfig)?.errorMessage ? 12 : 0) + this.getButtonTopMargin() + 28; } getReadableErrorMessage(error: string): string { @@ -45,37 +51,50 @@ export abstract class FormModalUiHandler extends ModalUiHandler { setup(): void { super.setup(); - const fields = this.getFields(); + const config = this.getInputFieldConfigs(); const hasTitle = !!this.getModalTitle(); - fields.forEach((field, f) => { - const label = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * f, field, TextStyle.TOOLTIP_CONTENT); + if (config.length >= 1) { + this.updateFields(config, hasTitle); + } - this.modalContainer.add(label); + this.errorMessage = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * (config.length - 1) + 16 + this.getButtonTopMargin(), "", TextStyle.TOOLTIP_CONTENT); + this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK)); + this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true)); + this.errorMessage.setVisible(false); + this.modalContainer.add(this.errorMessage); + } + + protected updateFields(fieldsConfig: InputFieldConfig[], hasTitle: boolean) { + this.inputContainers = []; + this.inputs = []; + this.formLabels = []; + fieldsConfig.forEach((config, f) => { + const label = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * f, config.label, TextStyle.TOOLTIP_CONTENT); + label.name = "formLabel" + f; + + this.formLabels.push(label); + this.modalContainer.add(this.formLabels[this.formLabels.length - 1]); const inputContainer = this.scene.add.container(70, (hasTitle ? 28 : 2) + 20 * f); inputContainer.setVisible(false); const inputBg = addWindow(this.scene, 0, 0, 80, 16, false, false, 0, 0, WindowVariant.XTHIN); - const isPassword = field.includes(i18next.t("menu:password")) || field.includes(i18next.t("menu:confirmPassword")); - const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 20 }); + const isPassword = config?.isPassword; + const isReadOnly = config?.isReadOnly; + const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 20, readOnly: isReadOnly }); input.setOrigin(0, 0); inputContainer.add(inputBg); inputContainer.add(input); - this.modalContainer.add(inputContainer); this.inputContainers.push(inputContainer); + this.modalContainer.add(inputContainer); + this.inputs.push(input); }); - - this.errorMessage = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin(), "", TextStyle.TOOLTIP_CONTENT); - this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK)); - this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true)); - this.errorMessage.setVisible(false); - this.modalContainer.add(this.errorMessage); } show(args: any[]): boolean { @@ -149,3 +168,9 @@ export abstract class FormModalUiHandler extends ModalUiHandler { } } } + +export interface InputFieldConfig { + label: string, + isPassword?: boolean, + isReadOnly?: boolean +} diff --git a/src/ui/login-form-ui-handler.ts b/src/ui/login-form-ui-handler.ts index 8be432ad6c1..26a2a225ec6 100644 --- a/src/ui/login-form-ui-handler.ts +++ b/src/ui/login-form-ui-handler.ts @@ -1,4 +1,4 @@ -import { FormModalUiHandler } from "./form-modal-ui-handler"; +import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; import { ModalConfig } from "./modal-ui-handler"; import * as Utils from "../utils"; import { Mode } from "./ui"; @@ -17,9 +17,9 @@ interface BuildInteractableImageOpts { export default class LoginFormUiHandler extends FormModalUiHandler { private readonly ERR_USERNAME: string = "invalid username"; - private readonly ERR_PASSWORD: string = "invalid password"; - private readonly ERR_ACCOUNT_EXIST: string = "account doesn't exist"; - private readonly ERR_PASSWORD_MATCH: string = "password doesn't match"; + private readonly ERR_PASSWORD: string = "invalid password"; + private readonly ERR_ACCOUNT_EXIST: string = "account doesn't exist"; + private readonly ERR_PASSWORD_MATCH: string = "password doesn't match"; private readonly ERR_NO_SAVES: string = "No save files found"; private readonly ERR_TOO_MANY_SAVES: string = "Too many save files found"; @@ -75,10 +75,6 @@ export default class LoginFormUiHandler extends FormModalUiHandler { return i18next.t("menu:login"); } - override getFields(_config?: ModalConfig): string[] { - return [ i18next.t("menu:username"), i18next.t("menu:password") ]; - } - override getWidth(_config?: ModalConfig): number { return 160; } @@ -106,14 +102,21 @@ export default class LoginFormUiHandler extends FormModalUiHandler { case this.ERR_PASSWORD_MATCH: return i18next.t("menu:unmatchingPassword"); case this.ERR_NO_SAVES: - return i18next.t("menu:noSaves"); + return "P01: " + i18next.t("menu:noSaves"); case this.ERR_TOO_MANY_SAVES: - return i18next.t("menu:tooManySaves"); + return "P02: " + i18next.t("menu:tooManySaves"); } return super.getReadableErrorMessage(error); } + override getInputFieldConfigs(): InputFieldConfig[] { + const inputFieldConfigs: InputFieldConfig[] = []; + inputFieldConfigs.push({ label: i18next.t("menu:username") }); + inputFieldConfigs.push({ label: i18next.t("menu:password"), isPassword: true }); + return inputFieldConfigs; + } + override show(args: any[]): boolean { if (super.show(args)) { @@ -164,7 +167,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler { [ this.discordImage, this.googleImage, this.usernameInfoImage ].forEach((img) => img.off("pointerdown")); } - private processExternalProvider(config: ModalConfig) : void { + private processExternalProvider(config: ModalConfig): void { this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? ""); this.externalPartyTitle.setX(20 + this.externalPartyTitle.text.length); this.externalPartyTitle.setVisible(true); diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 0f4c2d2f53e..301d54daa3a 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -13,6 +13,7 @@ import { GameDataType } from "#enums/game-data-type"; 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"; enum MenuOptions { GAME_SETTINGS, @@ -387,16 +388,41 @@ export default class MenuUiHandler extends MessageUiHandler { communityOptions.push({ label: "Admin", handler: () => { - ui.playSelect(); - ui.setOverlayMode(Mode.ADMIN, { - buttonActions: [ - () => { - ui.revertMode(); - }, - () => { - ui.revertMode(); + + const skippedAdminModes: AdminMode[] = [ AdminMode.ADMIN ]; // this is here so that we can skip the menu populating enums that aren't meant for the menu, such as the AdminMode.ADMIN + const options: OptionSelectItem[] = []; + Object.values(AdminMode).filter((v) => !isNaN(Number(v)) && !skippedAdminModes.includes(v as AdminMode)).forEach((mode) => { // this gets all the enums in a way we can use + options.push({ + label: getAdminModeName(mode as AdminMode), + handler: () => { + ui.playSelect(); + ui.setOverlayMode(Mode.ADMIN, { + buttonActions: [ + // we double revert here and below to go back 2 layers of menus + () => { + ui.revertMode(); + ui.revertMode(); + }, + () => { + ui.revertMode(); + ui.revertMode(); + } + ] + }, mode); // mode is our AdminMode enum + return true; } - ] + }); + }); + options.push({ + label: "Cancel", + handler: () => { + ui.revertMode(); + return true; + } + }); + this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, { + options: options, + delay: 0 }); return true; }, diff --git a/src/ui/modal-ui-handler.ts b/src/ui/modal-ui-handler.ts index 60204f18c4a..79f1e8afeed 100644 --- a/src/ui/modal-ui-handler.ts +++ b/src/ui/modal-ui-handler.ts @@ -15,12 +15,14 @@ export abstract class ModalUiHandler extends UiHandler { protected titleText: Phaser.GameObjects.Text; protected buttonContainers: Phaser.GameObjects.Container[]; protected buttonBgs: Phaser.GameObjects.NineSlice[]; + protected buttonLabels: Phaser.GameObjects.Text[]; constructor(scene: BattleScene, mode: Mode | null = null) { super(scene, mode); this.buttonContainers = []; this.buttonBgs = []; + this.buttonLabels = []; } abstract getModalTitle(config?: ModalConfig): string; @@ -75,6 +77,7 @@ export abstract class ModalUiHandler extends UiHandler { const buttonContainer = this.scene.add.container(0, buttonTopMargin); + this.buttonLabels.push(buttonLabel); this.buttonBgs.push(buttonBg); this.buttonContainers.push(buttonContainer); diff --git a/src/ui/registration-form-ui-handler.ts b/src/ui/registration-form-ui-handler.ts index 2f8486bcdb3..fc9eb85cbaf 100644 --- a/src/ui/registration-form-ui-handler.ts +++ b/src/ui/registration-form-ui-handler.ts @@ -1,4 +1,4 @@ -import { FormModalUiHandler } from "./form-modal-ui-handler"; +import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; import { ModalConfig } from "./modal-ui-handler"; import * as Utils from "../utils"; import { Mode } from "./ui"; @@ -24,10 +24,6 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler { return i18next.t("menu:register"); } - getFields(config?: ModalConfig): string[] { - return [ i18next.t("menu:username"), i18next.t("menu:password"), i18next.t("menu:confirmPassword") ]; - } - getWidth(config?: ModalConfig): number { return 160; } @@ -61,6 +57,14 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler { return super.getReadableErrorMessage(error); } + override getInputFieldConfigs(): InputFieldConfig[] { + const inputFieldConfigs: InputFieldConfig[] = []; + inputFieldConfigs.push({ label: i18next.t("menu:username") }); + inputFieldConfigs.push({ label: i18next.t("menu:password"), isPassword: true }); + inputFieldConfigs.push({ label: i18next.t("menu:confirmPassword"), isPassword: true }); + return inputFieldConfigs; + } + setup(): void { super.setup(); diff --git a/src/ui/rename-form-ui-handler.ts b/src/ui/rename-form-ui-handler.ts index 078177cafb1..6e4c4c6809d 100644 --- a/src/ui/rename-form-ui-handler.ts +++ b/src/ui/rename-form-ui-handler.ts @@ -1,4 +1,4 @@ -import { FormModalUiHandler } from "./form-modal-ui-handler"; +import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; import { ModalConfig } from "./modal-ui-handler"; import i18next from "i18next"; import { PlayerPokemon } from "#app/field/pokemon"; @@ -8,10 +8,6 @@ export default class RenameFormUiHandler extends FormModalUiHandler { return i18next.t("menu:renamePokemon"); } - getFields(config?: ModalConfig): string[] { - return [ i18next.t("menu:nickname") ]; - } - getWidth(config?: ModalConfig): number { return 160; } @@ -33,6 +29,10 @@ export default class RenameFormUiHandler extends FormModalUiHandler { return super.getReadableErrorMessage(error); } + override getInputFieldConfigs(): InputFieldConfig[] { + return [{ label: i18next.t("menu:nickname") }]; + } + show(args: any[]): boolean { if (super.show(args)) { const config = args[0] as ModalConfig; diff --git a/src/ui/test-dialogue-ui-handler.ts b/src/ui/test-dialogue-ui-handler.ts index 34fb80ecc89..bf0e7f6723f 100644 --- a/src/ui/test-dialogue-ui-handler.ts +++ b/src/ui/test-dialogue-ui-handler.ts @@ -1,4 +1,4 @@ -import { FormModalUiHandler } from "./form-modal-ui-handler"; +import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; import { ModalConfig } from "./modal-ui-handler"; import i18next from "i18next"; import { PlayerPokemon } from "#app/field/pokemon"; @@ -43,10 +43,6 @@ export default class TestDialogueUiHandler extends FormModalUiHandler { return "Test Dialogue"; } - getFields(config?: ModalConfig): string[] { - return [ "Dialogue" ]; - } - getWidth(config?: ModalConfig): number { return 300; } @@ -68,8 +64,15 @@ export default class TestDialogueUiHandler extends FormModalUiHandler { return super.getReadableErrorMessage(error); } + override getInputFieldConfigs(): InputFieldConfig[] { + return [{ label: "Dialogue" }]; + } + show(args: any[]): boolean { const ui = this.getUi(); + const hasTitle = !!this.getModalTitle(); + this.updateFields(this.getInputFieldConfigs(), hasTitle); + this.updateContainer(args[0] as ModalConfig); const input = this.inputs[0]; input.setMaxLength(255); From 414e0a5447dfe1ef119291a57a1fc53cc45ec29b Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:17:55 -0700 Subject: [PATCH 17/17] [Balance] Change Tyrogue to move-based evolutions (#4694) --- src/data/balance/pokemon-evolutions.ts | 19 +++++++++++++++---- src/data/balance/pokemon-level-moves.ts | 9 +++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index 7d0d86fadbf..4a6e44e0d51 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -1,7 +1,6 @@ import { Gender } from "#app/data/gender"; import { PokeballType } from "#app/data/pokeball"; import Pokemon from "#app/field/pokemon"; -import { Stat } from "#enums/stat"; import { Type } from "#app/data/type"; import * as Utils from "#app/utils"; import { WeatherType } from "#app/data/weather"; @@ -271,9 +270,21 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.MAROWAK, 28, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.TYROGUE]: [ - new SpeciesEvolution(Species.HITMONLEE, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] > p.stats[Stat.DEF])), - new SpeciesEvolution(Species.HITMONCHAN, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] < p.stats[Stat.DEF])), - new SpeciesEvolution(Species.HITMONTOP, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] === p.stats[Stat.DEF])) + /** + * Custom: Evolves into Hitmonlee, Hitmonchan or Hitmontop at level 20 + * if it knows Low Sweep, Mach Punch, or Rapid Spin, respectively. + * If Tyrogue knows multiple of these moves, its evolution is based on + * the first qualifying move in its moveset. + */ + new SpeciesEvolution(Species.HITMONLEE, 20, null, new SpeciesEvolutionCondition(p => + p.getMoveset(true).find(move => move && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(move?.moveId))?.moveId === Moves.LOW_SWEEP + )), + new SpeciesEvolution(Species.HITMONCHAN, 20, null, new SpeciesEvolutionCondition(p => + p.getMoveset(true).find(move => move && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(move?.moveId))?.moveId === Moves.MACH_PUNCH + )), + new SpeciesEvolution(Species.HITMONTOP, 20, null, new SpeciesEvolutionCondition(p => + p.getMoveset(true).find(move => move && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(move?.moveId))?.moveId === Moves.RAPID_SPIN + )), ], [Species.KOFFING]: [ new SpeciesEvolution(Species.GALAR_WEEZING, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), diff --git a/src/data/balance/pokemon-level-moves.ts b/src/data/balance/pokemon-level-moves.ts index 53f547c4504..71d98fb4fc2 100644 --- a/src/data/balance/pokemon-level-moves.ts +++ b/src/data/balance/pokemon-level-moves.ts @@ -1835,6 +1835,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.LOW_SWEEP ], [ 1, Moves.JUMP_KICK ], [ 1, Moves.ROLLING_KICK ], + [ 1, Moves.MACH_PUNCH ], // Previous Stage Move, Custom + [ 1, Moves.RAPID_SPIN ], // Previous Stage Move, Custom [ 4, Moves.DOUBLE_KICK ], [ 8, Moves.LOW_KICK ], [ 12, Moves.ENDURE ], @@ -1857,6 +1859,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.FEINT ], [ 1, Moves.PURSUIT ], [ 1, Moves.COMET_PUNCH ], + [ 1, Moves.LOW_SWEEP ], // Previous Stage Move, Custom + [ 1, Moves.RAPID_SPIN ], // Previous Stage Move, Custom [ 4, Moves.MACH_PUNCH ], [ 8, Moves.VACUUM_WAVE ], [ 12, Moves.DETECT ], @@ -4148,6 +4152,9 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.FOCUS_ENERGY ], [ 1, Moves.FAKE_OUT ], [ 1, Moves.HELPING_HAND ], + [ 10, Moves.LOW_SWEEP ], // Custom + [ 10, Moves.MACH_PUNCH ], // Custom + [ 10, Moves.RAPID_SPIN ], // Custom ], [Species.HITMONTOP]: [ [ EVOLVE_MOVE, Moves.TRIPLE_KICK ], @@ -4159,6 +4166,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.FEINT ], [ 1, Moves.PURSUIT ], [ 1, Moves.ROLLING_KICK ], + [ 1, Moves.LOW_SWEEP ], // Previous Stage Move, Custom + [ 1, Moves.MACH_PUNCH ], // Previous Stage Move, Custom [ 4, Moves.QUICK_ATTACK ], [ 8, Moves.GYRO_BALL ], [ 12, Moves.DETECT ],