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/21] [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/21] [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/21] [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/21] [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/21] [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/21] 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/21] [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/21] [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/21] [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/21] [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/21] [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/21] [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/21] [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/21] [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/21] [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 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 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/21] [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/21] [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 ], From a50763cd897b431b2be80ee34026464dc0a13800 Mon Sep 17 00:00:00 2001 From: Frederico Santos Date: Fri, 25 Oct 2024 23:29:20 +0100 Subject: [PATCH 18/21] updating locales --- public/locales | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales b/public/locales index 87615556d8a..71390cba88f 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 87615556d8a2bd7eef7abac818f84423a8a13b03 +Subproject commit 71390cba88f4103d0d2273d59a6dd8340a4fa54f From 6418f46bf7e97ee986e26b54aaa125a930c74248 Mon Sep 17 00:00:00 2001 From: chaosgrimmon <31082757+chaosgrimmon@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:02:05 -0400 Subject: [PATCH 19/21] [Sprite] Fix transparency of pixels on Fletchinder (#4720) * [Sprite] Front Fletchinder * [Sprite] Back Fletchinder * [Sprite] Shiny front Fletchinder * [Sprite] Shiny back Fletchinder --- public/images/pokemon/exp/662.png | Bin 1718 -> 1869 bytes public/images/pokemon/exp/back/662.png | Bin 1439 -> 1526 bytes public/images/pokemon/exp/back/shiny/662.png | Bin 1439 -> 1526 bytes public/images/pokemon/exp/shiny/662.png | Bin 1721 -> 1870 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/images/pokemon/exp/662.png b/public/images/pokemon/exp/662.png index 090e6e4c91f61b1d7b93064474bdd46e2de2f04a..e47863f724b8998808b7a4f7377497dafdfe38f4 100644 GIT binary patch delta 1812 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 br{ga%y@t7ye^)2~0000~c1m8>n zbv1fNW?nAh_J241Lso6On6`od)D%#2Q&UFn_&()gY`1SXuGLBcFx%gI4Ny<1d0W$| zOSoNetDZEX_8zSfd^Jr}041iz8qZq=Qy^lShF0o2ar7eO*1{zv_vk%^i=fpCZ{ z6b!_({ePn=lU2hknmP_qx?w;f7rxzNtf6*K(2<3Lfr=f)Sb2Ko5|vOekfI@@rDg^Y z^++&qk^$1xt=SbpM?Dk__?H(uz755?#7~JdQtiSM2Eyy~?FLIMBa+W#YncNB{`K{K zE!-s3=CiwJTX-!>hIw(1HD2D%Kh zAhfZ}YQMb#lLDW)*;w|n@ir0+eBkExmAe#ftiV12Iw-meVydhqx&R;gHI_O30Pz@F zEq_|(JOYTv$ZF>b>>=Q@L1-Wt5RZ}7&_E8@www2L2qhep|4hU{%zQteQ~49tJz~G? zq&WfaM?%Cu$4GMm-j9SxeqR&j1i4Aogdn2E50QNPN^^qTq-sJCQDcWlBS>?4L@m8O z7U#r;D#e!mYNBg?T@B@hh{|Vn6(r7yt$&tQ&#tx}ijMZ2E6fRXW2qgKs0njI)!v-t znESOPI(bNP!rU5aS))`{O^_3+RxPKe?LIzpA3&N@FSl5ZYHEUK((s|Hs6cYD<{}3OhL65t9woYzbwFQ`;s&#kzN*n)i5f^ zNuZV;FUw6gTBIk)=@YfGLxu_RbAP(tDytU@Q{(4!vsq3r)>AoMit&1}dgg?l7u{~;LdMuGEjj#zq!ds1@8RjCL%}s_WaF;hX8K%Hp-rQuA0(W^glwk_o<=s$5DR5U(8l}Kpq%;fM zopTcS%>uVbEjwO5DRB2KQ(eshH{kYpF?hHFcT??87r3J%f%dTq-0kU}1#S+7=Y-1z zuUX(m+-8AWVm4}lTX3_H6CAX_EzgM`VAkG$ePn- delta 1421 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℞~o8>&TJ>+IN4{d9Iwpq?Y-3{rU z^N=F+XS{3G&fml#%Xzf)H*rXE2~GV?0(_l~{Y?VE%}=!UHI(dfn z{-#czp}oJUl4t1PZz|*&B7gJQ%-=kmXNdewBlFqL-#nIQNZ+5iHxv#0&4YP{)ZYwG zlDfZnEYHx--#nCO=;&{M9?3KG^fwRW8M^wL2l5PE{Y`nEp|8Iw&olJ(H^q5|-u|XE z&(Pc7l;#Izsb)toaAqE_q#aH-{kFgai+h?+3(_9f0MCq#i{;uCS%`|f#Glc5?FVt;!i5VLE?l^9K_CABwqvb_T8_8?00000NkvXXu0mjf DmMNn4 delta 1663 zcmYk7dtB1@9>%eLnqpvzr4unzC@+PqoHcI=s2R#7FOi$Wa%M)Nm{jV^BuQn}EVQ{Z zymX+vwJ5EH}isGnEZoFBmKc|6s(r7PfJWAnEs{C z;l0r1_b9ak3l_x-8**L$ISyR|AEi_d-dtLI$rj`IR>Ja>(9!fAVq$(rUSOZkGWVaR ze$?O8n3>t_)Ju;GyBi6iWf6VZHgwCc5-ni!lIa)Ee-BncTJuVk#JP3H z#M3$HM`OLR9a_0EJs;2*9<_`i-EBbEQ8O7!;W-16RnPs)B?ZNme z`*B0<4*D>=I#-hH=oP;a|BH52WVDfoOG;ufOwAp~vWx7NI5pAZiKAs^$*ZSY+g-2s z?PxwOU1+#OV-y=>Uvyb-wLX|fJ=xkGwk9}ezVTFZ=H1o&j!3_*D=sdOK+!J28=K1y zNkz?FU0eR$EWG<=8`X)noDtjMCCRwTez@i1p$qN7-ccNzoT8u%^tq}|>E0V-u2UK# zv;ES*N)mGD;g{AZq)9WbjvpmI4gZ%BOR~P*JDa_a)yN66BJGiCZ^7X#H1}P=swh3uwV#cDP^F}#sC%?0Vl%4K# z!GaGir$zH`pB>3g=Z6Q2KNVan0TD#Lp2{44`ilA=s$_FF)0Kp%C+`UY$wXt-yTdXA z9}}EJ=qcEP?Y4#naB$s+D8KA6XqX8+OD@RfeQtYQ<``>>l(U8IIBntAiDjl!t>qwX zX$e8Fz)7+c2ttfQtgVw=I1*xom!v@oAMr%^Nz>2n720M$M-Z-iGrG4e7`VuVWz4bM ztz|H@maHdpzu$M>a{Fsa=xhMbPjN2>L4Zrmkvt+lh`Y&&1BAL;X|rcp_NRF7_^I3i z`8GCIF@5AlSQ>Yj2H|wxn@RJ3fX7be*emDf@hM;j?a?lVTaf!ZVyD*+;r6c#ul*5= z8G;&8fNQL6P!s~3E2Z%;_xn)}2(Uy!<1rm|rqMc$7jyw_{+dGRaSRgZQ-QHXRm!y! zWX@~gKeWl|Eog;3jaJ^t#uEi0a^Y+roC9#*LUY)TERQ<8F#5aFffY)s9mcl~KR0-? z!PC-?TY{Jn#zxUefu3%}$i27TA*#GAwNh-N6`^Cloy*lZxI1nq#k^=1d7auGqS@8$559ij)G}-03*Z z8QFt4PM=06>!LVL@Y^DjTCMHc#-Fv3hcF>YyBkvvw5!z~u)#NRn`}-vWT+|bFtNIt fDqe}G+t~82k(Nec(Nw+mrvV)eAo Date: Fri, 25 Oct 2024 19:34:40 -0400 Subject: [PATCH 20/21] [Beta] Stop Transform giving copied moves negative fractional `ppUp` (#4722) * [Beta] Stop Transform giving copied moves negative fractional `ppUp` * Remove some bangs/etc --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/ability.ts | 15 +++++++------ src/data/move.ts | 13 +++++++----- src/field/pokemon.ts | 33 +++++++++++++++++++---------- src/modifier/modifier-type.ts | 2 +- src/modifier/modifier.ts | 2 +- src/system/pokemon-data.ts | 2 +- src/test/abilities/imposter.test.ts | 15 ++++++++++--- src/test/moves/transform.test.ts | 17 +++++++++++---- 8 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index d761657f5cd..01e99966ff8 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2453,16 +2453,19 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { pokemon.setStatStage(s, target.getStatStage(s)); } - 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.moveset = target.getMoveset().map((m) => { + if (m) { + // If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5. + return new PokemonMove(m.moveId, 0, 0, false, Math.min(m.getMove().pp, 5)); + } else { + console.warn(`Imposter: somehow iterating over a ${m} value when copying moveset!`); + return new PokemonMove(Moves.NONE); + } }); pokemon.summonData.types = target.getTypes(); promises.push(pokemon.updateInfo()); - pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target!.name, })); + pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target.name, })); pokemon.scene.playSound("battle_anims/PRSFX- Transform"); promises.push(pokemon.loadAssets(false).then(() => { pokemon.playAnim(); diff --git a/src/data/move.ts b/src/data/move.ts index efdd4568927..9979b24cc24 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6668,11 +6668,14 @@ export class TransformAttr extends MoveEffectAttr { user.setStatStage(s, target.getStatStage(s)); } - 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.moveset = target.getMoveset().map((m) => { + if (m) { + // If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5. + return new PokemonMove(m.moveId, 0, 0, false, Math.min(m.getMove().pp, 5)); + } else { + console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`); + return new PokemonMove(Moves.NONE); + } }); user.summonData.types = target.getTypes(); promises.push(user.updateInfo()); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d41c1f9eefa..a708b2067a7 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4431,7 +4431,7 @@ export class PlayerPokemon extends Pokemon { this.scene.removePartyMemberModifiers(fusedPartyMemberIndex); this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0]; const newPartyMemberIndex = this.scene.getParty().indexOf(this); - pokemon.getMoveset(true).map(m => this.scene.unshiftPhase(new LearnMovePhase(this.scene, newPartyMemberIndex, m!.getMove().id))); // TODO: is the bang correct? + pokemon.getMoveset(true).map((m: PokemonMove) => this.scene.unshiftPhase(new LearnMovePhase(this.scene, newPartyMemberIndex, m.getMove().id))); pokemon.destroy(); this.updateFusionPalette(); resolve(); @@ -4452,8 +4452,12 @@ export class PlayerPokemon extends Pokemon { /** Returns a deep copy of this Pokemon's moveset array */ copyMoveset(): PokemonMove[] { const newMoveset : PokemonMove[] = []; - this.moveset.forEach(move => - newMoveset.push(new PokemonMove(move!.moveId, 0, move!.ppUp, move!.virtual))); // TODO: are those bangs correct? + this.moveset.forEach((move) => { + // TODO: refactor `moveset` to not accept `null`s + if (move) { + newMoveset.push(new PokemonMove(move.moveId, 0, move.ppUp, move.virtual, move.maxPpOverride)); + } + }); return newMoveset; } @@ -5180,15 +5184,22 @@ export interface DamageCalculationResult { **/ export class PokemonMove { public moveId: Moves; - public ppUsed: integer; - public ppUp: integer; + public ppUsed: number; + public ppUp: number; public virtual: boolean; - constructor(moveId: Moves, ppUsed?: integer, ppUp?: integer, virtual?: boolean) { + /** + * If defined and nonzero, overrides the maximum PP of the move (e.g., due to move being copied by Transform). + * This also nullifies all effects of `ppUp`. + */ + public maxPpOverride?: number; + + constructor(moveId: Moves, ppUsed: number = 0, ppUp: number = 0, virtual: boolean = false, maxPpOverride?: number) { this.moveId = moveId; - this.ppUsed = ppUsed || 0; - this.ppUp = ppUp || 0; - this.virtual = !!virtual; + this.ppUsed = ppUsed; + this.ppUp = ppUp; + this.virtual = virtual; + this.maxPpOverride = maxPpOverride; } /** @@ -5225,7 +5236,7 @@ export class PokemonMove { } getMovePp(): integer { - return this.getMove().pp + this.ppUp * Utils.toDmgValue(this.getMove().pp / 5); + return this.maxPpOverride || (this.getMove().pp + this.ppUp * Utils.toDmgValue(this.getMove().pp / 5)); } getPpRatio(): number { @@ -5242,6 +5253,6 @@ export class PokemonMove { * @return {PokemonMove} A valid pokemonmove object */ static loadMove(source: PokemonMove | any): PokemonMove { - return new PokemonMove(source.moveId, source.ppUsed, source.ppUp, source.virtual); + return new PokemonMove(source.moveId, source.ppUsed, source.ppUp, source.virtual, source.maxPpOverride); } } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 3e475c62590..f4b59b9d882 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -384,7 +384,7 @@ export class PokemonPpUpModifierType extends PokemonMoveModifierType { (_pokemon: PlayerPokemon) => { return null; }, (pokemonMove: PokemonMove) => { - if (pokemonMove.getMove().pp < 5 || pokemonMove.ppUp >= 3) { + if (pokemonMove.getMove().pp < 5 || pokemonMove.ppUp >= 3 || pokemonMove.maxPpOverride) { return PartyUiHandler.NoEffectMessage; } return null; diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 11f16f103a5..36f94b99b20 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -2166,7 +2166,7 @@ export class PokemonPpUpModifier extends ConsumablePokemonMoveModifier { override apply(playerPokemon: PlayerPokemon): boolean { const move = playerPokemon.getMoveset()[this.moveIndex]; - if (move) { + if (move && !move.maxPpOverride) { move.ppUp = Math.min(move.ppUp + this.upPoints, 3); } diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 421739a9da1..c8756e4dd7f 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -135,7 +135,7 @@ export default class PokemonData { } } } else { - 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)); + 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, m.virtual, m.maxPpOverride)); if (!forHistory) { this.status = source.status ? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining) diff --git a/src/test/abilities/imposter.test.ts b/src/test/abilities/imposter.test.ts index 7aaac5ca8c4..3445b3b322c 100644 --- a/src/test/abilities/imposter.test.ts +++ b/src/test/abilities/imposter.test.ts @@ -60,18 +60,19 @@ describe("Abilities - Imposter", () => { const playerMoveset = player.getMoveset(); const enemyMoveset = player.getMoveset(); + expect(playerMoveset.length).toBe(enemyMoveset.length); for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) { - // TODO: Checks for 5 PP should be done here when that gets addressed expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId); } const playerTypes = player.getTypes(); const enemyTypes = enemy.getTypes(); + expect(playerTypes.length).toBe(enemyTypes.length); for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) { expect(playerTypes[i]).toBe(enemyTypes[i]); } - }, 20000); + }); it("should copy in-battle overridden stats", async () => { game.override.enemyMoveset([ Moves.POWER_SPLIT ]); @@ -104,7 +105,15 @@ describe("Abilities - Imposter", () => { await game.phaseInterceptor.to(TurnEndPhase); player.getMoveset().forEach(move => { - expect(move!.getMovePp()).toBeLessThanOrEqual(5); + // Should set correct maximum PP without touching `ppUp` + if (move) { + if (move.moveId === Moves.SKETCH) { + expect(move.getMovePp()).toBe(1); + } else { + expect(move.getMovePp()).toBe(5); + } + expect(move.ppUp).toBe(0); + } }); }); }); diff --git a/src/test/moves/transform.test.ts b/src/test/moves/transform.test.ts index 8c0f5eda7b2..adb97b42af7 100644 --- a/src/test/moves/transform.test.ts +++ b/src/test/moves/transform.test.ts @@ -60,18 +60,19 @@ describe("Moves - Transform", () => { const playerMoveset = player.getMoveset(); const enemyMoveset = player.getMoveset(); + expect(playerMoveset.length).toBe(enemyMoveset.length); for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) { - // TODO: Checks for 5 PP should be done here when that gets addressed expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId); } const playerTypes = player.getTypes(); const enemyTypes = enemy.getTypes(); + expect(playerTypes.length).toBe(enemyTypes.length); for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) { expect(playerTypes[i]).toBe(enemyTypes[i]); } - }, 20000); + }); it("should copy in-battle overridden stats", async () => { game.override.enemyMoveset([ Moves.POWER_SPLIT ]); @@ -94,7 +95,7 @@ describe("Moves - Transform", () => { expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); }); - it ("should set each move's pp to a maximum of 5", async () => { + 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 ]); @@ -104,7 +105,15 @@ describe("Moves - Transform", () => { await game.phaseInterceptor.to(TurnEndPhase); player.getMoveset().forEach(move => { - expect(move!.getMovePp()).toBeLessThanOrEqual(5); + // Should set correct maximum PP without touching `ppUp` + if (move) { + if (move.moveId === Moves.SKETCH) { + expect(move.getMovePp()).toBe(1); + } else { + expect(move.getMovePp()).toBe(5); + } + expect(move.ppUp).toBe(0); + } }); }); }); From 61cf937cab471d083f78f483c6d22aefaac72e3d Mon Sep 17 00:00:00 2001 From: Smewkie <168606612+Smewkie@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:38:12 -0500 Subject: [PATCH 21/21] [Balance] Ferroseed HA Ferroseed HA --- src/data/pokemon-species.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 96d1eb430fb..a93c35829ea 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -1864,7 +1864,7 @@ export function initSpecies() { new PokemonSpecies(Species.ALOMOMOLA, 5, false, false, false, "Caring Pokémon", Type.WATER, null, 1.2, 31.6, Abilities.HEALER, Abilities.HYDRATION, Abilities.REGENERATOR, 470, 165, 75, 80, 40, 45, 65, 75, 70, 165, GrowthRate.FAST, 50, false), new PokemonSpecies(Species.JOLTIK, 5, false, false, false, "Attaching Pokémon", Type.BUG, Type.ELECTRIC, 0.1, 0.6, Abilities.COMPOUND_EYES, Abilities.UNNERVE, Abilities.SWARM, 319, 50, 47, 50, 57, 50, 65, 190, 50, 64, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.GALVANTULA, 5, false, false, false, "EleSpider Pokémon", Type.BUG, Type.ELECTRIC, 0.8, 14.3, Abilities.COMPOUND_EYES, Abilities.UNNERVE, Abilities.SWARM, 472, 70, 77, 60, 97, 60, 108, 75, 50, 165, GrowthRate.MEDIUM_FAST, 50, false), - new PokemonSpecies(Species.FERROSEED, 5, false, false, false, "Thorn Seed Pokémon", Type.GRASS, Type.STEEL, 0.6, 18.8, Abilities.IRON_BARBS, Abilities.NONE, Abilities.IRON_BARBS, 305, 44, 50, 91, 24, 86, 10, 255, 50, 61, GrowthRate.MEDIUM_FAST, 50, false), + new PokemonSpecies(Species.FERROSEED, 5, false, false, false, "Thorn Seed Pokémon", Type.GRASS, Type.STEEL, 0.6, 18.8, Abilities.IRON_BARBS, Abilities.NONE, Abilities.ANTICIPATION, 305, 44, 50, 91, 24, 86, 10, 255, 50, 61, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.FERROTHORN, 5, false, false, false, "Thorn Pod Pokémon", Type.GRASS, Type.STEEL, 1, 110, Abilities.IRON_BARBS, Abilities.NONE, Abilities.ANTICIPATION, 489, 74, 94, 131, 54, 116, 20, 90, 50, 171, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.KLINK, 5, false, false, false, "Gear Pokémon", Type.STEEL, null, 0.3, 21, Abilities.PLUS, Abilities.MINUS, Abilities.CLEAR_BODY, 300, 40, 55, 70, 45, 60, 30, 130, 50, 60, GrowthRate.MEDIUM_SLOW, null, false), new PokemonSpecies(Species.KLANG, 5, false, false, false, "Gear Pokémon", Type.STEEL, null, 0.6, 51, Abilities.PLUS, Abilities.MINUS, Abilities.CLEAR_BODY, 440, 60, 80, 95, 70, 85, 50, 60, 50, 154, GrowthRate.MEDIUM_SLOW, null, false),