From 14b47c0eefa06fd87f0a8281b2e5a523da7523c1 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 20 Apr 2025 17:17:25 -0400 Subject: [PATCH] Grabbed reverted changes from stuff --- src/data/abilities/ability.ts | 187 +++-- src/data/battler-tags.ts | 72 +- src/data/berry.ts | 183 +++-- src/data/custom-pokemon-data.ts | 28 +- src/data/moves/move.ts | 86 ++- .../global-trade-system-encounter.ts | 4 +- .../the-winstrate-challenge-encounter.ts | 2 +- .../utils/encounter-phase-utils.ts | 7 +- .../encounter-transformation-sequence.ts | 4 +- src/field/pokemon.ts | 672 ++++++++++-------- src/modifier/modifier.ts | 31 +- src/overrides.ts | 10 +- src/phases/battle-end-phase.ts | 6 +- src/phases/berry-phase.ts | 89 ++- src/phases/encounter-phase.ts | 16 +- src/phases/evolution-phase.ts | 4 +- src/phases/form-change-phase.ts | 2 +- src/phases/move-phase.ts | 4 +- src/phases/new-biome-encounter-phase.ts | 11 +- src/phases/next-encounter-phase.ts | 3 +- src/phases/quiet-form-change-phase.ts | 2 +- src/phases/show-ability-phase.ts | 4 +- src/phases/summon-phase.ts | 8 +- src/phases/switch-summon-phase.ts | 4 +- src/phases/turn-end-phase.ts | 5 +- src/system/game-data.ts | 108 +-- src/system/pokemon-data.ts | 135 ++-- src/ui/battle-info.ts | 4 +- src/ui/fight-ui-handler.ts | 15 +- src/ui/party-ui-handler.ts | 2 +- src/ui/summary-ui-handler.ts | 8 +- src/ui/target-select-ui-handler.ts | 2 +- test/abilities/cud_chew.test.ts | 280 ++++++++ test/abilities/good_as_gold.test.ts | 2 +- test/abilities/harvest.test.ts | 308 ++++++++ test/abilities/illusion.test.ts | 12 +- test/abilities/infiltrator.test.ts | 8 +- test/abilities/libero.test.ts | 16 +- test/abilities/protean.test.ts | 16 +- test/abilities/quick_draw.test.ts | 6 +- test/abilities/wimp_out.test.ts | 9 +- test/battle/inverse_battle.test.ts | 4 +- test/battlerTags/substitute.test.ts | 1 - test/moves/dive.test.ts | 2 +- test/moves/instruct.test.ts | 6 +- test/moves/order_up.test.ts | 2 +- test/moves/powder.test.ts | 2 +- test/moves/rage_fist.test.ts | 85 ++- test/moves/u_turn.test.ts | 4 +- test/testUtils/helpers/moveHelper.ts | 11 + test/testUtils/helpers/overridesHelper.ts | 15 + 51 files changed, 1680 insertions(+), 827 deletions(-) create mode 100644 test/abilities/cud_chew.test.ts create mode 100644 test/abilities/harvest.test.ts diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 17a8eddf47f..b0ced76380c 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -60,6 +60,11 @@ import { SwitchType } from "#enums/switch-type"; import { MoveFlags } from "#enums/MoveFlags"; import { MoveTarget } from "#enums/MoveTarget"; import { MoveCategory } from "#enums/MoveCategory"; +import type { BerryType } from "#enums/berry-type"; +import { CommonAnimPhase } from "#app/phases/common-anim-phase"; +import { CommonAnim } from "../battle-anims"; +import { getBerryEffectFunc } from "../berry"; +import { BerryUsedEvent } from "#app/events/battle-scene"; // Type imports import type { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; @@ -2663,7 +2668,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr { } /** - * Used by Imposter + * Attribute used by {@linkcode Abilities.IMPOSTER} to transform into a random opposing pokemon on entry. */ export class PostSummonTransformAbAttr extends PostSummonAbAttr { constructor() { @@ -2698,7 +2703,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { const targets = pokemon.getOpponents(); const target = this.getTarget(targets); - if (!!target.summonData?.illusion) { + if (target.summonData.illusion) { return false; } @@ -3727,7 +3732,7 @@ function getAnticipationCondition(): AbAttrCondition { */ function getOncePerBattleCondition(ability: Abilities): AbAttrCondition { return (pokemon: Pokemon) => { - return !pokemon.battleData?.abilitiesApplied.includes(ability); + return !pokemon.waveData.abilitiesApplied.has(ability); }; } @@ -4016,7 +4021,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr { /** * After the turn ends, resets the status of either the ability holder or their ally - * @param {boolean} allyTarget Whether to target ally, defaults to false (self-target) + * @param allyTarget Whether to target ally, defaults to false (self-target) */ export class PostTurnResetStatusAbAttr extends PostTurnAbAttr { private allyTarget: boolean; @@ -4046,26 +4051,39 @@ export class PostTurnResetStatusAbAttr extends PostTurnAbAttr { } /** - * After the turn ends, try to create an extra item + * Attribute to try and restore eaten berries after the turn ends. + * Used by {@linkcode Abilities.HARVEST}. */ -export class PostTurnLootAbAttr extends PostTurnAbAttr { +export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr { /** - * @param itemType - The type of item to create * @param procChance - Chance to create an item - * @see {@linkcode applyPostTurn()} + * @see {@linkcode createEatenBerry()} */ constructor( - /** Extend itemType to add more options */ - private itemType: "EATEN_BERRIES" | "HELD_BERRIES", private procChance: (pokemon: Pokemon) => number ) { super(); } override canApplyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { + // check if we have at least 1 recoverable berry + const cappedBerries = new Set( + globalScene.getModifiers(BerryModifier, pokemon.isPlayer()).filter( + (bm) => bm.pokemonId === pokemon.id && bm.getCountUnderMax() < 1 + ).map((bm) => bm.berryType) + ); + + const hasBerryUnderCap = pokemon.battleData.berriesEaten.some( + (bt) => !cappedBerries.has(bt) + ); + + if (!hasBerryUnderCap) { + return false; + } + // Clamp procChance to [0, 1]. Skip if didn't proc (less than pass) const pass = Phaser.Math.RND.realInRange(0, 1); - return !(Math.max(Math.min(this.procChance(pokemon), 1), 0) < pass) && this.itemType === "EATEN_BERRIES" && !!pokemon.battleData.berriesEaten; + return Math.max(Math.min(this.procChance(pokemon), 1), 0) >= pass; } override applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void { @@ -4076,10 +4094,19 @@ export class PostTurnLootAbAttr extends PostTurnAbAttr { * Create a new berry chosen randomly from the berries the pokemon ate this battle * @param pokemon The pokemon with this ability * @param simulated whether the associated ability call is simulated - * @returns whether a new berry was created + * @returns `true` if a new berry was created */ createEatenBerry(pokemon: Pokemon, simulated: boolean): boolean { - const berriesEaten = pokemon.battleData.berriesEaten; + // get all berries we just ate that are under cap + const cappedBerries = new Set( + globalScene.getModifiers(BerryModifier, pokemon.isPlayer()).filter( + (bm) => bm.pokemonId === pokemon.id && bm.getCountUnderMax() < 1 + ).map((bm) => bm.berryType) + ); + + const berriesEaten = pokemon.battleData.berriesEaten.filter( + (bt) => !cappedBerries.has(bt) + ); if (!berriesEaten.length) { return false; @@ -4089,36 +4116,98 @@ export class PostTurnLootAbAttr extends PostTurnAbAttr { return true; } + // Pick a random berry to yoink const randomIdx = randSeedInt(berriesEaten.length); const chosenBerryType = berriesEaten[randomIdx]; + pokemon.battleData.berriesEaten.splice(randomIdx, 1); // Remove berry from memory const chosenBerry = new BerryModifierType(chosenBerryType); - berriesEaten.splice(randomIdx); // Remove berry from memory + // Add the randomly chosen berry or update the existing one const berryModifier = globalScene.findModifier( - (m) => m instanceof BerryModifier && m.berryType === chosenBerryType, + (m) => m instanceof BerryModifier && m.berryType === chosenBerryType && m.pokemonId == pokemon.id, pokemon.isPlayer() ) as BerryModifier | undefined; - if (!berryModifier) { + if (berryModifier) { + berryModifier.stackCount++ + } else { + // make new modifier const newBerry = new BerryModifier(chosenBerry, pokemon.id, chosenBerryType, 1); if (pokemon.isPlayer()) { globalScene.addModifier(newBerry); } else { globalScene.addEnemyModifier(newBerry); } - } else if (berryModifier.stackCount < berryModifier.getMaxHeldItemCount(pokemon)) { - berryModifier.stackCount++; } - globalScene.queueMessage(i18next.t("abilityTriggers:postTurnLootCreateEatenBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: chosenBerry.name })); globalScene.updateModifiers(pokemon.isPlayer()); - + globalScene.queueMessage(i18next.t("abilityTriggers:postTurnLootCreateEatenBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: chosenBerry.name })); return true; } } /** - * Attribute used for {@linkcode Abilities.MOODY} + * Attribute to track and re-trigger last turn's berries at the end of the `BerryPhase`. + * Used by {@linkcode Abilities.CUD_CHEW}. +*/ +export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr { + constructor() { + super(true); + } + + /** + * @returns `true` if the pokemon ate anything last turn + */ + override canApply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean { + return !!pokemon.summonData.berriesEatenLast.length; + } + + /** + * Cause this {@linkcode Pokemon} to regurgitate and eat all berries + * inside its `berriesEatenLast` array. + * @param pokemon The pokemon having the tummy ache + * @param _passive N/A + * @param _simulated N/A + * @param _cancelled N/A + * @param _args N/A + */ + override apply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: BooleanHolder | null, _args: any[]): void { + // play berry animation + globalScene.unshiftPhase( + new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM), + ); + + // Re-apply effects of all berries previously scarfed. + // This technically doesn't count as "eating" a berry (for unnerve/stuff cheeks/unburden) + for (const berryType of pokemon.summonData.berriesEatenLast) { + getBerryEffectFunc(berryType)(pokemon); + const bMod = new BerryModifier(new BerryModifierType(berryType), pokemon.id, berryType, 1); + globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(bMod)); // trigger message + } + } + + /** + * @returns `true` if the pokemon ate anything this turn (we move it into `battleData`) + */ + override canApplyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean { + return !!pokemon.turnData.berriesEaten.length; + } + + /** + * Move this {@linkcode Pokemon}'s `berriesEaten` array inside `PokemonTurnData` + * into its `summonData`. + * @param pokemon The {@linkcode Pokemon} having a nice snack + * @param _passive N/A + * @param _simulated N/A + * @param _args N/A + */ + override applyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): void { + pokemon.summonData.berriesEatenLast = pokemon.turnData.berriesEaten; + } +} + +/** + * Attribute used for {@linkcode Abilities.MOODY} to randomly raise and lower stats at turn end. */ export class MoodyAbAttr extends PostTurnAbAttr { constructor() { @@ -4212,7 +4301,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr { } /** * Deals damage to all sleeping opponents equal to 1/8 of their max hp (min 1) - * @param pokemon Pokemon that has this ability + * @param pokemon {@linkcode Pokemon} with this ability * @param passive N/A * @param simulated `true` if applying in a simulated call. * @param args N/A @@ -4394,7 +4483,7 @@ export class PostItemLostAbAttr extends AbAttr { } /** - * Applies a Battler Tag to the Pokemon after it loses or consumes item + * Applies a Battler Tag to the Pokemon after it loses or consumes an item * @extends PostItemLostAbAttr */ export class PostItemLostApplyBattlerTagAbAttr extends PostItemLostAbAttr { @@ -4506,17 +4595,19 @@ export class HealFromBerryUseAbAttr extends AbAttr { } override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, ...args: [BooleanHolder, any[]]): void { + if (simulated) { + return; + } + const { name: abilityName } = passive ? pokemon.getPassiveAbility() : pokemon.getAbility(); - if (!simulated) { - globalScene.unshiftPhase( - new PokemonHealPhase( - pokemon.getBattlerIndex(), - toDmgValue(pokemon.getMaxHp() * this.healPercent), - i18next.t("abilityTriggers:healFromBerryUse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), - true + globalScene.unshiftPhase( + new PokemonHealPhase( + pokemon.getBattlerIndex(), + toDmgValue(pokemon.getMaxHp() * this.healPercent), + i18next.t("abilityTriggers:healFromBerryUse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), + true ) ); - } } } @@ -4548,7 +4639,8 @@ export class CheckTrappedAbAttr extends AbAttr { simulated: boolean, trapped: BooleanHolder, otherPokemon: Pokemon, - args: any[]): boolean { + args: any[], + ): boolean { return true; } @@ -5154,15 +5246,15 @@ export class IllusionPreSummonAbAttr extends PreSummonAbAttr { } override canApplyPreSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean { - pokemon.initSummondata() - if(pokemon.hasTrainer()){ + pokemon.initSummonData() + if (pokemon.hasTrainer()) { const party: Pokemon[] = (pokemon.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p.isAllowedInBattle()); const lastPokemon: Pokemon = party.filter(p => p !==pokemon).at(-1) || pokemon; const speciesId = lastPokemon.species.speciesId; // If the last conscious Pokémon in the party is a Terastallized Ogerpon or Terapagos, Illusion will not activate. // Illusion will also not activate if the Pokémon with Illusion is Terastallized and the last Pokémon in the party is Ogerpon or Terapagos. - if ( + if ( lastPokemon === pokemon || ((speciesId === Species.OGERPON || speciesId === Species.TERAPAGOS) && (lastPokemon.isTerastallized || pokemon.isTerastallized)) ) { @@ -5192,7 +5284,7 @@ export class IllusionBreakAbAttr extends PostDefendAbAttr { override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { const breakIllusion: HitResult[] = [ HitResult.EFFECTIVE, HitResult.SUPER_EFFECTIVE, HitResult.NOT_VERY_EFFECTIVE, HitResult.ONE_HIT_KO ]; - return breakIllusion.includes(hitResult) && !!pokemon.summonData?.illusion + return breakIllusion.includes(hitResult) && !!pokemon.summonData.illusion } } @@ -5413,11 +5505,8 @@ function applySingleAbAttrs( globalScene.queueAbilityDisplay(pokemon, passive, false); } - if (pokemon.summonData && !pokemon.summonData.abilitiesApplied.includes(ability.id)) { - pokemon.summonData.abilitiesApplied.push(ability.id); - } - if (pokemon.battleData && !simulated && !pokemon.battleData.abilitiesApplied.includes(ability.id)) { - pokemon.battleData.abilitiesApplied.push(ability.id); + if (!simulated) { + pokemon.waveData.abilitiesApplied.add(ability.id); } globalScene.clearPhaseQueueSplice(); @@ -6281,17 +6370,14 @@ export function applyOnLoseAbAttrs(pokemon: Pokemon, passive = false, simulated /** * Sets the ability of a Pokémon as revealed. - * * @param pokemon - The Pokémon whose ability is being revealed. */ function setAbilityRevealed(pokemon: Pokemon): void { - if (pokemon.battleData) { - pokemon.battleData.abilityRevealed = true; - } + pokemon.waveData.abilityRevealed = true; } /** - * Returns the Pokemon with weather-based forms + * Returns all Pokemon on field with weather-based forms */ function getPokemonWithWeatherBasedForms() { return globalScene.getField(true).filter(p => @@ -6741,8 +6827,7 @@ export function initAbilities() { .attr(MovePowerBoostAbAttr, (user, target, move) => move.category === MoveCategory.SPECIAL && user?.status?.effect === StatusEffect.BURN, 1.5), new Ability(Abilities.HARVEST, 5) .attr( - PostTurnLootAbAttr, - "EATEN_BERRIES", + PostTurnRestoreBerryAbAttr, /** Rate is doubled when under sun {@link https://dex.pokemonshowdown.com/abilities/harvest} */ (pokemon) => 0.5 * (getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)(pokemon) ? 2 : 1) ) @@ -6863,7 +6948,7 @@ export function initAbilities() { .attr(HealFromBerryUseAbAttr, 1 / 3), new Ability(Abilities.PROTEAN, 6) .attr(PokemonTypeChangeAbAttr), - //.condition((p) => !p.summonData?.abilitiesApplied.includes(Abilities.PROTEAN)), //Gen 9 Implementation + //.condition((p) => !p.summonData.abilitiesApplied.includes(Abilities.PROTEAN)), //Gen 9 Implementation new Ability(Abilities.FUR_COAT, 6) .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, 0.5) .ignorable(), @@ -7109,7 +7194,7 @@ export function initAbilities() { .attr(PostSummonStatStageChangeAbAttr, [ Stat.DEF ], 1, true), new Ability(Abilities.LIBERO, 8) .attr(PokemonTypeChangeAbAttr), - //.condition((p) => !p.summonData?.abilitiesApplied.includes(Abilities.LIBERO)), //Gen 9 Implementation + //.condition((p) => !p.summonData.abilitiesApplied.includes(Abilities.LIBERO)), //Gen 9 Implementation new Ability(Abilities.BALL_FETCH, 8) .attr(FetchBallAbAttr) .condition(getOncePerBattleCondition(Abilities.BALL_FETCH)), @@ -7326,7 +7411,7 @@ export function initAbilities() { new Ability(Abilities.OPPORTUNIST, 9) .attr(StatStageChangeCopyAbAttr), new Ability(Abilities.CUD_CHEW, 9) - .unimplemented(), + .attr(RepeatBerryNextTurnAbAttr), new Ability(Abilities.SHARPNESS, 9) .attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), new Ability(Abilities.SUPREME_OVERLORD, 9) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 401fd9903d1..f440e57c3c7 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1,4 +1,5 @@ import { globalScene } from "#app/global-scene"; +import Overrides from "#app/overrides"; import { applyAbAttrs, BlockNonDirectDamageAbAttr, @@ -90,7 +91,12 @@ export class BattlerTag { onOverlap(_pokemon: Pokemon): void {} + /** + * Tick down this {@linkcode BattlerTag}'s duration. + * @returns `true` if the tag should be kept (`turnCount` > 0`) + */ lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { + // TODO: Maybe flip this (return `true` if tag needs removal) return --this.turnCount > 0; } @@ -107,9 +113,9 @@ export class BattlerTag { } /** - * When given a battler tag or json representing one, load the data for it. - * This is meant to be inherited from by any battler tag with custom attributes - * @param {BattlerTag | any} source A battler tag + * Load the data for a given {@linkcode BattlerTag} or JSON representation thereof. + * Should be inherited from by any battler tag with custom attributes. + * @param source The battler tag to load */ loadTag(source: BattlerTag | any): void { this.turnCount = source.turnCount; @@ -119,7 +125,7 @@ export class BattlerTag { /** * Helper function that retrieves the source Pokemon object - * @returns The source {@linkcode Pokemon} or `null` if none is found + * @returns The source {@linkcode Pokemon}, or `null` if none is found */ public getSourcePokemon(): Pokemon | null { return this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; @@ -139,8 +145,8 @@ export interface TerrainBattlerTag { * in-game. This is not to be confused with {@linkcode Moves.DISABLE}. * * Descendants can override {@linkcode isMoveRestricted} to restrict moves that - * match a condition. A restricted move gets cancelled before it is used. Players and enemies should not be allowed - * to select restricted moves. + * match a condition. A restricted move gets cancelled before it is used. + * Players and enemies should not be allowed to select restricted moves. */ export abstract class MoveRestrictionBattlerTag extends BattlerTag { constructor( @@ -739,31 +745,33 @@ export class ConfusedTag extends BattlerTag { } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { - const ret = lapseType !== BattlerTagLapseType.CUSTOM && super.lapse(pokemon, lapseType); + const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM && super.lapse(pokemon, lapseType); - if (ret) { - globalScene.queueMessage( - i18next.t("battlerTags:confusedLapse", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - }), - ); - globalScene.unshiftPhase(new CommonAnimPhase(pokemon.getBattlerIndex(), undefined, CommonAnim.CONFUSION)); - - // 1/3 chance of hitting self with a 40 base power move - if (pokemon.randSeedInt(3) === 0) { - const atk = pokemon.getEffectiveStat(Stat.ATK); - const def = pokemon.getEffectiveStat(Stat.DEF); - const damage = toDmgValue( - ((((2 * pokemon.level) / 5 + 2) * 40 * atk) / def / 50 + 2) * (pokemon.randSeedIntRange(85, 100) / 100), - ); - globalScene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself")); - pokemon.damageAndUpdate(damage, { result: HitResult.CONFUSION }); - pokemon.battleData.hitCount++; - (globalScene.getCurrentPhase() as MovePhase).cancel(); - } + if (!shouldLapse) { + return false; } - return ret; + globalScene.queueMessage( + i18next.t("battlerTags:confusedLapse", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + }), + ); + globalScene.unshiftPhase(new CommonAnimPhase(pokemon.getBattlerIndex(), undefined, CommonAnim.CONFUSION)); + + // 1/3 chance of hitting self with a 40 base power move + if (pokemon.randSeedInt(3) === 0 || Overrides.CONFUSION_ACTIVATION_OVERRIDE === true) { + const atk = pokemon.getEffectiveStat(Stat.ATK); + const def = pokemon.getEffectiveStat(Stat.DEF); + const damage = toDmgValue( + ((((2 * pokemon.level) / 5 + 2) * 40 * atk) / def / 50 + 2) * (pokemon.randSeedIntRange(85, 100) / 100), + ); + // Intentionally don't increment rage fist's hitCount + globalScene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself")); + pokemon.damageAndUpdate(damage, { result: HitResult.CONFUSION }); + (globalScene.getCurrentPhase() as MovePhase).cancel(); + } + + return true; } getDescriptor(): string { @@ -1110,8 +1118,8 @@ export class FrenzyTag extends BattlerTag { } /** - * Applies the effects of the move Encore onto the target Pokemon - * Encore forces the target Pokemon to use its most-recent move for 3 turns + * Applies the effects of {@linkcode Moves.ENCORE} onto the target Pokemon. + * Encore forces the target Pokemon to use its most-recent move for 3 turns. */ export class EncoreTag extends MoveRestrictionBattlerTag { public moveId: Moves; @@ -1126,10 +1134,6 @@ export class EncoreTag extends MoveRestrictionBattlerTag { ); } - /** - * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag - */ loadTag(source: BattlerTag | any): void { super.loadTag(source); this.moveId = source.moveId as Moves; diff --git a/src/data/berry.ts b/src/data/berry.ts index e118b45711c..59b7671bb26 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -3,13 +3,7 @@ import type Pokemon from "../field/pokemon"; import { HitResult } from "../field/pokemon"; import { getStatusEffectHealText } from "./status-effect"; import { NumberHolder, toDmgValue, randSeedInt } from "#app/utils"; -import { - DoubleBerryEffectAbAttr, - PostItemLostAbAttr, - ReduceBerryUseThresholdAbAttr, - applyAbAttrs, - applyPostItemLostAbAttrs, -} from "./abilities/ability"; +import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs } from "./abilities/ability"; import i18next from "i18next"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -70,97 +64,94 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate { } } -export type BerryEffectFunc = (pokemon: Pokemon, berryOwner?: Pokemon) => void; +export type BerryEffectFunc = (consumer: Pokemon) => void; export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { - switch (berryType) { - case BerryType.SITRUS: - case BerryType.ENIGMA: - return (pokemon: Pokemon, berryOwner?: Pokemon) => { - if (pokemon.battleData) { - pokemon.battleData.berriesEaten.push(berryType); - } - const hpHealed = new NumberHolder(toDmgValue(pokemon.getMaxHp() / 4)); - applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed); - globalScene.unshiftPhase( - new PokemonHealPhase( - pokemon.getBattlerIndex(), - hpHealed.value, - i18next.t("battle:hpHealBerry", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - berryName: getBerryName(berryType), - }), - true, - ), - ); - applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); - }; - case BerryType.LUM: - return (pokemon: Pokemon, berryOwner?: Pokemon) => { - if (pokemon.battleData) { - pokemon.battleData.berriesEaten.push(berryType); - } - if (pokemon.status) { - globalScene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon))); - } - pokemon.resetStatus(true, true); - pokemon.updateInfo(); - applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); - }; - case BerryType.LIECHI: - case BerryType.GANLON: - case BerryType.PETAYA: - case BerryType.APICOT: - case BerryType.SALAC: - return (pokemon: Pokemon, berryOwner?: Pokemon) => { - if (pokemon.battleData) { - pokemon.battleData.berriesEaten.push(berryType); - } - // Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth - const stat: BattleStat = berryType - BerryType.ENIGMA; - const statStages = new NumberHolder(1); - applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages); - globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, [stat], statStages.value)); - applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); - }; - case BerryType.LANSAT: - return (pokemon: Pokemon, berryOwner?: Pokemon) => { - if (pokemon.battleData) { - pokemon.battleData.berriesEaten.push(berryType); - } - pokemon.addTag(BattlerTagType.CRIT_BOOST); - applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); - }; - case BerryType.STARF: - return (pokemon: Pokemon, berryOwner?: Pokemon) => { - if (pokemon.battleData) { - pokemon.battleData.berriesEaten.push(berryType); - } - const randStat = randSeedInt(Stat.SPD, Stat.ATK); - const stages = new NumberHolder(2); - applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages); - globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, [randStat], stages.value)); - applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); - }; - case BerryType.LEPPA: - return (pokemon: Pokemon, berryOwner?: Pokemon) => { - if (pokemon.battleData) { - pokemon.battleData.berriesEaten.push(berryType); - } - const ppRestoreMove = pokemon.getMoveset().find(m => !m.getPpRatio()) - ? pokemon.getMoveset().find(m => !m.getPpRatio()) - : pokemon.getMoveset().find(m => m.getPpRatio() < 1); - if (ppRestoreMove !== undefined) { - ppRestoreMove!.ppUsed = Math.max(ppRestoreMove!.ppUsed - 10, 0); - globalScene.queueMessage( - i18next.t("battle:ppHealBerry", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - moveName: ppRestoreMove!.getName(), - berryName: getBerryName(berryType), - }), + return (consumer: Pokemon) => { + // Apply an effect pertaining to what berry we're using + switch (berryType) { + case BerryType.SITRUS: + case BerryType.ENIGMA: + { + const hpHealed = new NumberHolder(toDmgValue(consumer.getMaxHp() / 4)); + applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, hpHealed); + globalScene.unshiftPhase( + new PokemonHealPhase( + consumer.getBattlerIndex(), + hpHealed.value, + i18next.t("battle:hpHealBerry", { + pokemonNameWithAffix: getPokemonNameWithAffix(consumer), + berryName: getBerryName(berryType), + }), + true, + ), ); - applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); } - }; - } + break; + case BerryType.LUM: + { + if (consumer.status) { + globalScene.queueMessage( + getStatusEffectHealText(consumer.status.effect, getPokemonNameWithAffix(consumer)), + ); + } + consumer.resetStatus(true, true); + consumer.updateInfo(); + } + break; + case BerryType.LIECHI: + case BerryType.GANLON: + case BerryType.PETAYA: + case BerryType.APICOT: + case BerryType.SALAC: + { + // Offset BerryType such that LIECHI --> Stat.ATK = 1, GANLON --> Stat.DEF = 2, etc etc. + const stat: BattleStat = berryType - BerryType.ENIGMA; + const statStages = new NumberHolder(1); + applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, statStages); + globalScene.unshiftPhase( + new StatStageChangePhase(consumer.getBattlerIndex(), true, [stat], statStages.value), + ); + } + break; + + case BerryType.LANSAT: + { + consumer.addTag(BattlerTagType.CRIT_BOOST); + } + break; + + case BerryType.STARF: + { + const randStat = randSeedInt(Stat.SPD, Stat.ATK); + const stages = new NumberHolder(2); + applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, stages); + globalScene.unshiftPhase( + new StatStageChangePhase(consumer.getBattlerIndex(), true, [randStat], stages.value), + ); + } + break; + + case BerryType.LEPPA: + { + // Pick the first move completely out of PP, or else the first one that has any PP missing + const ppRestoreMove = + consumer.getMoveset().find(m => m.ppUsed === m.getMovePp()) ?? + consumer.getMoveset().find(m => m.ppUsed < m.getMovePp()); + if (ppRestoreMove) { + ppRestoreMove.ppUsed = Math.max(ppRestoreMove.ppUsed - 10, 0); + globalScene.queueMessage( + i18next.t("battle:ppHealBerry", { + pokemonNameWithAffix: getPokemonNameWithAffix(consumer), + moveName: ppRestoreMove.getName(), + berryName: getBerryName(berryType), + }), + ); + } + } + break; + default: + console.error("Incorrect BerryType %d passed to GetBerryEffectFunc", berryType); + } + }; } diff --git a/src/data/custom-pokemon-data.ts b/src/data/custom-pokemon-data.ts index d95d9f77b83..add634c514b 100644 --- a/src/data/custom-pokemon-data.ts +++ b/src/data/custom-pokemon-data.ts @@ -4,33 +4,19 @@ import { isNullOrUndefined } from "#app/utils"; import type { Nature } from "#enums/nature"; /** - * Data that can customize a Pokemon in non-standard ways from its Species - * Used by Mystery Encounters and Mints - * Also used as a counter how often a Pokemon got hit until new arena encounter + * Data that can customize a Pokemon in non-standard ways from its Species. + * Includes abilities, nature, changed types, etc. */ export class CustomPokemonData { - public spriteScale: number; - public ability: Abilities | -1; - public passive: Abilities | -1; - public nature: Nature | -1; - public types: PokemonType[]; - /** `hitsReceivedCount` aka `hitsRecCount` saves how often the pokemon got hit until a new arena encounter (used for Rage Fist) */ - public hitsRecCount: number; + public spriteScale = -1; + public ability: Abilities | -1 = -1; + public passive: Abilities | -1 = -1; + public nature: Nature | -1 = -1; + public types: PokemonType[] = []; constructor(data?: CustomPokemonData | Partial) { if (!isNullOrUndefined(data)) { Object.assign(this, data); } - - this.spriteScale = this.spriteScale ?? -1; - this.ability = this.ability ?? -1; - this.passive = this.passive ?? -1; - this.nature = this.nature ?? -1; - this.types = this.types ?? []; - this.hitsRecCount = this.hitsRecCount ?? 0; - } - - resetHitReceivedCount(): void { - this.hitsRecCount = 0; } } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 9546a6a40e5..8c383b03ea4 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -2666,13 +2666,14 @@ export class EatBerryAttr extends MoveEffectAttr { constructor() { super(true, { trigger: MoveEffectTrigger.HIT }); } + /** * Causes the target to eat a berry. - * @param user {@linkcode Pokemon} Pokemon that used the move - * @param target {@linkcode Pokemon} Pokemon that will eat a berry - * @param move {@linkcode Move} The move being used + * @param user The {@linkcode Pokemon} Pokemon that used the move + * @param target The {@linkcode Pokemon} Pokemon that will eat the berry + * @param move The {@linkcode Move} being used * @param args Unused - * @returns {boolean} true if the function succeeds + * @returns `true` if the function succeeds */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (!super.apply(user, target, move, args)) { @@ -2681,8 +2682,11 @@ export class EatBerryAttr extends MoveEffectAttr { const heldBerries = this.getTargetHeldBerries(target); if (heldBerries.length <= 0) { + // no berries makes munchlax very sad... return false; } + + // pick a random berry to gobble and check if we preserve it this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)]; const preserve = new BooleanHolder(false); globalScene.applyModifiers(PreserveBerryModifier, target.isPlayer(), target, preserve); // check for berry pouch preservation @@ -2690,6 +2694,7 @@ export class EatBerryAttr extends MoveEffectAttr { this.reduceBerryModifier(target); } this.eatBerry(target); + return true; } @@ -2705,49 +2710,63 @@ export class EatBerryAttr extends MoveEffectAttr { globalScene.updateModifiers(target.isPlayer()); } - eatBerry(consumer: Pokemon, berryOwner?: Pokemon) { - getBerryEffectFunc(this.chosenBerry!.berryType)(consumer, berryOwner); // consumer eats the berry + eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer) { + // consumer eats berry, owner triggers unburden and similar effects + getBerryEffectFunc(this.chosenBerry!.berryType)(consumer); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner, false); + applyAbAttrs(HealFromBerryUseAbAttr, consumer, new BooleanHolder(false)); + + // Harvest doesn't track berries eaten by other pokemon + consumer.recordEatenBerry(this.chosenBerry!.berryType, berryOwner !== consumer); } } /** - * Attribute used for moves that steal a random berry from the target. The user then eats the stolen berry. - * Used for Pluck & Bug Bite. + * Attribute used for moves that steal and eat a random berry from the target. + * Used for {@linkcode Moves.PLUCK} & {@linkcode Moves.BUG_BITE}. */ export class StealEatBerryAttr extends EatBerryAttr { constructor() { super(); } + /** * User steals a random berry from the target and then eats it. - * @param {Pokemon} user Pokemon that used the move and will eat the stolen berry - * @param {Pokemon} target Pokemon that will have its berry stolen - * @param {Move} move Move being used - * @param {any[]} args Unused - * @returns {boolean} true if the function succeeds + * @param user the {@linkcode Pokemon} using the move; will eat the stolen berry + * @param target the {@linkcode Pokemon} having its berry stolen + * @param move the {@linkcode Move} being used + * @param args N/A + * @returns `true` if the function succeeds */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + // Stealing fails against substitute if (move.hitsSubstitute(user, target)) { return false; } + + // check for abilities that block item theft const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // check for abilities that block item theft + applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); if (cancelled.value === true) { return false; } + // check if the target even _has_ a berry in the first place + // TODO: Check if Pluck displays messages when used against sticky hold mons w/o berries const heldBerries = this.getTargetHeldBerries(target); if (heldBerries.length <= 0) { return false; } - // if the target has berries, pick a random berry and steal it + + // pick a random berry and eat it this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)]; applyPostItemLostAbAttrs(PostItemLostAbAttr, target, false); const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); globalScene.queueMessage(message); this.reduceBerryModifier(target); this.eatBerry(user, target); + return true; } } @@ -4119,30 +4138,23 @@ export class FriendshipPowerAttr extends VariablePowerAttr { /** * This Attribute calculates the current power of {@linkcode Moves.RAGE_FIST}. - * The counter for power calculation does not reset on every wave but on every new arena encounter + * The counter for power calculation does not reset on every wave but on every new arena encounter. + * Self-inflicted confusion damage and hits taken by a Subsitute are ignored. */ export class RageFistPowerAttr extends VariablePowerAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const { hitCount, prevHitCount } = user.battleData; + /* Reasons this works correctly: + * Confusion calls user.damageAndUpdate() directly (no counter increment), + * Substitute hits call user.damageAndUpdate() with a damage value of 0, also causing + no counter increment + */ + const hitCount = user.battleData.hitCount; const basePower: NumberHolder = args[0]; - this.updateHitReceivedCount(user, hitCount, prevHitCount); - - basePower.value = 50 + (Math.min(user.customPokemonData.hitsRecCount, 6) * 50); - + basePower.value = 50 * (1 + Math.min(hitCount, 6)); return true; } - /** - * Updates the number of hits the Pokemon has taken in battle - * @param user Pokemon calling Rage Fist - * @param hitCount The number of received hits this battle - * @param previousHitCount The number of received hits this battle since last time Rage Fist was used - */ - protected updateHitReceivedCount(user: Pokemon, hitCount: number, previousHitCount: number): void { - user.customPokemonData.hitsRecCount += (hitCount - previousHitCount); - user.battleData.prevHitCount = hitCount; - } } /** @@ -8034,7 +8046,7 @@ export class MoveCondition { export class FirstMoveCondition extends MoveCondition { constructor() { - super((user, target, move) => user.battleSummonData?.waveTurnCount === 1); + super((user, target, move) => user.summonData.waveTurnCount === 1); } getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { @@ -8676,7 +8688,7 @@ export function initMoves() { new StatusMove(Moves.TRANSFORM, PokemonType.NORMAL, -1, 10, -1, 0, 1) .attr(TransformAttr) .condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE)) - .condition((user, target, move) => !target.summonData?.illusion && !user.summonData?.illusion) + .condition((user, target, move) => !target.summonData.illusion && !user.summonData.illusion) // transforming from or into fusion pokemon causes various problems (such as crashes) .condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE) && !user.fusionSpecies && !target.fusionSpecies) .ignoresProtect(), @@ -9386,6 +9398,11 @@ export function initMoves() { new AttackMove(Moves.NATURAL_GIFT, PokemonType.NORMAL, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 4) .makesContact(false) .unimplemented(), + /* + NOTE: To whoever tries to implement this, reminder to push to battleData.berriesEaten + and enable the harvest test.. + Do NOT push to berriesEatenLast or else cud chew will puke the berry. + */ new AttackMove(Moves.FEINT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 30, 100, 10, -1, 2, 4) .attr(RemoveBattlerTagAttr, [ BattlerTagType.PROTECTED ]) .attr(RemoveArenaTagsAttr, [ ArenaTagType.QUICK_GUARD, ArenaTagType.WIDE_GUARD, ArenaTagType.MAT_BLOCK, ArenaTagType.CRAFTY_SHIELD ], false) @@ -10005,7 +10022,7 @@ export function initMoves() { .condition(new FirstMoveCondition()) .condition(failIfLastCondition), new AttackMove(Moves.BELCH, PokemonType.POISON, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 6) - .condition((user, target, move) => user.battleData.berriesEaten.length > 0), + .condition((user, target, move) => user.battleData.hasEatenBerry), new StatusMove(Moves.ROTOTILLER, PokemonType.GROUND, -1, 10, -1, 0, 6) .target(MoveTarget.ALL) .condition((user, target, move) => { @@ -11132,7 +11149,6 @@ export function initMoves() { new AttackMove(Moves.TWIN_BEAM, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 40, 100, 10, -1, 0, 9) .attr(MultiHitAttr, MultiHitType._2), new AttackMove(Moves.RAGE_FIST, PokemonType.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9) - .edgeCase() // Counter incorrectly increases on confusion self-hits .attr(RageFistPowerAttr) .punchingMove(), new AttackMove(Moves.ARMOR_CANNON, PokemonType.FIRE, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index f80620647b0..617153e98e0 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -677,7 +677,7 @@ function doPokemonTradeSequence(tradedPokemon: PlayerPokemon, receivedPokemon: P sprite.setPipelineData("shiny", tradedPokemon.shiny); sprite.setPipelineData("variant", tradedPokemon.variant); ["spriteColors", "fusionSpriteColors"].map(k => { - if (tradedPokemon.summonData?.speciesForm) { + if (tradedPokemon.summonData.speciesForm) { k += "Base"; } sprite.pipelineData[k] = tradedPokemon.getSprite().pipelineData[k]; @@ -703,7 +703,7 @@ function doPokemonTradeSequence(tradedPokemon: PlayerPokemon, receivedPokemon: P sprite.setPipelineData("shiny", receivedPokemon.shiny); sprite.setPipelineData("variant", receivedPokemon.variant); ["spriteColors", "fusionSpriteColors"].map(k => { - if (receivedPokemon.summonData?.speciesForm) { + if (receivedPokemon.summonData.speciesForm) { k += "Base"; } sprite.pipelineData[k] = receivedPokemon.getSprite().pipelineData[k]; diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index 41bf87351f4..fea41ba99e2 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -222,7 +222,7 @@ function endTrainerBattleAndShowDialogue(): Promise { globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeAbilityTrigger); } - pokemon.resetBattleData(); + pokemon.resetBattleAndWaveData(); applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); } diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index a9f6b787878..112edebe2af 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -9,7 +9,7 @@ import { import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import type { AiType, PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { EnemyPokemon, FieldPosition, PokemonMove, PokemonSummonData } from "#app/field/pokemon"; +import { EnemyPokemon, FieldPosition, PokemonMove } from "#app/field/pokemon"; import type { CustomModifierSettings, ModifierType } from "#app/modifier/modifier-type"; import { getPartyLuckValue, @@ -347,11 +347,6 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): enemyPokemon.status = new Status(status, 0, cureTurn); } - // Set summon data fields - if (!enemyPokemon.summonData) { - enemyPokemon.summonData = new PokemonSummonData(); - } - // Set ability if (!isNullOrUndefined(config.abilityIndex)) { enemyPokemon.abilityIndex = config.abilityIndex; diff --git a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts index 15085bb2bf8..954cefc99e7 100644 --- a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts +++ b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts @@ -88,7 +88,7 @@ export function doPokemonTransformationSequence( sprite.setPipelineData("shiny", previousPokemon.shiny); sprite.setPipelineData("variant", previousPokemon.variant); ["spriteColors", "fusionSpriteColors"].map(k => { - if (previousPokemon.summonData?.speciesForm) { + if (previousPokemon.summonData.speciesForm) { k += "Base"; } sprite.pipelineData[k] = previousPokemon.getSprite().pipelineData[k]; @@ -108,7 +108,7 @@ export function doPokemonTransformationSequence( sprite.setPipelineData("shiny", transformPokemon.shiny); sprite.setPipelineData("variant", transformPokemon.variant); ["spriteColors", "fusionSpriteColors"].map(k => { - if (transformPokemon.summonData?.speciesForm) { + if (transformPokemon.summonData.speciesForm) { k += "Base"; } sprite.pipelineData[k] = transformPokemon.getSprite().pipelineData[k]; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 22ede4260c3..b42f915a642 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -128,6 +128,7 @@ import { TarShotTag, AutotomizedTag, PowerTrickTag, + loadBattlerTag, } from "../data/battler-tags"; import { WeatherType } from "#enums/weather-type"; import { @@ -306,7 +307,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public luck: number; public pauseEvolutions: boolean; public pokerus: boolean; - public switchOutStatus: boolean; + public switchOutStatus = false; public evoCounter: number; public teraType: PokemonType; public isTerastallized: boolean; @@ -322,13 +323,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public fusionCustomPokemonData: CustomPokemonData | null; public fusionTeraType: PokemonType; + public customPokemonData: CustomPokemonData = new CustomPokemonData(); + + /** + * TODO: Figure out if we can remove this thing + */ private summonDataPrimer: PokemonSummonData | null; + /* Pokemon data types, in vague order of precedence */ + + /** Data that resets on switch (stat stages, battler tags, etc.) */ public summonData: PokemonSummonData; - public battleData: PokemonBattleData; - public battleSummonData: PokemonBattleSummonData; - public turnData: PokemonTurnData; - public customPokemonData: CustomPokemonData; + /** Wave data correponding to moves/ability information revealed */ + public waveData: PokemonWaveData = new PokemonWaveData; + /** Data that resets only on battle end (hit count, harvest berries, etc.) */ + public battleData: PokemonBattleData = new PokemonBattleData; + /** Per-turn data like hit count & flinch tracking */ + public turnData: PokemonTurnData = new PokemonTurnData; /** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */ public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; @@ -342,6 +353,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { private shinySparkle: Phaser.GameObjects.Sprite; + // TODO: Rework this constructor - it's _far_ too complicated and could be modernized + // in a similar manner to the PokemonData constructor constructor( x: number, y: number, @@ -362,38 +375,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { throw `Cannot create a player Pokemon for species '${species.getName(formIndex)}'`; } - const hiddenAbilityChance = new NumberHolder( - BASE_HIDDEN_ABILITY_CHANCE, - ); - if (!this.hasTrainer()) { - globalScene.applyModifiers( - HiddenAbilityRateBoosterModifier, - true, - hiddenAbilityChance, - ); - } - this.species = species; this.pokeball = dataSource?.pokeball || PokeballType.POKEBALL; this.level = level; - this.switchOutStatus = false; - // Determine the ability index - if (abilityIndex !== undefined) { - this.abilityIndex = abilityIndex; // Use the provided ability index if it is defined - } else { - // If abilityIndex is not provided, determine it based on species and hidden ability - const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); - const randAbilityIndex = randSeedInt(2); - if (species.abilityHidden && hasHiddenAbility) { - // If the species has a hidden ability and the hidden ability is present - this.abilityIndex = 2; - } else { - // If there is no hidden ability or species does not have a hidden ability - this.abilityIndex = - species.ability2 !== species.ability1 ? randAbilityIndex : 0; // Use random ability index if species has a second ability, otherwise use 0 - } - } + this.abilityIndex = abilityIndex ?? this.generateAbilityIndex() + if (formIndex !== undefined) { this.formIndex = formIndex; } @@ -409,6 +396,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.exp = dataSource?.exp || getLevelTotalExp(this.level, species.growthRate); this.levelExp = dataSource?.levelExp || 0; + + // TODO?: Maybe instead of using such a giant if statement, maybe some optional chaining/null coaclescing would look better if (dataSource) { this.id = dataSource.id; this.hp = dataSource.hp; @@ -456,6 +445,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.customPokemonData = new CustomPokemonData( dataSource.customPokemonData, ); + this.summonData = dataSource.summonData; + this.battleData = dataSource.battleData; this.teraType = dataSource.teraType; this.isTerastallized = dataSource.isTerastallized; this.stellarTypesBoosted = dataSource.stellarTypesBoosted ?? []; @@ -484,8 +475,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.variant = this.shiny ? this.generateShinyVariant() : 0; } - this.customPokemonData = new CustomPokemonData(); - if (nature !== undefined) { this.setNature(nature); } else { @@ -535,14 +524,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!dataSource) { this.calculateStats(); } + } /** * @param {boolean} useIllusion - Whether we want the fake name or the real name of the Pokemon (for Illusion ability). */ getNameToRender(useIllusion: boolean = true) { - const name: string = (!useIllusion && !!this.summonData?.illusion) ? this.summonData?.illusion.basePokemon!.name : this.name; - const nickname: string = (!useIllusion && !!this.summonData?.illusion) ? this.summonData?.illusion.basePokemon!.nickname : this.nickname; + const name: string = (!useIllusion && this.summonData.illusion) ? this.summonData.illusion.basePokemon.name : this.name; + const nickname: string = (!useIllusion && this.summonData.illusion) ? this.summonData.illusion.basePokemon.nickname : this.nickname; try { if (nickname) { return decodeURIComponent(escape(atob(nickname))); @@ -556,7 +546,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { getPokeball(useIllusion = false){ if(useIllusion){ - return this.summonData?.illusion?.pokeball ?? this.pokeball + return this.summonData.illusion?.pokeball ?? this.pokeball } else { return this.pokeball } @@ -695,11 +685,38 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } + /** Generate `abilityIndex` based on species and hidden ability if not pre-defined. */ + public generateAbilityIndex(): number { + + // Roll for hidden ability chance, applying any ability charms for enemy mons + const hiddenAbilityChance = new NumberHolder( + BASE_HIDDEN_ABILITY_CHANCE, + ); + if (!this.hasTrainer()) { + globalScene.applyModifiers( + HiddenAbilityRateBoosterModifier, + true, + hiddenAbilityChance, + ); + } + + // If the roll succeeded and we have one, use HA; otherwise pick a random ability + const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + if (this.species.abilityHidden && hasHiddenAbility) { + return 2; + } + + // only use random ability if species has a second ability + return this.species.ability2 !== this.species.ability1 ? randSeedInt(2) : 0; + } + + + /** * Generate an illusion of the last pokemon in the party, as other wild pokemon in the area. */ setIllusion(pokemon: Pokemon): boolean { - if(!!this.summonData?.illusion){ + if (this.summonData.illusion) { this.breakIllusion(); } if (this.hasTrainer()) { @@ -759,15 +776,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } breakIllusion(): boolean { - if (!this.summonData?.illusion) { + if (!this.summonData.illusion) { return false; } else { - this.name = this.summonData?.illusion.basePokemon.name; - this.nickname = this.summonData?.illusion.basePokemon.nickname; - this.shiny = this.summonData?.illusion.basePokemon.shiny; - this.variant = this.summonData?.illusion.basePokemon.variant; - this.fusionVariant = this.summonData?.illusion.basePokemon.fusionVariant; - this.fusionShiny = this.summonData?.illusion.basePokemon.fusionShiny; + this.name = this.summonData.illusion.basePokemon.name; + this.nickname = this.summonData.illusion.basePokemon.nickname; + this.shiny = this.summonData.illusion.basePokemon.shiny; + this.variant = this.summonData.illusion.basePokemon.variant; + this.fusionVariant = this.summonData.illusion.basePokemon.fusionVariant; + this.fusionShiny = this.summonData.illusion.basePokemon.fusionShiny; this.summonData.illusion = null; } if (this.isOnField()) { @@ -797,13 +814,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const loadPromises: Promise[] = []; // Assets for moves loadPromises.push(loadMoveAnimations(this.getMoveset().map(m => m.getMove().id))); - + // Load the assets for the species form - const formIndex = !!this.summonData?.illusion && useIllusion ? this.summonData?.illusion.formIndex : this.formIndex; + const formIndex = !!this.summonData.illusion && useIllusion ? this.summonData.illusion.formIndex : this.formIndex; loadPromises.push( this.getSpeciesForm(false, useIllusion).loadAssets( - this.getGender(useIllusion) === Gender.FEMALE, - formIndex, + this.getGender(useIllusion) === Gender.FEMALE, + formIndex, this.isShiny(useIllusion), this.getVariant(useIllusion) ), @@ -816,13 +833,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ); } if (this.getFusionSpeciesForm()) { - const fusionFormIndex = !!this.summonData?.illusion && useIllusion ? this.summonData?.illusion.fusionFormIndex : this.fusionFormIndex; - const fusionShiny = !!this.summonData?.illusion && !useIllusion ? this.summonData?.illusion.basePokemon!.fusionShiny : this.fusionShiny; - const fusionVariant = !!this.summonData?.illusion && !useIllusion ? this.summonData?.illusion.basePokemon!.fusionVariant : this.fusionVariant; + const fusionFormIndex = !!this.summonData.illusion && useIllusion ? this.summonData.illusion.fusionFormIndex : this.fusionFormIndex; + const fusionShiny = !!this.summonData.illusion && !useIllusion ? this.summonData.illusion.basePokemon!.fusionShiny : this.fusionShiny; + const fusionVariant = !!this.summonData.illusion && !useIllusion ? this.summonData.illusion.basePokemon!.fusionVariant : this.fusionVariant; loadPromises.push(this.getFusionSpeciesForm(false, useIllusion).loadAssets( - this.getFusionGender(false, useIllusion) === Gender.FEMALE, - fusionFormIndex, - fusionShiny, + this.getFusionGender(false, useIllusion) === Gender.FEMALE, + fusionFormIndex, + fusionShiny, fusionVariant )); globalScene.loadPokemonAtlas( @@ -871,7 +888,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // update the fusion palette this.updateFusionPalette(); - if (this.summonData?.speciesForm) { + if (this.summonData.speciesForm) { this.updateFusionPalette(true); } } @@ -981,11 +998,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } getSpriteId(ignoreOverride?: boolean): string { - const formIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.formIndex! : this.formIndex; + const formIndex: integer = !!this.summonData.illusion ? this.summonData.illusion.formIndex! : this.formIndex; return this.getSpeciesForm(ignoreOverride, true).getSpriteId( - this.getGender(ignoreOverride, true) === Gender.FEMALE, - formIndex, - this.shiny, + this.getGender(ignoreOverride, true) === Gender.FEMALE, + formIndex, + this.shiny, this.variant ); } @@ -995,13 +1012,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { back = this.isPlayer(); } - const formIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.formIndex! : this.formIndex; + const formIndex: integer = !!this.summonData.illusion ? this.summonData.illusion.formIndex! : this.formIndex; return this.getSpeciesForm(ignoreOverride, true).getSpriteId( - this.getGender(ignoreOverride, true) === Gender.FEMALE, - formIndex, - this.shiny, - this.variant, + this.getGender(ignoreOverride, true) === Gender.FEMALE, + formIndex, + this.shiny, + this.variant, back ); } @@ -1010,8 +1027,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.getSpeciesForm(ignoreOverride, false).getSpriteKey( this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, - this.summonData?.illusion?.basePokemon.shiny ?? this.shiny, - this.summonData?.illusion?.basePokemon.variant ?? this.variant + this.summonData.illusion?.basePokemon.shiny ?? this.shiny, + this.summonData.illusion?.basePokemon.variant ?? this.variant ); } @@ -1020,11 +1037,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } getFusionSpriteId(ignoreOverride?: boolean): string { - const fusionFormIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.fusionFormIndex! : this.fusionFormIndex; + const fusionFormIndex: integer = !!this.summonData.illusion ? this.summonData.illusion.fusionFormIndex! : this.fusionFormIndex; return this.getFusionSpeciesForm(ignoreOverride, true).getSpriteId( - this.getFusionGender(ignoreOverride, true) === Gender.FEMALE, - fusionFormIndex, - this.fusionShiny, + this.getFusionGender(ignoreOverride, true) === Gender.FEMALE, + fusionFormIndex, + this.fusionShiny, this.fusionVariant ); } @@ -1034,13 +1051,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { back = this.isPlayer(); } - const fusionFormIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.fusionFormIndex! : this.fusionFormIndex; + const fusionFormIndex: integer = !!this.summonData.illusion ? this.summonData.illusion.fusionFormIndex! : this.fusionFormIndex; return this.getFusionSpeciesForm(ignoreOverride, true).getSpriteId( - this.getFusionGender(ignoreOverride, true) === Gender.FEMALE, - fusionFormIndex, - this.fusionShiny, - this.fusionVariant, + this.getFusionGender(ignoreOverride, true) === Gender.FEMALE, + fusionFormIndex, + this.fusionShiny, + this.fusionVariant, back ); } @@ -1060,53 +1077,56 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } getIconAtlasKey(ignoreOverride?: boolean): string { - const formIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.formIndex : this.formIndex; + const formIndex: integer = !!this.summonData.illusion ? this.summonData.illusion.formIndex : this.formIndex; return this.getSpeciesForm(ignoreOverride, true).getIconAtlasKey( - formIndex, - this.shiny, + formIndex, + this.shiny, this.variant ); } getFusionIconAtlasKey(ignoreOverride?: boolean): string { return this.getFusionSpeciesForm(ignoreOverride, true).getIconAtlasKey( - this.fusionFormIndex, - this.fusionShiny, + this.fusionFormIndex, + this.fusionShiny, this.fusionVariant ); } getIconId(ignoreOverride?: boolean): string { - const formIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.formIndex : this.formIndex; + const formIndex: integer = !!this.summonData.illusion ? this.summonData.illusion.formIndex : this.formIndex; return this.getSpeciesForm(ignoreOverride, true).getIconId( - this.getGender(ignoreOverride, true) === Gender.FEMALE, - formIndex, - this.shiny, + this.getGender(ignoreOverride, true) === Gender.FEMALE, + formIndex, + this.shiny, this.variant ); } getFusionIconId(ignoreOverride?: boolean): string { - const fusionFormIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.fusionFormIndex! : this.fusionFormIndex; + const fusionFormIndex: integer = !!this.summonData.illusion ? this.summonData.illusion.fusionFormIndex! : this.fusionFormIndex; return this.getFusionSpeciesForm(ignoreOverride, true).getIconId( - this.getFusionGender(ignoreOverride, true) === Gender.FEMALE, - fusionFormIndex, - this.fusionShiny, + this.getFusionGender(ignoreOverride, true) === Gender.FEMALE, + fusionFormIndex, + this.fusionShiny, this.fusionVariant ); } /** - * @param {boolean} useIllusion - Whether we want the speciesForm of the illusion or not. + * Get this {@linkcode Pokemon}'s {@linkcode PokemonSpeciesForm}. + * @param ignoreOverride - ?????; default `false`. + * @param useIllusion - `true` to use the speciesForm of the illusion; default `false`. */ - getSpeciesForm(ignoreOverride?: boolean, useIllusion: boolean = false): PokemonSpeciesForm { - const species: PokemonSpecies = useIllusion && !!this.summonData?.illusion ? getPokemonSpecies(this.summonData?.illusion.species) : this.species; + getSpeciesForm(ignoreOverride: boolean = false, useIllusion: boolean = false): PokemonSpeciesForm { + const species: PokemonSpecies = useIllusion && !!this.summonData.illusion ? getPokemonSpecies(this.summonData.illusion.species) : this.species; - const formIndex: integer = useIllusion && !!this.summonData?.illusion ? this.summonData?.illusion.formIndex : this.formIndex; + const formIndex: integer = useIllusion && !!this.summonData.illusion ? this.summonData.illusion.formIndex : this.formIndex; - if (!ignoreOverride && this.summonData?.speciesForm) { + if (!ignoreOverride && this.summonData.speciesForm) { return this.summonData.speciesForm; } + if (species.forms && species.forms.length > 0) { return species.forms[formIndex]; } @@ -1118,14 +1138,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} useIllusion - Whether we want the fusionSpeciesForm of the illusion or not. */ getFusionSpeciesForm(ignoreOverride?: boolean, useIllusion: boolean = false): PokemonSpeciesForm { - const fusionSpecies: PokemonSpecies = useIllusion && !!this.summonData?.illusion ? this.summonData?.illusion.fusionSpecies! : this.fusionSpecies!; - const fusionFormIndex: integer = useIllusion && !!this.summonData?.illusion ? this.summonData?.illusion.fusionFormIndex! : this.fusionFormIndex; + const fusionSpecies: PokemonSpecies = useIllusion && !!this.summonData.illusion ? this.summonData.illusion.fusionSpecies! : this.fusionSpecies!; + const fusionFormIndex: integer = useIllusion && !!this.summonData.illusion ? this.summonData.illusion.fusionFormIndex! : this.fusionFormIndex; - if (!ignoreOverride && this.summonData?.speciesForm) { + if (!ignoreOverride && this.summonData.speciesForm) { return this.summonData.fusionSpeciesForm; } if ( - !fusionSpecies?.forms?.length || + !fusionSpecies?.forms?.length || fusionFormIndex >= fusionSpecies?.forms.length ) { return fusionSpecies; @@ -1357,7 +1377,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @returns the numeric values of the {@linkcode Pokemon}'s stats */ getStats(bypassSummonData = true): number[] { - if (!bypassSummonData && this.summonData?.stats) { + if (!bypassSummonData && this.summonData.stats) { return this.summonData.stats; } return this.stats; @@ -1389,12 +1409,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param bypassSummonData write to actual stats (`true` by default) or in-battle overridden stats (`false`) */ setStat(stat: PermanentStat, value: number, bypassSummonData = true): void { - if (value >= 0) { - if (!bypassSummonData && this.summonData) { - this.summonData.stats[stat] = value; - } else { - this.stats[stat] = value; - } + if (value < 0) { + return; + } + + if (!bypassSummonData) { + this.summonData.stats[stat] = value; + } else { + this.stats[stat] = value; } } @@ -1423,19 +1445,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param value the desired numeric value */ setStatStage(stat: BattleStat, value: number): void { - if (this.summonData) { - if (value >= -6) { - this.summonData.statStages[stat - 1] = Math.min(value, 6); - } else { - this.summonData.statStages[stat - 1] = Math.max(value, -6); - } + if (value >= -6) { + this.summonData.statStages[stat - 1] = Math.min(value, 6); + } else { + this.summonData.statStages[stat - 1] = Math.max(value, -6); } } /** * Calculate the critical-hit stage of a move used against this pokemon by * the given source - * + * * @param source the {@linkcode Pokemon} who using the move * @param move the {@linkcode Move} being used * @returns the final critical-hit stage value @@ -1790,9 +1810,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} useIllusion - Whether we want the fake or real gender (illusion ability). */ getGender(ignoreOverride?: boolean, useIllusion: boolean = false): Gender { - if (useIllusion && !!this.summonData?.illusion) { - return this.summonData?.illusion.gender!; - } else if (!ignoreOverride && this.summonData?.gender !== undefined) { + if (useIllusion && !!this.summonData.illusion) { + return this.summonData.illusion.gender!; + } else if (!ignoreOverride && this.summonData.gender !== undefined) { return this.summonData.gender; } return this.gender; @@ -1802,9 +1822,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} useIllusion - Whether we want the fake or real gender (illusion ability). */ getFusionGender(ignoreOverride?: boolean, useIllusion: boolean = false): Gender { - if (useIllusion && !!this.summonData?.illusion) { - return this.summonData?.illusion.fusionGender!; - } else if (!ignoreOverride && this.summonData?.fusionGender !== undefined) { + if (useIllusion && !!this.summonData.illusion) { + return this.summonData.illusion.fusionGender!; + } else if (!ignoreOverride && this.summonData.fusionGender !== undefined) { return this.summonData.fusionGender; } return this.fusionGender; @@ -1814,21 +1834,21 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} useIllusion - Whether we want the fake or real shininess (illusion ability). */ isShiny(useIllusion: boolean = false): boolean { - if (!useIllusion && !!this.summonData?.illusion) { - return this.summonData?.illusion.basePokemon?.shiny || (!!this.summonData?.illusion.fusionSpecies && this.summonData?.illusion.basePokemon?.fusionShiny) || false; + if (!useIllusion && !!this.summonData.illusion) { + return this.summonData.illusion.basePokemon?.shiny || (!!this.summonData.illusion.fusionSpecies && this.summonData.illusion.basePokemon?.fusionShiny) || false; } else { return this.shiny || (this.isFusion(useIllusion) && this.fusionShiny); } } /** - * + * * @param useIllusion - Whether we want the fake or real shininess (illusion ability). * @returns `true` if the {@linkcode Pokemon} is shiny and the fusion is shiny as well, `false` otherwise */ isDoubleShiny(useIllusion: boolean = false): boolean { - if (!useIllusion && !!this.summonData?.illusion) { - return this.isFusion(false) && this.summonData?.illusion.basePokemon.shiny && this.summonData?.illusion.basePokemon.fusionShiny; + if (!useIllusion && !!this.summonData.illusion) { + return this.isFusion(false) && this.summonData.illusion.basePokemon.shiny && this.summonData.illusion.basePokemon.fusionShiny; } else { return this.isFusion(useIllusion) && this.shiny && this.fusionShiny; } @@ -1838,21 +1858,21 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} useIllusion - Whether we want the fake or real variant (illusion ability). */ getVariant(useIllusion: boolean = false): Variant { - if (!useIllusion && !!this.summonData?.illusion) { - return !this.isFusion(false) - ? this.summonData?.illusion.basePokemon!.variant + if (!useIllusion && !!this.summonData.illusion) { + return !this.isFusion(false) + ? this.summonData.illusion.basePokemon!.variant : Math.max(this.variant, this.fusionVariant) as Variant; } else { - return !this.isFusion(true) - ? this.variant + return !this.isFusion(true) + ? this.variant : Math.max(this.variant, this.fusionVariant) as Variant; } } getBaseVariant(doubleShiny: boolean): Variant { if (doubleShiny) { - return !!this.summonData?.illusion - ? this.summonData?.illusion.basePokemon!.variant + return !!this.summonData.illusion + ? this.summonData.illusion.basePokemon!.variant : this.variant; } else { return this.getVariant(); @@ -1864,8 +1884,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } isFusion(useIllusion: boolean = false): boolean { - if (useIllusion && !!this.summonData?.illusion) { - return !!this.summonData?.illusion.fusionSpecies; + if (useIllusion && !!this.summonData.illusion) { + return !!this.summonData.illusion.fusionSpecies; } else { return !!this.fusionSpecies; } @@ -1875,8 +1895,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} useIllusion - Whether we want the fake name or the real name of the Pokemon (for Illusion ability). */ getName(useIllusion: boolean = false): string { - return (!useIllusion && !!this.summonData?.illusion && this.summonData?.illusion.basePokemon) - ? this.summonData?.illusion.basePokemon.name + return (!useIllusion && !!this.summonData.illusion && this.summonData.illusion.basePokemon) + ? this.summonData.illusion.basePokemon.name : this.name; } @@ -1910,7 +1930,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { getMoveset(ignoreOverride?: boolean): PokemonMove[] { const ret = - !ignoreOverride && this.summonData?.moveset + !ignoreOverride && this.summonData.moveset ? this.summonData.moveset : this.moveset; @@ -1996,9 +2016,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @returns array of {@linkcode PokemonType} */ public getTypes( - includeTeraType = false, - forDefend: boolean = false, - ignoreOverride?: boolean, + includeTeraType = false, + forDefend: boolean = false, + ignoreOverride?: boolean, useIllusion: boolean | "AUTO" = "AUTO" ): PokemonType[] { const types: PokemonType[] = []; @@ -2017,10 +2037,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const doIllusion: boolean = (useIllusion === "AUTO") ? !forDefend : useIllusion; if ( - !ignoreOverride && - this.summonData?.types && - this.summonData.types.length > 0 && - (!this.summonData?.illusion || !doIllusion) + !ignoreOverride && + this.summonData.types && + this.summonData.types.length > 0 && + (!this.summonData.illusion || !doIllusion) ) { this.summonData.types.forEach(t => types.push(t)); } else { @@ -2147,7 +2167,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @returns The non-passive {@linkcode Ability} of the pokemon */ public getAbility(ignoreOverride = false): Ability { - if (!ignoreOverride && this.summonData?.ability) { + if (!ignoreOverride && this.summonData.ability) { return allAbilities[this.summonData.ability]; } if (Overrides.ABILITY_OVERRIDE && this.isPlayer()) { @@ -2213,8 +2233,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Accounts for all the various effects which can affect whether an ability will be present or * in effect, and both passive and non-passive. * @param attrType - {@linkcode AbAttr} The ability attribute to check for. - * @param canApply - If `false`, it doesn't check whether the ability is currently active; Default `true` - * @param ignoreOverride - If `true`, it ignores ability changing effects; Default `false` + * @param canApply - Whether to check if the ability is currently active; Default `true` + * @param ignoreOverride - Whether to ignore ability changing effects; Default `false` * @returns An array of all the ability attributes on this ability. */ public getAbilityAttrs( @@ -2326,7 +2346,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } if ( - this.summonData?.abilitySuppressed && + this.summonData.abilitySuppressed && ability.isSuppressable ) { return false; @@ -2367,15 +2387,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Checks whether a pokemon has the specified ability and it's in effect. Accounts for all the various * effects which can affect whether an ability will be present or in effect, and both passive and * non-passive. This is the primary way to check whether a pokemon has a particular ability. - * @param {Abilities} ability The ability to check for - * @param {boolean} canApply If false, it doesn't check whether the ability is currently active - * @param {boolean} ignoreOverride If true, it ignores ability changing effects - * @returns {boolean} Whether the ability is present and active + * @param ability The ability to check for + * @param canApply - Whether to check if the ability is currently active; default `true` + * @param ignoreOverride Whether to ignore ability changing effects; default `false` + * @returns `true` if the ability is present and active */ public hasAbility( ability: Abilities, canApply = true, - ignoreOverride?: boolean, + ignoreOverride = false, ): boolean { if ( this.getAbility(ignoreOverride).id === ability && @@ -2398,15 +2418,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Accounts for all the various effects which can affect whether an ability will be present or * in effect, and both passive and non-passive. This is one of the two primary ways to check * whether a pokemon has a particular ability. - * @param {AbAttr} attrType The ability attribute to check for - * @param {boolean} canApply If false, it doesn't check whether the ability is currently active - * @param {boolean} ignoreOverride If true, it ignores ability changing effects - * @returns {boolean} Whether an ability with that attribute is present and active + * @param attrType The {@link AbAttr | ability attribute} to check for + * @param canApply - Whether to check if the ability is currently active; default `true` + * @param ignoreOverride Whether to ignore ability changing effects; default `false` + * @returns `true` if an ability with the given {@linkcode AbAttr} is present and active */ public hasAbilityWithAttr( attrType: Constructor, canApply = true, - ignoreOverride?: boolean, + ignoreOverride = false, ): boolean { if ( (!canApply || this.canApplyAbility()) && @@ -2600,14 +2620,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const moveType = source.getMoveType(move); const typeMultiplier = new NumberHolder( - move.category !== MoveCategory.STATUS || + move.category !== MoveCategory.STATUS || move.hasAttr(RespectAttackTypeImmunityAttr) ? this.getAttackTypeEffectiveness( - moveType, - source, - false, - simulated, - move, + moveType, + source, + false, + simulated, + move, useIllusion ) : 1); @@ -2718,11 +2738,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @returns a multiplier for the type effectiveness */ getAttackTypeEffectiveness( - moveType: PokemonType, - source?: Pokemon, - ignoreStrongWinds: boolean = false, - simulated: boolean = true, - move?: Move, + moveType: PokemonType, + source?: Pokemon, + ignoreStrongWinds: boolean = false, + simulated: boolean = true, + move?: Move, useIllusion: boolean = false ): TypeDamageMultiplier { if (moveType === PokemonType.STELLAR) { @@ -3102,7 +3122,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } const move = new PokemonMove(moveId); this.moveset[moveIndex] = move; - if (this.summonData?.moveset) { + if (this.summonData.moveset) { this.summonData.moveset[moveIndex] = move; } } @@ -3841,7 +3861,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { getOpponent(targetIndex: number): Pokemon | null { const ret = this.getOpponents()[targetIndex]; - if (ret.summonData) { + if (ret.summonData) { // TODO: why does this check for summonData and can we remove it? return ret; } return null; @@ -4666,12 +4686,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * We explicitly require to ignore the faint phase here, as we want to show the messages * about the critical hit and the super effective/not very effective messages before the faint phase. */ - const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, - { - result: result as DamageResult, - isCritical, - ignoreFaintPhase: true, - source + const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, + { + result: result as DamageResult, + isCritical, + ignoreFaintPhase: true, + source }); if (damage > 0) { @@ -4752,9 +4772,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Called by damageAndUpdate() * @param damage integer * @param ignoreSegments boolean, not currently used - * @param preventEndure used to update damage if endure or sturdy - * @param ignoreFaintPhase flag on wheter to add FaintPhase if pokemon after applying damage faints - * @returns integer representing damage + * @param preventEndure used to update damage if endure or sturdy + * @param ignoreFaintPhas flag on whether to add FaintPhase if pokemon after applying damage faints + * @returns integer representing damage dealt */ damage( damage: number, @@ -4767,6 +4787,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } const surviveDamage = new BooleanHolder(false); + // check for endure and other abilities that would prevent us from death if (!preventEndure && this.hp - damage <= 0) { if (this.hp >= 1 && this.getTag(BattlerTagType.ENDURING)) { surviveDamage.value = this.lapseTag(BattlerTagType.ENDURING); @@ -4822,25 +4843,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { */ damageAndUpdate(damage: number, { - result = HitResult.EFFECTIVE, - isCritical = false, - ignoreSegments = false, - ignoreFaintPhase = false, + result = HitResult.EFFECTIVE, + isCritical = false, + ignoreSegments = false, + ignoreFaintPhase = false, source = undefined, }: { - result?: DamageResult, - isCritical?: boolean, - ignoreSegments?: boolean, - ignoreFaintPhase?: boolean, + result?: DamageResult, + isCritical?: boolean, + ignoreSegments?: boolean, + ignoreFaintPhase?: boolean, source?: Pokemon, } = {} ): number { const isIndirectDamage = [ HitResult.INDIRECT, HitResult.INDIRECT_KO ].includes(result); const damagePhase = new DamageAnimPhase( - this.getBattlerIndex(), - damage, - result as DamageResult, + this.getBattlerIndex(), + damage, + result as DamageResult, isCritical ); globalScene.unshiftPhase(damagePhase); @@ -4973,51 +4994,52 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** @overload */ - getTag(tagType: BattlerTagType): BattlerTag | nil; + getTag(tagType: BattlerTagType): BattlerTag | undefined; /** @overload */ - getTag(tagType: Constructor): T | nil; + getTag(tagType: Constructor): T | undefined; - getTag(tagType: BattlerTagType | Constructor): BattlerTag | nil { - if (!this.summonData) { - return null; - } + getTag(tagType: BattlerTagType | Constructor): BattlerTag | undefined { return tagType instanceof Function ? this.summonData.tags.find(t => t instanceof tagType) : this.summonData.tags.find(t => t.tagType === tagType); } findTag(tagFilter: (tag: BattlerTag) => boolean) { - if (!this.summonData) { - return null; - } return this.summonData.tags.find(t => tagFilter(t)); } findTags(tagFilter: (tag: BattlerTag) => boolean): BattlerTag[] { - if (!this.summonData) { - return []; - } return this.summonData.tags.filter(t => tagFilter(t)); } + /** + * Tick down the first {@linkcode BattlerTag} found matching the given {@linkcode BattlerTagType}, + * removing it if its duration goes below 0. + * @param tagType the {@linkcode BattlerTagType} to check against + * @returns `true` if the tag was present + */ lapseTag(tagType: BattlerTagType): boolean { - if (!this.summonData) { - return false; - } const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); - if (tag && !tag.lapse(this, BattlerTagLapseType.CUSTOM)) { + if (!tag) { + return false + } + + if (!tag.lapse(this, BattlerTagLapseType.CUSTOM)) { tag.onRemove(this); tags.splice(tags.indexOf(tag), 1); } - return !!tag; + return true } + /** + * Tick down all {@linkcode BattlerTags} matching the given {@linkcode BattlerTagLapseType}, + * removing any whose durations fall below 0. + * @param tagType the {@linkcode BattlerTagLapseType} to tick down + */ + lapseTags(lapseType: BattlerTagLapseType): void { - if (!this.summonData) { - return; - } const tags = this.summonData.tags; tags .filter( @@ -5032,23 +5054,24 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }); } - removeTag(tagType: BattlerTagType): boolean { - if (!this.summonData) { - return false; - } + /** + * Remove the first tag matching the given {@linkcode BattlerTagType}. + * @param tagType the {@linkcode BattlerTagType} to search for and remove + */ + removeTag(tagType: BattlerTagType): void { const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (tag) { tag.onRemove(this); tags.splice(tags.indexOf(tag), 1); } - return !!tag; } - findAndRemoveTags(tagFilter: (tag: BattlerTag) => boolean): boolean { - if (!this.summonData) { - return false; - } + /** + * Find and remove all {@linkcode BattlerTag}s matching the given function. + * @param tagFilter a function dictating which tags to remove + */ + findAndRemoveTags(tagFilter: (tag: BattlerTag) => boolean): void { const tags = this.summonData.tags; const tagsToRemove = tags.filter(t => tagFilter(t)); for (const tag of tagsToRemove) { @@ -5056,7 +5079,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { tag.onRemove(this); tags.splice(tags.indexOf(tag), 1); } - return true; } removeTagsBySourceId(sourceId: number): void { @@ -5186,7 +5208,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } public getMoveHistory(): TurnMove[] { - return this.battleSummonData.moveHistory; + return this.summonData.moveHistory; } public pushMoveHistory(turnMove: TurnMove): void { @@ -5510,6 +5532,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ): boolean { if (effect !== StatusEffect.FAINT) { if (overrideStatus ? this.status?.effect === effect : this.status) { + // Only 1 non-volatile status per pokemon (subsequent attempts always fail) return false; } if ( @@ -5517,6 +5540,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { !ignoreField && globalScene.arena.terrain?.terrainType === TerrainType.MISTY ) { + // Misty terrain prevents status application return false; } } @@ -5526,6 +5550,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { sourcePokemon !== this && this.isSafeguarded(sourcePokemon) ) { + // Safeguard blocks all non-self inflicted status effects return false; } @@ -5534,14 +5559,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { switch (effect) { case StatusEffect.POISON: case StatusEffect.TOXIC: - // Check if the Pokemon is immune to Poison/Toxic or if the source pokemon is canceling the immunity - const poisonImmunity = types.map(defType => { - // Check if the Pokemon is not immune to Poison/Toxic + // Check whether any of the Pokemon's types is immune to Poison/Toxic + // and not being ignored by the source pokemon's ability + const typeImmune = types.some(defType => { if (defType !== PokemonType.POISON && defType !== PokemonType.STEEL) { + // type not immune to poison return false; } - // Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity + // Check if the source Pokemon has an ability that cancels the immunity const cancelImmunity = new BooleanHolder(false); if (sourcePokemon) { applyAbAttrs( @@ -5552,18 +5578,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { effect, defType, ); - if (cancelImmunity.value) { - return false; - } } - return true; + return !cancelImmunity.value; }); - if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) { - if (poisonImmunity.includes(true)) { - return false; - } + if (typeImmune) { + return false; } break; case StatusEffect.PARALYSIS: @@ -5598,6 +5619,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { break; } + // Check any status immunity abilities from the user or its allies const cancelled = new BooleanHolder(false); applyPreSetStatusAbAttrs( StatusEffectImmunityAbAttr, @@ -5619,14 +5641,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { quiet, this, sourcePokemon, ) if (cancelled.value) { - break; + return false; } } - if (cancelled.value) { - return false; - } - return true; } @@ -5737,22 +5755,32 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } // For PreSummonAbAttr to get access to summonData - initSummondata(): void { + initSummonData(): void { this.summonData = this.summonData ?? this.summonDataPrimer ?? new PokemonSummonData() } resetSummonData(): void { - const illusion: IllusionData | null = this.summonData?.illusion; - if (this.summonData?.speciesForm) { + const illusion: IllusionData | null = this.summonData.illusion; + if (this.summonData.speciesForm) { this.summonData.speciesForm = null; this.updateFusionPalette(); } this.summonData = new PokemonSummonData(); this.setSwitchOutStatus(false); if (!this.battleData) { - this.resetBattleData(); + this.resetBattleAndWaveData(); } - this.resetBattleSummonData(); + if (this.getTag(BattlerTagType.SEEDED)) { + this.lapseTag(BattlerTagType.SEEDED); + } + if (globalScene) { + globalScene.triggerPokemonFormChange( + this, + SpeciesFormChangePostMoveTrigger, + true, + ); + } + if (this.summonDataPrimer) { for (const k of Object.keys(this.summonDataPrimer)) { if (this.summonDataPrimer[k]) { @@ -5782,22 +5810,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.updateInfo(); } - resetBattleData(): void { + /** + Reset a {@linkcode Pokemon}'s per-battle {@linkcode PokemonBattleData | battleData}, + as well as any transient {@linkcode PokemonWaveData | waveData} for the current wave. + Called before a new battle starts. + */ + resetBattleAndWaveData(): void { this.battleData = new PokemonBattleData(); + this.resetWaveData(); } - resetBattleSummonData(): void { - this.battleSummonData = new PokemonBattleSummonData(); - if (this.getTag(BattlerTagType.SEEDED)) { - this.lapseTag(BattlerTagType.SEEDED); - } - if (globalScene) { - globalScene.triggerPokemonFormChange( - this, - SpeciesFormChangePostMoveTrigger, - true, - ); - } + /** + Reset a {@linkcode Pokemon}'s {@linkcode PokemonWaveData | waveData}. + Called once per new wave start as well as by {@linkcode resetBattleAndWaveData}. + */ + resetWaveData(): void { + this.waveData = new PokemonWaveData(); } resetTera(): void { @@ -5918,10 +5946,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { .filter(s => !!s) .map(s => { s.pipelineData[ - `spriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}` + `spriteColors${ignoreOveride && this.summonData.speciesForm ? "Base" : ""}` ] = []; s.pipelineData[ - `fusionSpriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}` + `fusionSpriteColors${ignoreOveride && this.summonData.speciesForm ? "Base" : ""}` ] = []; }); return; @@ -6278,10 +6306,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { .filter(s => !!s) .map(s => { s.pipelineData[ - `spriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}` + `spriteColors${ignoreOveride && this.summonData.speciesForm ? "Base" : ""}` ] = spriteColors; s.pipelineData[ - `fusionSpriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}` + `fusionSpriteColors${ignoreOveride && this.summonData.speciesForm ? "Base" : ""}` ] = fusionSpriteColors; }); @@ -6334,7 +6362,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (clearEffects) { this.destroySubstitute(); - this.resetSummonData(); // this also calls `resetBattleSummonData` + this.resetSummonData(); } if (hideInfo) { this.hideInfo(); @@ -6404,7 +6432,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { heldItem: PokemonHeldItemModifier, forBattle = true, ): boolean { - if (heldItem.pokemonId === -1 || heldItem.pokemonId === this.id) { + if (heldItem.pokemonId !== -1 && heldItem.pokemonId !== this.id) { + return false; + } + heldItem.stackCount--; if (heldItem.stackCount <= 0) { globalScene.removeModifier(heldItem, !this.isPlayer()); @@ -6412,10 +6443,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (forBattle) { applyPostItemLostAbAttrs(PostItemLostAbAttr, this, false); } + return true; - } else { - return false; + } + + /** + * Record a berry being eaten for ability and move triggers. + * Only tracks things that proc _every_ time a berry is eaten. + * @param berryType The type of berry being eaten. + * @param updateHarvest Whether to track the berry for harvest; default `true`. + */ + public recordEatenBerry(berryType: BerryType, updateHarvest: boolean = true) { + this.battleData.hasEatenBerry = true; + if (updateHarvest) { + // Only track for harvest if we actually consumed the berry + this.battleData.berriesEaten.push(berryType) } + this.turnData.berriesEaten.push(berryType); } } @@ -6984,6 +7028,8 @@ export class PlayerPokemon extends Pokemon { if (partyMemberIndex > fusedPartyMemberIndex) { partyMemberIndex--; } + + // combine the two mons' held items const fusedPartyMemberHeldModifiers = globalScene.findModifiers( m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, true, @@ -7288,9 +7334,9 @@ export class EnemyPokemon extends Pokemon { p.getAttackDamage( this, move, - !p.battleData.abilityRevealed, + !p.waveData.abilityRevealed, false, - !p.getAlly()?.battleData.abilityRevealed, + !p.getAlly()?.waveData.abilityRevealed, false, isCritical, ).damage >= p.hp @@ -7351,11 +7397,18 @@ export class EnemyPokemon extends Pokemon { ) { targetScore = -20; } else if (move instanceof AttackMove) { - /** - * Attack moves are given extra multipliers to their base benefit score based on - * the move's type effectiveness against the target and whether the move is a STAB move. - */ - const effectiveness = target.getMoveEffectiveness(this, move, !target.battleData?.abilityRevealed, undefined, undefined, true); + /** + * Attack moves are given extra multipliers to their base benefit score based on + * the move's type effectiveness against the target and whether the move is a STAB move. + */ + const effectiveness = target.getMoveEffectiveness( + this, + move, + !target.waveData.abilityRevealed, + undefined, + undefined, + true); + if (target.isPlayer() !== this.isPlayer()) { targetScore *= effectiveness; if (this.isOfType(move.type)) { @@ -7798,13 +7851,16 @@ export interface AttackMoveResult { sourceBattlerIndex: BattlerIndex; } +/** +Persistent in-battle data for a {@linkcode Pokemon}. +Resets on switch or new battle. +*/ export class PokemonSummonData { /** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */ public statStages: number[] = [0, 0, 0, 0, 0, 0, 0]; public moveQueue: TurnMove[] = []; public tags: BattlerTag[] = []; public abilitySuppressed = false; - public abilitiesApplied: Abilities[] = []; public speciesForm: PokemonSpeciesForm | null; public fusionSpeciesForm: PokemonSpeciesForm; public ability: Abilities = Abilities.NONE; @@ -7818,21 +7874,13 @@ export class PokemonSummonData { // If not initialized this value will not be populated from save data. public types: PokemonType[] = []; public addedType: PokemonType | null = null; + + /** Data pertaining to this pokemon's illusion. */ public illusion: IllusionData | null = null; -} -export class PokemonBattleData { - /** counts the hits the pokemon received */ - public hitCount = 0; - /** used for {@linkcode Moves.RAGE_FIST} in order to save hit Counts received before Rage Fist is applied */ - public prevHitCount = 0; - public endured = false; - public berriesEaten: BerryType[] = []; - public abilitiesApplied: Abilities[] = []; - public abilityRevealed: boolean = false; -} + /** Array containing all berries eaten in the last turn; used by {@linkcode Abilities.CUD_CHEW} */ + public berriesEatenLast: BerryType[] = []; -export class PokemonBattleSummonData { /** The number of turns the pokemon has passed since entering the battle */ public turnCount = 1; /** The number of turns the pokemon has passed since the start of the wave */ @@ -7841,6 +7889,38 @@ export class PokemonBattleSummonData { public moveHistory: TurnMove[] = []; } +/** +Persistent data for a {@linkcode Pokemon}. +Resets at the start of a new battle (but not on switch). +*/ +export class PokemonBattleData { + /** counts the hits the pokemon received during this battle; used for {@linkcode Moves.RAGE_FIST} */ + public hitCount = 0; + /** Whether this has eaten a berry this battle; used for {@linkcode Moves.BELCH} */ + public hasEatenBerry: boolean = false; + /** A list of all berries eaten in this current battle; used by {@linkcode Abilities.HARVEST} */ + public berriesEaten: BerryType[] = []; +} + +/** +Temporary data for a {@linkcode Pokemon}. +Resets on new wave. +*/ +export class PokemonWaveData { + /** whether the pokemon has endured due to a {@linkcode BattlerTagType.ENDURE_TOKEN} */ + public endured = false; + /** + A set of all the abilities this {@linkcode Pokemon} has used in this wave. + Used to track once per battle conditions, as well as (hopefully) by the updated AI. + */ + public abilitiesApplied: Set = new Set; + public abilityRevealed = false; +} + +/** +Temporary data for a {@linkcode Pokemon}. +Resets at the start of a new turn. +*/ export class PokemonTurnData { public flinched = false; public acted = false; @@ -7868,6 +7948,12 @@ export class PokemonTurnData { * forced to act again in the same turn */ public extraTurns = 0; + /** + * All berries eaten by this pokemon in this turn. + * Saved into {@linkcode PokemonBattleData | BattleData} by {@linkcode Pe at turn end. + * @see {@linkcode PokemonsummonData.berriesEatenLast} + */ + public berriesEaten: BerryType[] = [] } export enum AiType { @@ -7905,8 +7991,8 @@ export type DamageResult = | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.ONE_HIT_KO - | HitResult.CONFUSION - | HitResult.INDIRECT_KO + | HitResult.CONFUSION + | HitResult.INDIRECT_KO | HitResult.INDIRECT; /** Interface containing the results of a damage calculation for a given move */ @@ -7923,8 +8009,8 @@ export interface DamageCalculationResult { * Wrapper class for the {@linkcode Move} class for Pokemon to interact with. * These are the moves assigned to a {@linkcode Pokemon} object. * It links to {@linkcode Move} class via the move ID. - * Compared to {@linkcode Move}, this class also tracks if a move has received. - * PP Ups, amount of PP used, and things like that. + * Compared to {@linkcode Move}, this class also tracks things like + * PP Ups recieved, PP used, etc. * @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented. * @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID. * @see {@linkcode usePp} - removes a point of PP from the move. @@ -7995,9 +8081,9 @@ export class PokemonMove { /** * Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp} - * @param {number} count Amount of PP to use + * @param count Amount of PP to use */ - usePp(count = 1) { + usePp(count: number = 1) { this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp()); } @@ -8017,9 +8103,9 @@ export class PokemonMove { } /** - * Copies an existing move or creates a valid PokemonMove object from json representing one - * @param {PokemonMove | any} source The data for the move to copy - * @return {PokemonMove} A valid pokemonmove object + * Copies an existing move or creates a valid {@linkcode PokemonMove} object from json representing one + * @param source The data for the move to copy; can be a {@linkcode PokemonMove} or JSON object representing one + * @returns A valid {@linkcode PokemonMove} object */ static loadMove(source: PokemonMove | any): PokemonMove { return new PokemonMove( diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 851fa33cedc..590da7ce586 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -47,7 +47,12 @@ import { } from "./modifier-type"; import { Color, ShadowColor } from "#enums/color"; import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters"; -import { applyAbAttrs, CommanderAbAttr } from "#app/data/abilities/ability"; +import { + applyAbAttrs, + applyPostItemLostAbAttrs, + CommanderAbAttr, + PostItemLostAbAttr, +} from "#app/data/abilities/ability"; import { globalScene } from "#app/global-scene"; export type ModifierPredicate = (modifier: Modifier) => boolean; @@ -1644,8 +1649,8 @@ export class FlinchChanceModifier extends PokemonHeldItemModifier { * @returns `true` if {@linkcode FlinchChanceModifier} has been applied */ override apply(pokemon: Pokemon, flinched: BooleanHolder): boolean { - // The check for pokemon.battleSummonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch - if (pokemon.battleSummonData && !flinched.value && pokemon.randSeedInt(100) < this.getStackCount() * this.chance) { + // The check for pokemon.summonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch + if (pokemon.summonData && !flinched.value && pokemon.randSeedInt(100) < this.getStackCount() * this.chance) { flinched.value = true; return true; } @@ -1866,11 +1871,15 @@ export class BerryModifier extends PokemonHeldItemModifier { override apply(pokemon: Pokemon): boolean { const preserve = new BooleanHolder(false); globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve); + this.consumed = !preserve.value; + // munch the berry and trigger unburden-like effects getBerryEffectFunc(this.berryType)(pokemon); - if (!preserve.value) { - this.consumed = true; - } + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + + // Update berry eaten trackers for Belch, Harvest, Cud Chew, etc. + // Don't recover it if we proc berry pouch (no item duplication) + pokemon.recordEatenBerry(this.berryType, this.consumed); return true; } @@ -1909,9 +1918,7 @@ export class PreserveBerryModifier extends PersistentModifier { * @returns always `true` */ override apply(pokemon: Pokemon, doPreserve: BooleanHolder): boolean { - if (!doPreserve.value) { - doPreserve.value = pokemon.randSeedInt(10) < this.getStackCount() * 3; - } + doPreserve.value ||= pokemon.randSeedInt(10) < this.getStackCount() * 3; return true; } @@ -3608,7 +3615,7 @@ export class EnemyAttackStatusEffectChanceModifier extends EnemyPersistentModifi super(type, stackCount); this.effect = effect; - //Hardcode temporarily + // Hardcode temporarily this.chance = 0.025 * (this.effect === StatusEffect.BURN || this.effect === StatusEffect.POISON ? 2 : 1); } @@ -3715,13 +3722,13 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { * @returns `true` if {@linkcode Pokemon} endured */ override apply(target: Pokemon): boolean { - if (target.battleData.endured || target.randSeedInt(100) >= this.chance * this.getStackCount()) { + if (target.waveData.endured || target.randSeedInt(100) >= this.chance * this.getStackCount()) { return false; } target.addTag(BattlerTagType.ENDURE_TOKEN, 1); - target.battleData.endured = true; + target.waveData.endured = true; return true; } diff --git a/src/overrides.ts b/src/overrides.ts index 21c72cd7b98..dc3aa0e9ce1 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -102,8 +102,16 @@ class DefaultOverrides { readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false; /** Set to `true` to be able to re-earn already unlocked achievements */ readonly ACHIEVEMENTS_REUNLOCK_OVERRIDE: boolean = false; - /** Set to `true` to force Paralysis and Freeze to always activate, or `false` to force them to not activate */ + /** + * Set to `true` to force Paralysis and Freeze to always activate, + * or `false` to force them to not activate (or clear for freeze). + */ readonly STATUS_ACTIVATION_OVERRIDE: boolean | null = null; + /** + * Set to `true` to force confusion to always trigger, + * or `false` to force it to never trigger. + */ + readonly CONFUSION_ACTIVATION_OVERRIDE: boolean|null = null; // ---------------- // PLAYER OVERRIDES diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index 275a9017dfa..b2f7867466c 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -58,9 +58,10 @@ export class BattleEndPhase extends BattlePhase { globalScene.unshiftPhase(new GameOverPhase(true)); } + // reset pokemon wave turn count, apply post battle effects, etc etc. for (const pokemon of globalScene.getField()) { - if (pokemon?.battleSummonData) { - pokemon.battleSummonData.waveTurnCount = 1; + if (pokemon?.summonData) { + pokemon.summonData.waveTurnCount = 1; } } @@ -81,6 +82,7 @@ export class BattleEndPhase extends BattlePhase { } } + // lapse all post battle modifiers that should lapse const lapsingModifiers = globalScene.findModifiers( m => m instanceof LapsingPersistentModifier || m instanceof LapsingPokemonHeldItemModifier, ) as (LapsingPersistentModifier | LapsingPokemonHeldItemModifier)[]; diff --git a/src/phases/berry-phase.ts b/src/phases/berry-phase.ts index ae593f66f34..31f0edd878f 100644 --- a/src/phases/berry-phase.ts +++ b/src/phases/berry-phase.ts @@ -1,4 +1,9 @@ -import { applyAbAttrs, PreventBerryUseAbAttr, HealFromBerryUseAbAttr } from "#app/data/abilities/ability"; +import { + applyAbAttrs, + PreventBerryUseAbAttr, + HealFromBerryUseAbAttr, + RepeatBerryNextTurnAbAttr, +} from "#app/data/abilities/ability"; import { CommonAnim } from "#app/data/battle-anims"; import { BerryUsedEvent } from "#app/events/battle-scene"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -8,6 +13,7 @@ import { BooleanHolder } from "#app/utils"; import { FieldPhase } from "./field-phase"; import { CommonAnimPhase } from "./common-anim-phase"; import { globalScene } from "#app/global-scene"; +import type Pokemon from "#app/field/pokemon"; /** The phase after attacks where the pokemon eat berries */ export class BerryPhase extends FieldPhase { @@ -15,40 +21,57 @@ export class BerryPhase extends FieldPhase { super.start(); this.executeForAll(pokemon => { - const hasUsableBerry = !!globalScene.findModifier(m => { - return m instanceof BerryModifier && m.shouldApply(pokemon); - }, pokemon.isPlayer()); - - if (hasUsableBerry) { - const cancelled = new BooleanHolder(false); - pokemon.getOpponents().map(opp => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled)); - - if (cancelled.value) { - globalScene.queueMessage( - i18next.t("abilityTriggers:preventBerryUse", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - }), - ); - } else { - globalScene.unshiftPhase( - new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM), - ); - - for (const berryModifier of globalScene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon)) { - if (berryModifier.consumed) { - berryModifier.consumed = false; - pokemon.loseHeldItem(berryModifier); - } - globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); // Announce a berry was used - } - - globalScene.updateModifiers(pokemon.isPlayer()); - - applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new BooleanHolder(false)); - } - } + this.eatBerries(pokemon); + applyAbAttrs(RepeatBerryNextTurnAbAttr, pokemon, null); }); this.end(); } + + /** + * Attempt to eat all of a given {@linkcode Pokemon}'s berries once. + * @param pokemon The {@linkcode Pokemon} to check + */ + eatBerries(pokemon: Pokemon): void { + // check if we even have anything to eat + const hasUsableBerry = !!globalScene.findModifier(m => { + return m instanceof BerryModifier && m.shouldApply(pokemon); + }, pokemon.isPlayer()); + if (!hasUsableBerry) { + return; + } + + // Check if any opponents have unnerve to block us from eating berries + const cancelled = new BooleanHolder(false); + pokemon.getOpponents().map(opp => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled)); + if (cancelled.value) { + globalScene.queueMessage( + i18next.t("abilityTriggers:preventBerryUse", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + }), + ); + return; + } + + // Play every endless player's least favorite animation + globalScene.unshiftPhase( + new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM), + ); + + // try to apply all berry modifiers for this pokemon + for (const berryModifier of globalScene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon)) { + if (berryModifier.consumed) { + berryModifier.consumed = false; + pokemon.loseHeldItem(berryModifier); + } + // No need to track berries being eaten; already done inside applyModifiers + globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); + } + + // update held modifiers and such + globalScene.updateModifiers(pokemon.isPlayer()); + + // Abilities.CHEEK_POUCH only works once per round of nom noms + applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new BooleanHolder(false)); + } } diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 67236c1c041..86ac6843c6e 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -1,7 +1,12 @@ import { BattlerIndex, BattleType } from "#app/battle"; import { globalScene } from "#app/global-scene"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; -import { applyAbAttrs, SyncEncounterNatureAbAttr, applyPreSummonAbAttrs, PreSummonAbAttr } from "#app/data/abilities/ability"; +import { + applyAbAttrs, + SyncEncounterNatureAbAttr, + applyPreSummonAbAttrs, + PreSummonAbAttr, +} from "#app/data/abilities/ability"; import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims"; import { getCharVariantFromDialogue } from "#app/data/dialogue"; import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; @@ -107,12 +112,6 @@ export class EncounterPhase extends BattlePhase { } if (!this.loaded) { if (battle.battleType === BattleType.TRAINER) { - //resets hitRecCount during Trainer ecnounter - for (const pokemon of globalScene.getPlayerParty()) { - if (pokemon) { - pokemon.customPokemonData.resetHitReceivedCount(); - } - } battle.enemyParty[e] = battle.trainer?.genPartyMember(e)!; // TODO:: is the bang correct here? } else { let enemySpecies = globalScene.randomSpecies(battle.waveIndex, level, true); @@ -134,7 +133,6 @@ export class EncounterPhase extends BattlePhase { if (globalScene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { battle.enemyParty[e].ivs = new Array(6).fill(31); } - // biome-ignore lint/complexity/noForEach: Improves readability globalScene .getPlayerParty() .slice(0, !battle.double ? 1 : 2) @@ -336,7 +334,7 @@ export class EncounterPhase extends BattlePhase { for (const pokemon of globalScene.getPlayerParty()) { if (pokemon) { - pokemon.resetBattleData(); + pokemon.resetBattleAndWaveData(); } } diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index 203c7542eff..e7cb3f6da5d 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -146,7 +146,7 @@ export class EvolutionPhase extends Phase { sprite.setPipelineData("shiny", this.pokemon.shiny); sprite.setPipelineData("variant", this.pokemon.variant); ["spriteColors", "fusionSpriteColors"].map(k => { - if (this.pokemon.summonData?.speciesForm) { + if (this.pokemon.summonData.speciesForm) { k += "Base"; } sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k]; @@ -178,7 +178,7 @@ export class EvolutionPhase extends Phase { sprite.setPipelineData("shiny", evolvedPokemon.shiny); sprite.setPipelineData("variant", evolvedPokemon.variant); ["spriteColors", "fusionSpriteColors"].map(k => { - if (evolvedPokemon.summonData?.speciesForm) { + if (evolvedPokemon.summonData.speciesForm) { k += "Base"; } sprite.pipelineData[k] = evolvedPokemon.getSprite().pipelineData[k]; diff --git a/src/phases/form-change-phase.ts b/src/phases/form-change-phase.ts index bf94284b117..2107da53a8c 100644 --- a/src/phases/form-change-phase.ts +++ b/src/phases/form-change-phase.ts @@ -51,7 +51,7 @@ export class FormChangePhase extends EvolutionPhase { sprite.setPipelineData("shiny", transformedPokemon.shiny); sprite.setPipelineData("variant", transformedPokemon.variant); ["spriteColors", "fusionSpriteColors"].map(k => { - if (transformedPokemon.summonData?.speciesForm) { + if (transformedPokemon.summonData.speciesForm) { k += "Base"; } sprite.pipelineData[k] = transformedPokemon.getSprite().pipelineData[k]; diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index dc394b8a134..41f74f67467 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -186,7 +186,7 @@ export class MovePhase extends BattlePhase { this.lapsePreMoveAndMoveTags(); - if (!(this.failed || this.cancelled)) { + if (!this.failed && !this.cancelled) { this.resolveFinalPreMoveCancellationChecks(); } @@ -617,7 +617,7 @@ export class MovePhase extends BattlePhase { globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed)); } - if (this.cancelled && this.pokemon.summonData?.tags?.find(t => t.tagType === BattlerTagType.FRENZY)) { + if (this.cancelled && this.pokemon.summonData.tags?.find(t => t.tagType === BattlerTagType.FRENZY)) { frenzyMissFunc(this.pokemon, this.move.getMove()); } diff --git a/src/phases/new-biome-encounter-phase.ts b/src/phases/new-biome-encounter-phase.ts index 6a7afcb8da8..55ab8701183 100644 --- a/src/phases/new-biome-encounter-phase.ts +++ b/src/phases/new-biome-encounter-phase.ts @@ -7,17 +7,16 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { doEncounter(): void { globalScene.playBgm(undefined, true); + // reset all battle data, perform form changes, etc. for (const pokemon of globalScene.getPlayerParty()) { if (pokemon) { - pokemon.resetBattleData(); - pokemon.customPokemonData.resetHitReceivedCount(); + pokemon.resetBattleAndWaveData(); + if (pokemon.isOnField()) { + applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null); + } } } - for (const pokemon of globalScene.getPlayerParty().filter(p => p.isOnField())) { - applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null); - } - const enemyField = globalScene.getEnemyField(); const moveTargets: any[] = [globalScene.arenaEnemy, enemyField]; const mysteryEncounter = globalScene.currentBattle?.mysteryEncounter?.introVisuals; diff --git a/src/phases/next-encounter-phase.ts b/src/phases/next-encounter-phase.ts index e5e61312c3b..4bfe86ad672 100644 --- a/src/phases/next-encounter-phase.ts +++ b/src/phases/next-encounter-phase.ts @@ -9,9 +9,10 @@ export class NextEncounterPhase extends EncounterPhase { doEncounter(): void { globalScene.playBgm(undefined, true); + // Reset all player transient wave data/intel. for (const pokemon of globalScene.getPlayerParty()) { if (pokemon) { - pokemon.resetBattleData(); + pokemon.resetWaveData(); } } diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index f476919a628..76411f62f77 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -74,7 +74,7 @@ export class QuietFormChangePhase extends BattlePhase { isTerastallized: this.pokemon.isTerastallized, }); ["spriteColors", "fusionSpriteColors"].map(k => { - if (this.pokemon.summonData?.speciesForm) { + if (this.pokemon.summonData.speciesForm) { k += "Base"; } sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k]; diff --git a/src/phases/show-ability-phase.ts b/src/phases/show-ability-phase.ts index 8097af33fe0..d6193ac3946 100644 --- a/src/phases/show-ability-phase.ts +++ b/src/phases/show-ability-phase.ts @@ -50,9 +50,7 @@ export class ShowAbilityPhase extends PokemonPhase { } globalScene.abilityBar.showAbility(this.pokemonName, this.abilityName, this.passive, this.player).then(() => { - if (pokemon?.battleData) { - pokemon.battleData.abilityRevealed = true; - } + pokemon.waveData.abilityRevealed = true; this.end(); }); diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index 60d45f19c0c..5e9a3e8417e 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -177,11 +177,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { } globalScene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); } - addPokeballOpenParticles( - pokemon.x, - pokemon.y - 16, - pokemon.getPokeball(true), - ); + addPokeballOpenParticles(pokemon.x, pokemon.y - 16, pokemon.getPokeball(true)); globalScene.updateModifiers(this.player); globalScene.updateFieldScale(); pokemon.showInfo(); @@ -202,7 +198,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { pokemon.getSprite().clearTint(); pokemon.resetSummonData(); // necessary to stay transformed during wild waves - if (pokemon.summonData?.speciesForm) { + if (pokemon.summonData.speciesForm) { pokemon.loadAssets(false); } globalScene.time.delayedCall(1000, () => this.end()); diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index f8728f3f9b9..c96b617c18c 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -227,8 +227,8 @@ export class SwitchSummonPhase extends SummonPhase { lastPokemonIsForceSwitchedAndNotFainted || lastPokemonHasForceSwitchAbAttr ) { - pokemon.battleSummonData.turnCount--; - pokemon.battleSummonData.waveTurnCount--; + pokemon.summonData.turnCount--; + pokemon.summonData.waveTurnCount--; } if (this.switchType === SwitchType.BATON_PASS && pokemon) { diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index fe16a4a864e..c7868324209 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -54,11 +54,10 @@ export class TurnEndPhase extends FieldPhase { } globalScene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon); - globalScene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon); - pokemon.battleSummonData.turnCount++; - pokemon.battleSummonData.waveTurnCount++; + pokemon.summonData.turnCount++; + pokemon.summonData.waveTurnCount++; }; this.executeForAll(handlePokemon); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 53146301666..119f45c177e 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1141,7 +1141,7 @@ export class GameData { ? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE : sessionData.enemyParty.length > 1, mysteryEncounterType, - )!; // TODO: is this bang correct? + ); battle.enemyLevels = sessionData.enemyParty.map(p => p.level); globalScene.arena.init(); @@ -1338,68 +1338,68 @@ export class GameData { } parseSessionData(dataStr: string): SessionSaveData { + // TODO: Add add `null`/`undefined` to the corresponding type signatures for this + // (or prevent them from being null) + // If the value is able to *not exist*, it should say so in the code + const sessionData = JSON.parse(dataStr, (k: string, v: any) => { - if (k === "party" || k === "enemyParty") { - const ret: PokemonData[] = []; - if (v === null) { - v = []; - } - for (const pd of v) { - ret.push(new PokemonData(pd)); - } - return ret; - } - - if (k === "trainer") { - return v ? new TrainerData(v) : null; - } - - if (k === "modifiers" || k === "enemyModifiers") { - const player = k === "modifiers"; - const ret: PersistentModifierData[] = []; - if (v === null) { - v = []; - } - for (const md of v) { - if (md?.className === "ExpBalanceModifier") { - // Temporarily limit EXP Balance until it gets reworked - md.stackCount = Math.min(md.stackCount, 4); + // TODO: Move this into migrate script + switch (k) { + case "party": + case "enemyParty": { + const ret: PokemonData[] = []; + for (const pd of v ?? []) { + ret.push(new PokemonData(pd)); } - if ( - (md instanceof Modifier.EnemyAttackStatusEffectChanceModifier && md.effect === StatusEffect.FREEZE) || - md.effect === StatusEffect.SLEEP - ) { - continue; + return ret; + } + + case "trainer": + return v ? new TrainerData(v) : null; + + case "modifiers": + case "enemyModifiers": { + const ret: PersistentModifierData[] = []; + for (const md of v ?? []) { + if (md?.className === "ExpBalanceModifier") { + // Temporarily limit EXP Balance until it gets reworked + md.stackCount = Math.min(md.stackCount, 4); + } + + if ( + md instanceof Modifier.EnemyAttackStatusEffectChanceModifier && + (md.effect === StatusEffect.FREEZE || md.effect === StatusEffect.SLEEP) + ) { + // Discard any old "sleep/freeze chance tokens". + // TODO: make this migrate script + continue; + } + + ret.push(new PersistentModifierData(md, k === "modifiers")); } - ret.push(new PersistentModifierData(md, player)); + return ret; } - return ret; - } - if (k === "arena") { - return new ArenaData(v); - } + case "arena": + return new ArenaData(v); - if (k === "challenges") { - const ret: ChallengeData[] = []; - if (v === null) { - v = []; + case "challenges": { + const ret: ChallengeData[] = []; + for (const c of v ?? []) { + ret.push(new ChallengeData(c)); + } + return ret; } - for (const c of v) { - ret.push(new ChallengeData(c)); - } - return ret; - } - if (k === "mysteryEncounterType") { - return v as MysteryEncounterType; - } + case "mysteryEncounterType": + return v as MysteryEncounterType; - if (k === "mysteryEncounterSaveData") { - return new MysteryEncounterSaveData(v); - } + case "mysteryEncounterSaveData": + return new MysteryEncounterSaveData(v); - return v; + default: + return v; + } }) as SessionSaveData; applySessionVersionMigration(sessionData); @@ -1430,7 +1430,7 @@ export class GameData { const systemData = useCachedSystem ? this.parseSystemData(decrypt(localStorage.getItem(`data_${loggedInUser?.username}`)!, bypassLogin)) : this.getSystemSaveData(); // TODO: is this bang correct? - + const request = { system: systemData, session: sessionData, diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 97ce494a43a..2977e4b637c 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -1,19 +1,19 @@ import { BattleType } from "../battle"; import { globalScene } from "#app/global-scene"; import type { Gender } from "../data/gender"; -import type { Nature } from "#enums/nature"; +import { Nature } from "#enums/nature"; import type { PokeballType } from "#enums/pokeball"; import { getPokemonSpecies, getPokemonSpeciesForm } from "../data/pokemon-species"; import { Status } from "../data/status-effect"; -import Pokemon, { EnemyPokemon, PokemonMove, PokemonSummonData } from "../field/pokemon"; +import Pokemon, { EnemyPokemon, PokemonMove, PokemonSummonData, type PokemonBattleData } from "../field/pokemon"; import { TrainerSlot } from "#enums/trainer-slot"; import type { Variant } from "#app/sprites/variant"; -import { loadBattlerTag } from "../data/battler-tags"; import type { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import type { Species } from "#enums/species"; import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import type { PokemonType } from "#enums/pokemon-type"; +import { loadBattlerTag } from "#app/data/battler-tags"; export default class PokemonData { public id: number; @@ -62,72 +62,67 @@ export default class PokemonData { public boss: boolean; public bossSegments?: number; + // Effects that need to be preserved between waves public summonData: PokemonSummonData; + public battleData: PokemonBattleData; public summonDataSpeciesFormIndex: number; - /** Data that can customize a Pokemon in non-standard ways from its Species */ public customPokemonData: CustomPokemonData; public fusionCustomPokemonData: CustomPokemonData; - // Deprecated attributes, needed for now to allow SessionData migration (see PR#4619 comments) + // Deprecated attributes, needed for now to allow SessionData migration (see PR#4619 comments). + // TODO: These can probably be safely deleted (what with the upgrade scripts and all) public natureOverride: Nature | -1; public mysteryEncounterPokemonData: CustomPokemonData | null; public fusionMysteryEncounterPokemonData: CustomPokemonData | null; + /** + * Construct a new {@linkcode PokemonData} instance out of a {@linkcode Pokemon} + * or JSON representation thereof. + * @param source The {@linkcode Pokemon} to convert into data (or a JSON object representing one) + * @param forHistory + */ constructor(source: Pokemon | any, forHistory = false) { - const sourcePokemon = source instanceof Pokemon ? source : null; + const sourcePokemon = source instanceof Pokemon ? source : undefined; this.id = source.id; this.player = sourcePokemon ? sourcePokemon.isPlayer() : source.player; this.species = sourcePokemon ? sourcePokemon.species.speciesId : source.species; - this.nickname = sourcePokemon - ? (!!sourcePokemon.summonData?.illusion ? sourcePokemon.summonData.illusion.basePokemon.nickname : sourcePokemon.nickname) - : source.nickname; + this.nickname = + sourcePokemon?.summonData.illusion?.basePokemon.nickname ?? sourcePokemon?.nickname ?? source.nickname; this.formIndex = Math.max(Math.min(source.formIndex, getPokemonSpecies(this.species).forms.length - 1), 0); this.abilityIndex = source.abilityIndex; this.passive = source.passive; - this.shiny = sourcePokemon ? sourcePokemon.isShiny() : source.shiny; - this.variant = sourcePokemon ? sourcePokemon.getVariant() : source.variant; + this.shiny = sourcePokemon?.isShiny() ?? source.shiny; + this.variant = sourcePokemon?.getVariant() ?? source.variant; this.pokeball = source.pokeball; this.level = source.level; this.exp = source.exp; - if (!forHistory) { - this.levelExp = source.levelExp; - } this.gender = source.gender; - if (!forHistory) { - this.hp = source.hp; - } this.stats = source.stats; this.ivs = source.ivs; - this.nature = source.nature !== undefined ? source.nature : (0 as Nature); - this.friendship = - source.friendship !== undefined ? source.friendship : getPokemonSpecies(this.species).baseFriendship; + this.nature = source.nature ?? Nature.HARDY; + this.friendship = source.friendship ?? getPokemonSpecies(this.species).baseFriendship; this.metLevel = source.metLevel || 5; - this.metBiome = source.metBiome !== undefined ? source.metBiome : -1; + this.metBiome = source.metBiome ?? -1; this.metSpecies = source.metSpecies; this.metWave = source.metWave ?? (this.metBiome === -1 ? -1 : 0); - this.luck = source.luck !== undefined ? source.luck : source.shiny ? source.variant + 1 : 0; - if (!forHistory) { - this.pauseEvolutions = !!source.pauseEvolutions; - this.evoCounter = source.evoCounter ?? 0; - } + this.luck = source.luck ?? (source.shiny ? source.variant + 1 : 0); this.pokerus = !!source.pokerus; this.teraType = source.teraType as PokemonType; - this.isTerastallized = source.isTerastallized || false; - this.stellarTypesBoosted = source.stellarTypesBoosted || []; + this.isTerastallized = !!source.isTerastallized; + this.stellarTypesBoosted = source.stellarTypesBoosted ?? []; this.fusionSpecies = sourcePokemon ? sourcePokemon.fusionSpecies?.speciesId : source.fusionSpecies; this.fusionFormIndex = source.fusionFormIndex; this.fusionAbilityIndex = source.fusionAbilityIndex; - this.fusionShiny = sourcePokemon - ? (!!sourcePokemon.summonData?.illusion ? sourcePokemon.summonData.illusion.basePokemon.fusionShiny : sourcePokemon.fusionShiny) - : source.fusionShiny; - this.fusionVariant = sourcePokemon - ? (!!sourcePokemon.summonData?.illusion ? sourcePokemon.summonData.illusion.basePokemon.fusionVariant : sourcePokemon.fusionVariant) - : source.fusionVariant; + this.fusionShiny = + sourcePokemon?.summonData.illusion?.basePokemon.fusionShiny ?? sourcePokemon?.fusionShiny ?? source.fusionShiny; + this.fusionVariant = + sourcePokemon?.summonData.illusion?.basePokemon.fusionVariant ?? + sourcePokemon?.fusionVariant ?? + source.fusionVariant; this.fusionGender = source.fusionGender; - this.fusionLuck = - source.fusionLuck !== undefined ? source.fusionLuck : source.fusionShiny ? source.fusionVariant + 1 : 0; + this.fusionLuck = source.fusionLuck ?? (source.fusionShiny ? source.fusionVariant + 1 : 0); this.fusionCustomPokemonData = new CustomPokemonData(source.fusionCustomPokemonData); this.fusionTeraType = (source.fusionTeraType ?? 0) as PokemonType; this.usedTMs = source.usedTMs ?? []; @@ -135,6 +130,7 @@ export default class PokemonData { this.customPokemonData = new CustomPokemonData(source.customPokemonData); // Deprecated, but needed for session data migration + // TODO: Do we really need this?? this.natureOverride = source.natureOverride; this.mysteryEncounterPokemonData = source.mysteryEncounterPokemonData ? new CustomPokemonData(source.mysteryEncounterPokemonData) @@ -143,51 +139,44 @@ export default class PokemonData { ? new CustomPokemonData(source.fusionMysteryEncounterPokemonData) : null; + this.moveset = + sourcePokemon?.moveset ?? + (source.moveset || [new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL)]) + .filter((m: any) => !!m) + .map((m: any) => new PokemonMove(m.moveId, m.ppUsed, m.ppUp, m.virtual, m.maxPpOverride)); + if (!forHistory) { + this.levelExp = source.levelExp; + this.hp = source.hp; + + this.pauseEvolutions = !!source.pauseEvolutions; + this.evoCounter = source.evoCounter ?? 0; + this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss); this.bossSegments = source.bossSegments; - } - - if (sourcePokemon) { - this.moveset = sourcePokemon.moveset; - if (!forHistory) { - this.status = sourcePokemon.status; - if (this.player && sourcePokemon.summonData) { - this.summonData = sourcePokemon.summonData; - this.summonDataSpeciesFormIndex = this.getSummonDataSpeciesFormIndex(); - } - } - } 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, m.virtual, m.maxPpOverride)); - if (!forHistory) { - this.status = source.status + this.status = + sourcePokemon?.status ?? + (source.status ? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining) - : null; + : null); + + // enemy pokemon don't use instantized summon data + if (this.player) { + this.summonData = sourcePokemon?.summonData ?? source.summonData; + } else { + console.log("this.player false!"); + this.summonData = new PokemonSummonData(); } - this.summonData = new PokemonSummonData(); - if (!forHistory && source.summonData) { - this.summonData.stats = source.summonData.stats; - this.summonData.statStages = source.summonData.statStages; - this.summonData.moveQueue = source.summonData.moveQueue; - this.summonData.abilitySuppressed = source.summonData.abilitySuppressed; - this.summonData.abilitiesApplied = source.summonData.abilitiesApplied; - - this.summonData.ability = source.summonData.ability; + if (!sourcePokemon) { this.summonData.moveset = source.summonData.moveset?.map(m => PokemonMove.loadMove(m)); - this.summonData.types = source.summonData.types; - this.summonData.speciesForm = source.summonData.speciesForm; - this.summonDataSpeciesFormIndex = source.summonDataSpeciesFormIndex; - this.summonData.illusionBroken = source.summonData.illusionBroken; - - if (source.summonData.tags) { - this.summonData.tags = source.summonData.tags?.map(t => loadBattlerTag(t)); - } else { - this.summonData.tags = []; - } + this.summonData.tags = source.tags.map((t: any) => loadBattlerTag(t)); } + + this.summonDataSpeciesFormIndex = sourcePokemon + ? this.getSummonDataSpeciesFormIndex() + : source.summonDataSpeciesFormIndex; + this.battleData = sourcePokemon?.battleData ?? source.battleData; } } diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 06c5f7fb3f1..d33ed5f1bd3 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -617,7 +617,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { return resolve(); } - const gender: Gender = !!pokemon.summonData?.illusion ? pokemon.summonData?.illusion.gender : pokemon.gender; + const gender = pokemon.summonData.illusion?.gender ?? pokemon.gender; this.genderText.setText(getGenderSymbol(gender)); this.genderText.setColor(getGenderColor(gender)); @@ -794,7 +794,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { const nameSizeTest = addTextObject(0, 0, displayName, TextStyle.BATTLE_INFO); nameTextWidth = nameSizeTest.displayWidth; - const gender: Gender = !!pokemon.summonData?.illusion ? pokemon.summonData?.illusion.gender : pokemon.gender; + const gender = pokemon.summonData?.illusion?.gender ?? pokemon.gender; while ( nameTextWidth > (this.player || !this.boss ? 60 : 98) - diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index 27985629e3d..6afe40f9932 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -127,7 +127,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle { messageHandler.commandWindow.setVisible(false); messageHandler.movesWindowContainer.setVisible(true); const pokemon = (globalScene.getCurrentPhase() as CommandPhase).getPokemon(); - if (pokemon.battleSummonData.turnCount <= 1) { + if (pokemon.summonData.turnCount <= 1) { this.setCursor(0); } else { this.setCursor(this.getCursor()); @@ -305,10 +305,10 @@ export default class FightUiHandler extends UiHandler implements InfoToggle { const effectiveness = opponent.getMoveEffectiveness( pokemon, pokemonMove.getMove(), - !opponent.battleData?.abilityRevealed, + !opponent.waveData.abilityRevealed, undefined, undefined, - true + true, ); if (effectiveness === undefined) { return undefined; @@ -353,7 +353,14 @@ export default class FightUiHandler extends UiHandler implements InfoToggle { const moveColors = opponents .map(opponent => - opponent.getMoveEffectiveness(pokemon, pokemonMove.getMove(), !opponent.battleData.abilityRevealed, undefined, undefined, true), + opponent.getMoveEffectiveness( + pokemon, + pokemonMove.getMove(), + !opponent.waveData.abilityRevealed, + undefined, + undefined, + true, + ), ) .sort((a, b) => b - a) .map(effectiveness => getTypeDamageMultiplierColor(effectiveness ?? 0, "offense")); diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index ba90108c274..301b598ac8e 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -1581,7 +1581,7 @@ class PartySlot extends Phaser.GameObjects.Container { fusionShinyStar.setOrigin(0, 0); fusionShinyStar.setPosition(shinyStar.x, shinyStar.y); fusionShinyStar.setTint( - getVariantTint(this.pokemon.summonData?.illusion?.basePokemon.fusionVariant ?? this.pokemon.fusionVariant), + getVariantTint(this.pokemon.summonData.illusion?.basePokemon.fusionVariant ?? this.pokemon.fusionVariant), ); slotInfoContainer.add(fusionShinyStar); diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 5ff4a02793d..5d658188b7e 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -359,15 +359,15 @@ export default class SummaryUiHandler extends UiHandler { this.pokemonSprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey()); this.pokemonSprite.setPipelineData( "shiny", - this.pokemon.summonData?.illusion?.basePokemon.shiny ?? this.pokemon.shiny, + this.pokemon.summonData.illusion?.basePokemon.shiny ?? this.pokemon.shiny, ); this.pokemonSprite.setPipelineData( "variant", - this.pokemon.summonData?.illusion?.basePokemon.variant ?? this.pokemon.variant, + this.pokemon.summonData.illusion?.basePokemon.variant ?? this.pokemon.variant, ); ["spriteColors", "fusionSpriteColors"].map(k => { delete this.pokemonSprite.pipelineData[`${k}Base`]; - if (this.pokemon?.summonData?.speciesForm) { + if (this.pokemon?.summonData.speciesForm) { k += "Base"; } this.pokemonSprite.pipelineData[k] = this.pokemon?.getSprite().pipelineData[k]; @@ -462,7 +462,7 @@ export default class SummaryUiHandler extends UiHandler { this.fusionShinyIcon.setVisible(doubleShiny); if (isFusion) { this.fusionShinyIcon.setTint( - getVariantTint(this.pokemon.summonData?.illusion?.basePokemon.fusionVariant ?? this.pokemon.fusionVariant), + getVariantTint(this.pokemon.summonData.illusion?.basePokemon.fusionVariant ?? this.pokemon.fusionVariant), ); } diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index a9f88b337f3..6b40e565c7a 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -71,7 +71,7 @@ export default class TargetSelectUiHandler extends UiHandler { */ resetCursor(cursorN: number, user: Pokemon): void { if (!isNullOrUndefined(cursorN)) { - if ([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2].includes(cursorN) || user.battleSummonData.waveTurnCount === 1) { + if ([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2].includes(cursorN) || user.summonData.waveTurnCount === 1) { // Reset cursor on the first turn of a fight or if an ally was targeted last turn cursorN = -1; } diff --git a/test/abilities/cud_chew.test.ts b/test/abilities/cud_chew.test.ts new file mode 100644 index 00000000000..6b3d55b7943 --- /dev/null +++ b/test/abilities/cud_chew.test.ts @@ -0,0 +1,280 @@ +import { RepeatBerryNextTurnAbAttr } from "#app/data/abilities/ability"; +import { getBerryEffectFunc } from "#app/data/berry"; +import Pokemon from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { BerryType } from "#enums/berry-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Cud Chew", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.resetAllMocks(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([Moves.BUG_BITE, Moves.SPLASH, Moves.HYPER_VOICE, Moves.STUFF_CHEEKS]) + .startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }]) + .ability(Abilities.CUD_CHEW) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + describe("tracks berries eaten", () => { + it("stores inside battledata at end of turn", async () => { + await game.classicMode.startBattle([Species.FARIGIRAF]); + + const farigiraf = game.scene.getPlayerPokemon()!; + farigiraf.hp = 1; // needed to allow sitrus procs + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + // berries tracked in turnData; not moved to battleData yet + expect(farigiraf.summonData.berriesEatenLast).toEqual([]); + expect(farigiraf.turnData.berriesEaten).toEqual([BerryType.SITRUS]); + + await game.phaseInterceptor.to("TurnEndPhase"); + + // berries stored in battleData; not yet cleared from turnData + expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]); + expect(farigiraf.turnData.berriesEaten).toEqual([BerryType.SITRUS]); + + await game.toNextTurn(); + + // turnData cleared on turn start + expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]); + expect(farigiraf.turnData.berriesEaten).toEqual([]); + }); + + it("can store multiple berries across 2 turns with teatime", async () => { + // always eat first berry for stuff cheeks & company + vi.spyOn(Pokemon.prototype, "randSeedInt").mockReturnValue(0); + game.override + .startingHeldItems([ + { name: "BERRY", type: BerryType.PETAYA, count: 3 }, + { name: "BERRY", type: BerryType.LIECHI, count: 3 }, + ]) + .enemyMoveset(Moves.TEATIME); + await game.classicMode.startBattle([Species.FARIGIRAF]); + + const farigiraf = game.scene.getPlayerPokemon()!; + farigiraf.hp = 1; // needed to allow berry procs + + game.move.select(Moves.STUFF_CHEEKS); + await game.toNextTurn(); + + // Ate 2 petayas from moves + 1 of each at turn end + expect(farigiraf.summonData.berriesEatenLast).toEqual([ + BerryType.PETAYA, + BerryType.PETAYA, + BerryType.PETAYA, + BerryType.LIECHI, + ]); + expect(farigiraf.turnData.berriesEaten).toEqual([]); + + game.move.select(Moves.STUFF_CHEEKS); + await game.toNextTurn(); + + // previous berries moved into summon data; newly eaten berries move into turn data + expect(farigiraf.summonData.berriesEatenLast).toEqual([ + BerryType.PETAYA, + BerryType.PETAYA, + BerryType.PETAYA, + BerryType.LIECHI, + ]); + expect(farigiraf.turnData.berriesEaten).toEqual([BerryType.PETAYA, BerryType.LIECHI, BerryType.LIECHI]); + expect(farigiraf.getStatStage(Stat.ATK)).toBe(4); // 1 --> 2+1 + + await game.toNextTurn(); + + // 1st array overridden after turn end + expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.PETAYA, BerryType.LIECHI, BerryType.LIECHI]); + }); + + it("resets array on switch", async () => { + await game.classicMode.startBattle([Species.FARIGIRAF, Species.GIRAFARIG]); + + const farigiraf = game.scene.getPlayerPokemon()!; + farigiraf.hp = 1; + + // eat berry turn 1, switch out turn 2 + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + const turn1Hp = farigiraf.hp; + game.doSwitchPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + // summonData got cleared due to switch, turnData got cleared due to turn end + expect(farigiraf.summonData.berriesEatenLast).toEqual([]); + expect(farigiraf.turnData.berriesEaten).toEqual([]); + expect(farigiraf.hp).toEqual(turn1Hp); + }); + + it("clears array if disabled", async () => { + game.override.enemyAbility(Abilities.NEUTRALIZING_GAS); + await game.classicMode.startBattle([Species.FARIGIRAF]); + + const farigiraf = game.scene.getPlayerPokemon()!; + farigiraf.hp = 1; + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(farigiraf.summonData.berriesEatenLast).toEqual([]); + expect(farigiraf.turnData.berriesEaten).toEqual([BerryType.SITRUS]); + + await game.toNextTurn(); + + // both arrays empty since neut gas disabled both the mid-turn and post-turn effects + expect(farigiraf.summonData.berriesEatenLast).toEqual([]); + expect(farigiraf.turnData.berriesEaten).toEqual([]); + }); + }); + + describe("regurgiates berries", () => { + it("re-triggers effects on eater without infinitely looping", async () => { + const apply = vi.spyOn(RepeatBerryNextTurnAbAttr.prototype, "apply"); + await game.classicMode.startBattle([Species.FARIGIRAF]); + + const farigiraf = game.scene.getPlayerPokemon()!; + farigiraf.hp = 1; + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + // ate 1 sitrus the turn prior, spitball pending + expect(farigiraf.battleData.berriesEaten).toEqual([BerryType.SITRUS]); + expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]); + expect(farigiraf.turnData.berriesEaten).toEqual([]); + expect(apply.mock.lastCall).toBeUndefined(); + + const turn1Hp = farigiraf.hp; + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + // healed back up to half without adding any more to array + expect(farigiraf.hp).toBeGreaterThan(turn1Hp); + expect(farigiraf.battleData.berriesEaten).toEqual([BerryType.SITRUS]); + expect(farigiraf.summonData.berriesEatenLast).toEqual([]); + expect(farigiraf.turnData.berriesEaten).toEqual([]); + }); + + it("bypasses unnerve", async () => { + game.override.enemyAbility(Abilities.UNNERVE); + await game.classicMode.startBattle([Species.FARIGIRAF]); + + const farigiraf = game.scene.getPlayerPokemon()!; + farigiraf.hp = 1; + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + // Turn end proc set the berriesEatenLast array back to being empty + expect(farigiraf.summonData.berriesEatenLast).toEqual([]); + expect(farigiraf.turnData.berriesEaten).toEqual([]); + expect(farigiraf.hp).toBeGreaterThanOrEqual(farigiraf.hp / 2); + }); + + it("doesn't trigger on non-eating removal", async () => { + game.override.enemyMoveset(Moves.INCINERATE); + await game.classicMode.startBattle([Species.FARIGIRAF]); + + const farigiraf = game.scene.getPlayerPokemon()!; + farigiraf.hp = farigiraf.getMaxHp() / 4; + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + // no berries eaten due to getting cooked + expect(farigiraf.summonData.berriesEatenLast).toEqual([]); + expect(farigiraf.turnData.berriesEaten).toEqual([]); + expect(farigiraf.hp).toBeLessThan(farigiraf.getMaxHp() / 4); + }); + + it("works with pluck even if berry is useless", async () => { + const bSpy = vi.fn(getBerryEffectFunc); + game.override + .enemySpecies(Species.BLAZIKEN) + .enemyHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }]) + .startingHeldItems([]); + await game.classicMode.startBattle([Species.FARIGIRAF]); + + game.move.select(Moves.BUG_BITE); + await game.toNextTurn(); + game.move.select(Moves.BUG_BITE); + await game.toNextTurn(); + + expect(bSpy).toBeCalledTimes(2); + }); + + it("works with Ripen", async () => { + const bSpy = vi.fn(getBerryEffectFunc); + game.override.passiveAbility(Abilities.RIPEN); + await game.classicMode.startBattle([Species.FARIGIRAF]); + + const farigiraf = game.scene.getPlayerPokemon()!; + farigiraf.hp = 1; + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + // Rounding errors only ever cost a maximum of 4 hp + expect(farigiraf.getInverseHp()).toBeLessThanOrEqual(3); + expect(bSpy).toHaveBeenCalledTimes(2); + }); + + it("is preserved on reload/wave clear", async () => { + game.override.enemyLevel(1); + await game.classicMode.startBattle([Species.FARIGIRAF]); + + const farigiraf = game.scene.getPlayerPokemon()!; + farigiraf.hp = 1; + + game.move.select(Moves.HYPER_VOICE); + await game.toNextWave(); + + // berry went yummy yummy in big fat giraffe tummy + expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]); + expect(farigiraf.hp).toBeGreaterThan(1); + + // reload and the berry should still be there + await game.reload.reloadSession(); + + const farigirafReloaded = game.scene.getPlayerPokemon()!; + expect(farigirafReloaded.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]); + + const wave1Hp = farigirafReloaded.hp; + + // blow up next wave and we should proc the repeat eating + game.move.select(Moves.HYPER_VOICE); + await game.toNextWave(); + + expect(farigirafReloaded.hp).toBeGreaterThan(wave1Hp); + }); + }); +}); diff --git a/test/abilities/good_as_gold.test.ts b/test/abilities/good_as_gold.test.ts index 4c4741a331f..b11dd700cc7 100644 --- a/test/abilities/good_as_gold.test.ts +++ b/test/abilities/good_as_gold.test.ts @@ -49,7 +49,7 @@ describe("Abilities - Good As Gold", () => { await game.phaseInterceptor.to("BerryPhase"); - expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.GOOD_AS_GOLD); + expect(player.waveData.abilitiesApplied).toContain(Abilities.GOOD_AS_GOLD); expect(player.getStatStage(Stat.ATK)).toBe(0); }); diff --git a/test/abilities/harvest.test.ts b/test/abilities/harvest.test.ts new file mode 100644 index 00000000000..5d2a23cbdba --- /dev/null +++ b/test/abilities/harvest.test.ts @@ -0,0 +1,308 @@ +import type Pokemon from "#app/field/pokemon"; +import { BerryModifier, PreserveBerryModifier } from "#app/modifier/modifier"; +import type { ModifierOverride } from "#app/modifier/modifier-type"; +import type { BooleanHolder } from "#app/utils"; +import { Abilities } from "#enums/abilities"; +import { BerryType } from "#enums/berry-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import { WeatherType } from "#enums/weather-type"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Harvest", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + const getPlayerBerries = () => + game.scene.getModifiers(BerryModifier, true).filter(b => b.pokemonId === game.scene.getPlayerPokemon()?.id); + + /** Check whether the player's Modifiers contains the specified berries. */ + function expectBerriesContaining(...berries: ModifierOverride[]): void { + const actualBerries: ModifierOverride[] = getPlayerBerries().map( + // only grab berry type and quantity since that's literally all we care about + b => ({ name: "BERRY", type: b.berryType, count: b.getStackCount() }), + ); + expect(actualBerries).toEqual(berries); + } + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.resetAllMocks(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([Moves.SPLASH, Moves.NATURAL_GIFT, Moves.FALSE_SWIPE, Moves.GASTRO_ACID]) + .ability(Abilities.HARVEST) + .startingLevel(100) + .battleType("single") + .disableCrits() + .statusActivation(false) // Since we're using nuzzle to proc both enigma and sitrus berries + .weather(WeatherType.SUNNY) // guaranteed recovery + .enemyLevel(1) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([Moves.SPLASH, Moves.NUZZLE, Moves.KNOCK_OFF, Moves.INCINERATE]); + }); + + it("replenishes eaten berries", async () => { + game.override.startingHeldItems([{ name: "BERRY", type: BerryType.LUM, count: 1 }]); + await game.classicMode.startBattle([Species.FEEBAS]); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.NUZZLE); + await game.phaseInterceptor.to("BerryPhase"); + expect(getPlayerBerries()).toHaveLength(0); + expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toHaveLength(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expectBerriesContaining({ name: "BERRY", type: BerryType.LUM, count: 1 }); + expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); + }); + + it("tracks berries eaten while disabled/not present", async () => { + // Note: this also checks for harvest not being present as neutralizing gas works by making + // the game consider all other pokemon to *not* have any ability. + game.override + .startingHeldItems([ + { name: "BERRY", type: BerryType.ENIGMA, count: 2 }, + { name: "BERRY", type: BerryType.LUM, count: 2 }, + ]) + .enemyAbility(Abilities.NEUTRALIZING_GAS) + .weather(WeatherType.NONE); // clear weather so we can control when harvest rolls succeed + await game.classicMode.startBattle([Species.MILOTIC]); + + const player = game.scene.getPlayerPokemon(); + + // Chug a few berries without harvest (should get tracked) + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.NUZZLE); + await game.toNextTurn(); + + expect(player?.battleData.berriesEaten).toEqual(expect.arrayContaining([BerryType.ENIGMA, BerryType.LUM])); + expect(getPlayerBerries()).toHaveLength(2); + + // Give ourselves harvest and disable enemy neut gas, + // but force our roll to fail so we don't accidentally recover anything + game.override.ability(Abilities.HARVEST); + game.move.select(Moves.GASTRO_ACID); + await game.forceEnemyMove(Moves.NUZZLE); + await game.phaseInterceptor.to("TurnEndPhase", false); + vi.spyOn(Phaser.Math.RND, "realInRange").mockReturnValue(0); + + expect(player?.battleData.berriesEaten).toEqual( + expect.arrayContaining([BerryType.ENIGMA, BerryType.LUM, BerryType.ENIGMA, BerryType.LUM]), + ); + expect(getPlayerBerries()).toHaveLength(0); + + // proc a high roll and we _should_ get a berry back! + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase", false); + vi.spyOn(Phaser.Math.RND, "realInRange").mockReturnValue(1); + await game.toNextTurn(); + + expect(player?.battleData.berriesEaten).toHaveLength(3); + expect(getPlayerBerries()).toHaveLength(1); + }); + + // TODO: Figure out why this is borking...??? + it("remembers berries eaten tracker across waves and save/reload", async () => { + game.override + .startingHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 2 }]) + .ability(Abilities.BALL_FETCH); // don't actually need harvest for this test + await game.classicMode.startBattle([Species.REGIELEKI]); + + const regieleki = game.scene.getPlayerPokemon()!; + regieleki.hp = 1; + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH); + await game.doKillOpponents(); + await game.phaseInterceptor.to("TurnEndPhase"); + + // ate 1 berry without recovering (no harvest) + expect(regieleki.battleData.berriesEaten).toEqual([BerryType.PETAYA]); + expect(getPlayerBerries()).toEqual([expect.objectContaining({ berryType: BerryType.PETAYA, stackCount: 1 })]); + expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.SPATK)).toBe(1); + + await game.toNextWave(); + + expect(regieleki.battleData.berriesEaten).toEqual([BerryType.PETAYA]); + + await game.reload.reloadSession(); + + const regielekiReloaded = game.scene.getPlayerPokemon()!; + expect(regielekiReloaded.battleData.berriesEaten).toEqual([BerryType.PETAYA]); + }); + + it("cannot restore capped berries", async () => { + const initBerries: ModifierOverride[] = [ + { name: "BERRY", type: BerryType.LUM, count: 2 }, + { name: "BERRY", type: BerryType.STARF, count: 2 }, + ]; + game.override.startingHeldItems(initBerries); + await game.classicMode.startBattle([Species.FEEBAS]); + const player = game.scene.getPlayerPokemon()!; + player.battleData.berriesEaten = [BerryType.LUM, BerryType.STARF]; + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + // Force RNG roll to hit the first berry we find. + // This does nothing on a success (since there'd only be a starf left to grab), + // but ensures we don't accidentally let any false positives through. + vi.spyOn(Phaser.Math.RND, "integerInRange").mockReturnValue(0); + await game.phaseInterceptor.to("TurnEndPhase"); + + expectBerriesContaining({ name: "BERRY", type: BerryType.STARF, count: 3 }); + }); + + it("does nothing if all berries are capped", async () => { + const initBerries: ModifierOverride[] = [ + { name: "BERRY", type: BerryType.LUM, count: 2 }, + { name: "BERRY", type: BerryType.STARF, count: 3 }, + ]; + game.override.startingHeldItems(initBerries); + await game.classicMode.startBattle([Species.FEEBAS]); + + const player = game.scene.getPlayerPokemon()!; + player.battleData.berriesEaten = [BerryType.LUM, BerryType.STARF]; + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expectBerriesContaining(...initBerries); + }); + + describe("move/ability interactions", () => { + it("cannot restore incinerated berries", async () => { + game.override.startingHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 3 }]); + await game.classicMode.startBattle([Species.FEEBAS]); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.INCINERATE); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); + }); + + it("cannot restore knocked off berries", async () => { + game.override.startingHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 3 }]); + await game.classicMode.startBattle([Species.FEEBAS]); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.KNOCK_OFF); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); + }); + + it("can restore berries eaten by Teatime", async () => { + const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.STARF, count: 1 }]; + game.override.startingHeldItems(initBerries).enemyMoveset(Moves.TEATIME); + await game.classicMode.startBattle([Species.FEEBAS]); + + // nom nom the berr berr yay yay + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); + expectBerriesContaining(...initBerries); + }); + + it("cannot restore Plucked berries for either side", async () => { + const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.PETAYA, count: 1 }]; + game.override.startingHeldItems(initBerries).enemyAbility(Abilities.HARVEST).enemyMoveset(Moves.PLUCK); + await game.classicMode.startBattle([Species.FEEBAS]); + + // gobble gobble gobble + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + // pluck triggers harvest for neither side + expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); + expect(game.scene.getEnemyPokemon()?.battleData.berriesEaten).toEqual([]); + expect(getPlayerBerries()).toEqual([]); + }); + + it("cannot restore berries preserved via Berry Pouch", async () => { + // mock berry pouch to have a 100% success rate + vi.spyOn(PreserveBerryModifier.prototype, "apply").mockImplementation( + (_pokemon: Pokemon, doPreserve: BooleanHolder): boolean => { + doPreserve.value = false; + return true; + }, + ); + + const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.PETAYA, count: 1 }]; + game.override.startingHeldItems(initBerries).startingModifier([{ name: "BERRY_POUCH", count: 1 }]); + await game.classicMode.startBattle([Species.FEEBAS]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase", false); + + // won;t trigger harvest since we didn't lose the berry (it just doesn't ever add it to the array) + expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); + expectBerriesContaining(...initBerries); + }); + + it("can restore stolen berries", async () => { + const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.SITRUS, count: 1 }]; + game.override.enemyHeldItems(initBerries).passiveAbility(Abilities.MAGICIAN).hasPassiveAbility(true); + await game.classicMode.startBattle([Species.MEOWSCARADA]); + + // pre damage + const player = game.scene.getPlayerPokemon()!; + player.hp = 1; + + // steal a sitrus and immediately consume it + game.move.select(Moves.FALSE_SWIPE); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + expect(player.battleData.berriesEaten).toEqual([BerryType.SITRUS]); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(player.battleData.berriesEaten).toEqual([]); + expectBerriesContaining(...initBerries); + }); + + // TODO: Enable once fling actually works...??? + it.todo("can restore berries flung at user", async () => { + game.override.enemyHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 1 }]).enemyMoveset(Moves.FLING); + await game.classicMode.startBattle([Species.FEEBAS]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toBe([]); + expect(getPlayerBerries()).toEqual([]); + }); + + // TODO: Enable once Nat Gift gets implemented...??? + it.todo("can restore berries consumed via Natural Gift", async () => { + const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.STARF, count: 1 }]; + game.override.startingHeldItems(initBerries); + await game.classicMode.startBattle([Species.FEEBAS]); + + game.move.select(Moves.NATURAL_GIFT); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toHaveLength(0); + expectBerriesContaining(...initBerries); + }); + }); +}); diff --git a/test/abilities/illusion.test.ts b/test/abilities/illusion.test.ts index aa77aa701b2..605322901df 100644 --- a/test/abilities/illusion.test.ts +++ b/test/abilities/illusion.test.ts @@ -39,8 +39,8 @@ describe("Abilities - Illusion", () => { const zoroark = game.scene.getPlayerPokemon()!; const zorua = game.scene.getEnemyPokemon()!; - expect(!!zoroark.summonData?.illusion).equals(true); - expect(!!zorua.summonData?.illusion).equals(true); + expect(!!zoroark.summonData.illusion).equals(true); + expect(!!zorua.summonData.illusion).equals(true); }); it("break after receiving damaging move", async () => { @@ -51,7 +51,7 @@ describe("Abilities - Illusion", () => { const zorua = game.scene.getEnemyPokemon()!; - expect(!!zorua.summonData?.illusion).equals(false); + expect(!!zorua.summonData.illusion).equals(false); expect(zorua.name).equals("Zorua"); }); @@ -63,7 +63,7 @@ describe("Abilities - Illusion", () => { const zorua = game.scene.getEnemyPokemon()!; - expect(!!zorua.summonData?.illusion).equals(false); + expect(!!zorua.summonData.illusion).equals(false); }); it("break if the ability is suppressed", async () => { @@ -72,7 +72,7 @@ describe("Abilities - Illusion", () => { const zorua = game.scene.getEnemyPokemon()!; - expect(!!zorua.summonData?.illusion).equals(false); + expect(!!zorua.summonData.illusion).equals(false); }); it("causes enemy AI to consider the illusion's type instead of the actual type when considering move effectiveness", async () => { @@ -117,7 +117,7 @@ describe("Abilities - Illusion", () => { const zoroark = game.scene.getPlayerPokemon()!; - expect(!!zoroark.summonData?.illusion).equals(true); + expect(!!zoroark.summonData.illusion).equals(true); }); it("copies the the name, nickname, gender, shininess, and pokeball from the illusion source", async () => { diff --git a/test/abilities/infiltrator.test.ts b/test/abilities/infiltrator.test.ts index 6278439651c..97748f66e78 100644 --- a/test/abilities/infiltrator.test.ts +++ b/test/abilities/infiltrator.test.ts @@ -68,7 +68,7 @@ describe("Abilities - Infiltrator", () => { const postScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage; expect(postScreenDmg).toBe(preScreenDmg); - expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR); + expect(player.waveData.abilitiesApplied).toContain(Abilities.INFILTRATOR); }); it("should bypass the target's Safeguard", async () => { @@ -83,7 +83,7 @@ describe("Abilities - Infiltrator", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemy.status?.effect).toBe(StatusEffect.SLEEP); - expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR); + expect(player.waveData.abilitiesApplied).toContain(Abilities.INFILTRATOR); }); // TODO: fix this interaction to pass this test @@ -99,7 +99,7 @@ describe("Abilities - Infiltrator", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemy.getStatStage(Stat.ATK)).toBe(-1); - expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR); + expect(player.waveData.abilitiesApplied).toContain(Abilities.INFILTRATOR); }); it("should bypass the target's Substitute", async () => { @@ -114,6 +114,6 @@ describe("Abilities - Infiltrator", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemy.getStatStage(Stat.ATK)).toBe(-1); - expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR); + expect(player.waveData.abilitiesApplied).toContain(Abilities.INFILTRATOR); }); }); diff --git a/test/abilities/libero.test.ts b/test/abilities/libero.test.ts index 22abf1c248f..3c0726270f9 100644 --- a/test/abilities/libero.test.ts +++ b/test/abilities/libero.test.ts @@ -67,7 +67,7 @@ describe("Abilities - Libero", () => { game.move.select(Moves.AGILITY); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied.filter(a => a === Abilities.LIBERO)).toHaveLength(1); + expect(leadPokemon.waveData.abilitiesApplied).toContain(Abilities.LIBERO); const leadPokemonType = PokemonType[leadPokemon.getTypes()[0]]; const moveType = PokemonType[allMoves[Moves.AGILITY].type]; expect(leadPokemonType).not.toBe(moveType); @@ -99,7 +99,7 @@ describe("Abilities - Libero", () => { game.move.select(Moves.WEATHER_BALL); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied).toContain(Abilities.LIBERO); + expect(leadPokemon.waveData.abilitiesApplied).toContain(Abilities.LIBERO); expect(leadPokemon.getTypes()).toHaveLength(1); const leadPokemonType = PokemonType[leadPokemon.getTypes()[0]], moveType = PokemonType[PokemonType.FIRE]; @@ -118,7 +118,7 @@ describe("Abilities - Libero", () => { game.move.select(Moves.TACKLE); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied).toContain(Abilities.LIBERO); + expect(leadPokemon.waveData.abilitiesApplied).toContain(Abilities.LIBERO); expect(leadPokemon.getTypes()).toHaveLength(1); const leadPokemonType = PokemonType[leadPokemon.getTypes()[0]], moveType = PokemonType[PokemonType.ICE]; @@ -214,7 +214,7 @@ describe("Abilities - Libero", () => { game.move.select(Moves.SPLASH); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.LIBERO); + expect(leadPokemon.waveData.abilitiesApplied).not.toContain(Abilities.LIBERO); }); test("ability is not applied if pokemon is terastallized", async () => { @@ -230,7 +230,7 @@ describe("Abilities - Libero", () => { game.move.select(Moves.SPLASH); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.LIBERO); + expect(leadPokemon.waveData.abilitiesApplied).not.toContain(Abilities.LIBERO); }); test("ability is not applied if pokemon uses struggle", async () => { @@ -244,7 +244,7 @@ describe("Abilities - Libero", () => { game.move.select(Moves.STRUGGLE); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.LIBERO); + expect(leadPokemon.waveData.abilitiesApplied).not.toContain(Abilities.LIBERO); }); test("ability is not applied if the pokemon's move fails", async () => { @@ -258,7 +258,7 @@ describe("Abilities - Libero", () => { game.move.select(Moves.BURN_UP); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.LIBERO); + expect(leadPokemon.waveData.abilitiesApplied).not.toContain(Abilities.LIBERO); }); test("ability applies correctly even if the pokemon's Trick-or-Treat fails", async () => { @@ -293,7 +293,7 @@ describe("Abilities - Libero", () => { }); function testPokemonTypeMatchesDefaultMoveType(pokemon: PlayerPokemon, move: Moves) { - expect(pokemon.summonData.abilitiesApplied).toContain(Abilities.LIBERO); + expect(pokemon.waveData.abilitiesApplied).toContain(Abilities.LIBERO); expect(pokemon.getTypes()).toHaveLength(1); const pokemonType = PokemonType[pokemon.getTypes()[0]], moveType = PokemonType[allMoves[move].type]; diff --git a/test/abilities/protean.test.ts b/test/abilities/protean.test.ts index 574033bb13f..9f870c74a71 100644 --- a/test/abilities/protean.test.ts +++ b/test/abilities/protean.test.ts @@ -67,7 +67,7 @@ describe("Abilities - Protean", () => { game.move.select(Moves.AGILITY); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied.filter(a => a === Abilities.PROTEAN)).toHaveLength(1); + expect(leadPokemon.waveData.abilitiesApplied).toContain(Abilities.PROTEAN); const leadPokemonType = PokemonType[leadPokemon.getTypes()[0]]; const moveType = PokemonType[allMoves[Moves.AGILITY].type]; expect(leadPokemonType).not.toBe(moveType); @@ -99,7 +99,7 @@ describe("Abilities - Protean", () => { game.move.select(Moves.WEATHER_BALL); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied).toContain(Abilities.PROTEAN); + expect(leadPokemon.waveData.abilitiesApplied).toContain(Abilities.PROTEAN); expect(leadPokemon.getTypes()).toHaveLength(1); const leadPokemonType = PokemonType[leadPokemon.getTypes()[0]], moveType = PokemonType[PokemonType.FIRE]; @@ -118,7 +118,7 @@ describe("Abilities - Protean", () => { game.move.select(Moves.TACKLE); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied).toContain(Abilities.PROTEAN); + expect(leadPokemon.waveData.abilitiesApplied).toContain(Abilities.PROTEAN); expect(leadPokemon.getTypes()).toHaveLength(1); const leadPokemonType = PokemonType[leadPokemon.getTypes()[0]], moveType = PokemonType[PokemonType.ICE]; @@ -214,7 +214,7 @@ describe("Abilities - Protean", () => { game.move.select(Moves.SPLASH); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.PROTEAN); + expect(leadPokemon.waveData.abilitiesApplied).not.toContain(Abilities.PROTEAN); }); test("ability is not applied if pokemon is terastallized", async () => { @@ -230,7 +230,7 @@ describe("Abilities - Protean", () => { game.move.select(Moves.SPLASH); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.PROTEAN); + expect(leadPokemon.waveData.abilitiesApplied).not.toContain(Abilities.PROTEAN); }); test("ability is not applied if pokemon uses struggle", async () => { @@ -244,7 +244,7 @@ describe("Abilities - Protean", () => { game.move.select(Moves.STRUGGLE); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.PROTEAN); + expect(leadPokemon.waveData.abilitiesApplied).not.toContain(Abilities.PROTEAN); }); test("ability is not applied if the pokemon's move fails", async () => { @@ -258,7 +258,7 @@ describe("Abilities - Protean", () => { game.move.select(Moves.BURN_UP); await game.phaseInterceptor.to(TurnEndPhase); - expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.PROTEAN); + expect(leadPokemon.waveData.abilitiesApplied).not.toContain(Abilities.PROTEAN); }); test("ability applies correctly even if the pokemon's Trick-or-Treat fails", async () => { @@ -293,7 +293,7 @@ describe("Abilities - Protean", () => { }); function testPokemonTypeMatchesDefaultMoveType(pokemon: PlayerPokemon, move: Moves) { - expect(pokemon.summonData.abilitiesApplied).toContain(Abilities.PROTEAN); + expect(pokemon.waveData.abilitiesApplied).toContain(Abilities.PROTEAN); expect(pokemon.getTypes()).toHaveLength(1); const pokemonType = PokemonType[pokemon.getTypes()[0]], moveType = PokemonType[allMoves[move].type]; diff --git a/test/abilities/quick_draw.test.ts b/test/abilities/quick_draw.test.ts index 1277fd5d3cb..9af8fa893f0 100644 --- a/test/abilities/quick_draw.test.ts +++ b/test/abilities/quick_draw.test.ts @@ -54,7 +54,7 @@ describe("Abilities - Quick Draw", () => { expect(pokemon.isFainted()).toBe(false); expect(enemy.isFainted()).toBe(true); - expect(pokemon.battleData.abilitiesApplied).contain(Abilities.QUICK_DRAW); + expect(pokemon.waveData.abilitiesApplied).contain(Abilities.QUICK_DRAW); }, 20000); test( @@ -76,7 +76,7 @@ describe("Abilities - Quick Draw", () => { expect(pokemon.isFainted()).toBe(true); expect(enemy.isFainted()).toBe(false); - expect(pokemon.battleData.abilitiesApplied).not.contain(Abilities.QUICK_DRAW); + expect(pokemon.waveData.abilitiesApplied).not.contain(Abilities.QUICK_DRAW); }, ); @@ -96,6 +96,6 @@ describe("Abilities - Quick Draw", () => { expect(pokemon.isFainted()).toBe(true); expect(enemy.isFainted()).toBe(false); - expect(pokemon.battleData.abilitiesApplied).contain(Abilities.QUICK_DRAW); + expect(pokemon.waveData.abilitiesApplied).contain(Abilities.QUICK_DRAW); }, 20000); }); diff --git a/test/abilities/wimp_out.test.ts b/test/abilities/wimp_out.test.ts index 294025a10e7..1a4ddd31967 100644 --- a/test/abilities/wimp_out.test.ts +++ b/test/abilities/wimp_out.test.ts @@ -155,7 +155,7 @@ describe("Abilities - Wimp Out", () => { game.doSelectPartyPokemon(1); await game.phaseInterceptor.to("SwitchSummonPhase", false); - expect(wimpod.summonData.abilitiesApplied).not.toContain(Abilities.WIMP_OUT); + expect(wimpod.waveData.abilitiesApplied).not.toContain(Abilities.WIMP_OUT); await game.phaseInterceptor.to("TurnEndPhase"); @@ -534,12 +534,12 @@ describe("Abilities - Wimp Out", () => { .enemyAbility(Abilities.WIMP_OUT) .startingLevel(50) .enemyLevel(1) - .enemyMoveset([ Moves.SPLASH, Moves.ENDURE ]) + .enemyMoveset([Moves.SPLASH, Moves.ENDURE]) .battleType("double") - .moveset([ Moves.DRAGON_ENERGY, Moves.SPLASH ]) + .moveset([Moves.DRAGON_ENERGY, Moves.SPLASH]) .startingWave(wave); - await game.classicMode.startBattle([ Species.REGIDRAGO, Species.MAGIKARP ]); + await game.classicMode.startBattle([Species.REGIDRAGO, Species.MAGIKARP]); // turn 1 game.move.select(Moves.DRAGON_ENERGY, 0); @@ -549,6 +549,5 @@ describe("Abilities - Wimp Out", () => { await game.phaseInterceptor.to("SelectModifierPhase"); expect(game.scene.currentBattle.waveIndex).toBe(wave + 1); - }); }); diff --git a/test/battle/inverse_battle.test.ts b/test/battle/inverse_battle.test.ts index 83109c35740..949fd484f4c 100644 --- a/test/battle/inverse_battle.test.ts +++ b/test/battle/inverse_battle.test.ts @@ -179,12 +179,12 @@ describe("Inverse Battle", () => { expect(enemy.status?.effect).toBe(StatusEffect.PARALYSIS); }); - it("Anticipation should trigger on 2x effective moves - Anticipation against Thunderbolt", async () => { + it("Anticipation should trigger on 2x effective moves", async () => { game.override.moveset([Moves.THUNDERBOLT]).enemySpecies(Species.SANDSHREW).enemyAbility(Abilities.ANTICIPATION); await game.challengeMode.startBattle(); - expect(game.scene.getEnemyPokemon()?.summonData.abilitiesApplied[0]).toBe(Abilities.ANTICIPATION); + expect(game.scene.getEnemyPokemon()?.waveData.abilitiesApplied).toContain(Abilities.ANTICIPATION); }); it("Conversion 2 should change the type to the resistive type - Conversion 2 against Dragonite", async () => { diff --git a/test/battlerTags/substitute.test.ts b/test/battlerTags/substitute.test.ts index fca3dc5ef7e..7aeceb42635 100644 --- a/test/battlerTags/substitute.test.ts +++ b/test/battlerTags/substitute.test.ts @@ -42,7 +42,6 @@ describe("BattlerTag - SubstituteTag", () => { // simulate a Trapped tag set by another Pokemon, then expect the filter to catch it. const trapTag = new BindTag(5, 0); expect(tagFilter(trapTag)).toBeTruthy(); - return true; }) as Pokemon["findAndRemoveTags"], } as unknown as Pokemon; diff --git a/test/moves/dive.test.ts b/test/moves/dive.test.ts index d7b53701a25..82938ad65f3 100644 --- a/test/moves/dive.test.ts +++ b/test/moves/dive.test.ts @@ -105,7 +105,7 @@ describe("Moves - Dive", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); - expect(enemyPokemon.battleData.abilitiesApplied[0]).toBe(Abilities.ROUGH_SKIN); + expect(enemyPokemon.waveData.abilitiesApplied).toContain(Abilities.ROUGH_SKIN); }); it("should cancel attack after Harsh Sunlight is set", async () => { diff --git a/test/moves/instruct.test.ts b/test/moves/instruct.test.ts index 079c8803ddc..c0a87fb92c5 100644 --- a/test/moves/instruct.test.ts +++ b/test/moves/instruct.test.ts @@ -228,7 +228,7 @@ describe("Moves - Instruct", () => { const amoonguss = game.scene.getPlayerPokemon()!; game.move.changeMoveset(amoonguss, Moves.SEED_BOMB); - amoonguss.battleSummonData.moveHistory = [ + amoonguss.summonData.moveHistory = [ { move: Moves.SEED_BOMB, targets: [BattlerIndex.ENEMY], @@ -301,7 +301,7 @@ describe("Moves - Instruct", () => { const player = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; - enemy.battleSummonData.moveHistory = [ + enemy.summonData.moveHistory = [ { move: Moves.SONIC_BOOM, targets: [BattlerIndex.PLAYER], @@ -350,7 +350,7 @@ describe("Moves - Instruct", () => { await game.classicMode.startBattle([Species.LUCARIO, Species.BANETTE]); const enemyPokemon = game.scene.getEnemyPokemon()!; - enemyPokemon.battleSummonData.moveHistory = [ + enemyPokemon.summonData.moveHistory = [ { move: Moves.WHIRLWIND, targets: [BattlerIndex.PLAYER], diff --git a/test/moves/order_up.test.ts b/test/moves/order_up.test.ts index 516f7f625a3..f13b86206b4 100644 --- a/test/moves/order_up.test.ts +++ b/test/moves/order_up.test.ts @@ -81,7 +81,7 @@ describe("Moves - Order Up", () => { await game.phaseInterceptor.to("BerryPhase", false); - expect(dondozo.battleData.abilitiesApplied.includes(Abilities.SHEER_FORCE)).toBeTruthy(); + expect(dondozo.waveData.abilitiesApplied).toContain(Abilities.SHEER_FORCE); expect(dondozo.getStatStage(Stat.ATK)).toBe(3); }); }); diff --git a/test/moves/powder.test.ts b/test/moves/powder.test.ts index 522b0b74ca7..737d3873aa5 100644 --- a/test/moves/powder.test.ts +++ b/test/moves/powder.test.ts @@ -146,7 +146,7 @@ describe("Moves - Powder", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - expect(enemyPokemon.summonData?.types).not.toBe(PokemonType.FIRE); + expect(enemyPokemon.summonData.types).not.toBe(PokemonType.FIRE); }); it("should cancel Fire-type moves generated by the target's Dancer ability", async () => { diff --git a/test/moves/rage_fist.test.ts b/test/moves/rage_fist.test.ts index f44901c5aba..0e0d05b03c2 100644 --- a/test/moves/rage_fist.test.ts +++ b/test/moves/rage_fist.test.ts @@ -28,7 +28,7 @@ describe("Moves - Rage Fist", () => { game = new GameManager(phaserGame); game.override .battleType("single") - .moveset([Moves.RAGE_FIST, Moves.SPLASH, Moves.SUBSTITUTE]) + .moveset([Moves.RAGE_FIST, Moves.SPLASH, Moves.SUBSTITUTE, Moves.TIDY_UP]) .startingLevel(100) .enemyLevel(1) .enemyAbility(Abilities.BALL_FETCH) @@ -37,7 +37,7 @@ describe("Moves - Rage Fist", () => { vi.spyOn(move, "calculateBattlePower"); }); - it("should have 100 more power if hit twice before calling Rage Fist", async () => { + it("should gain power per hit taken", async () => { game.override.enemySpecies(Species.MAGIKARP); await game.classicMode.startBattle([Species.MAGIKARP]); @@ -49,7 +49,69 @@ describe("Moves - Rage Fist", () => { expect(move.calculateBattlePower).toHaveLastReturnedWith(150); }); - it("should maintain its power during next battle if it is within the same arena encounter", async () => { + it("caps at 6 hits taken", async () => { + game.override.enemySpecies(Species.MAGIKARP); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + // spam splash against magikarp hitting us 2 times per turn + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + game.move.select(Moves.RAGE_FIST); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("TurnEndPhase"); + + // hit 8 times, but nothing else + expect(game.scene.getPlayerPokemon()?.battleData.hitCount).toBe(8); + expect(move.calculateBattlePower).toHaveLastReturnedWith(350); + }); + + it("should not count subsitute hits or confusion damage", async () => { + game.override.enemySpecies(Species.MAGIKARP).startingWave(4).enemyMoveset([Moves.CONFUSE_RAY, Moves.DOUBLE_KICK]); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + game.move.select(Moves.SUBSTITUTE); + await game.forceEnemyMove(Moves.DOUBLE_KICK); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.phaseInterceptor.to("BerryPhase"); + + // no increase due to substitute + expect(move.calculateBattlePower).toHaveLastReturnedWith(50); + expect(game.scene.getPlayerPokemon()?.battleData.hitCount).toBe(0); + + await game.toNextTurn(); + + // remove substitute and get confused + game.move.select(Moves.TIDY_UP); + await game.forceEnemyMove(Moves.CONFUSE_RAY); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + + game.move.select(Moves.RAGE_FIST); + await game.move.forceStatusActivation(true); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + // didn't go up + expect(game.scene.getPlayerPokemon()?.battleData.hitCount).toBe(0); + + await game.toNextTurn(); + + game.move.select(Moves.RAGE_FIST); + await game.move.forceStatusActivation(false); + await game.toNextTurn(); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(150); + expect(game.scene.getPlayerPokemon()?.battleData.hitCount).toBe(2); + }); + + it("should maintain hits recieved between wild waves", async () => { game.override.enemySpecies(Species.MAGIKARP).startingWave(1); await game.classicMode.startBattle([Species.MAGIKARP]); @@ -63,10 +125,11 @@ describe("Moves - Rage Fist", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(move.calculateBattlePower).toHaveLastReturnedWith(250); + expect(game.scene.getPlayerPokemon()?.battleData.hitCount).toBe(4); }); - it("should reset the hitRecCounter if we enter new trainer battle", async () => { - game.override.enemySpecies(Species.MAGIKARP).startingWave(4); + it("should reset hits recieved during trainer battles", async () => { + game.override.enemySpecies(Species.MAGIKARP).startingWave(19); await game.classicMode.startBattle([Species.MAGIKARP]); @@ -81,18 +144,6 @@ describe("Moves - Rage Fist", () => { expect(move.calculateBattlePower).toHaveLastReturnedWith(150); }); - it("should not increase the hitCounter if Substitute is hit", async () => { - game.override.enemySpecies(Species.MAGIKARP).startingWave(4); - - await game.classicMode.startBattle([Species.MAGIKARP]); - - game.move.select(Moves.SUBSTITUTE); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to("MoveEffectPhase"); - - expect(game.scene.getPlayerPokemon()?.customPokemonData.hitsRecCount).toBe(0); - }); - it("should reset the hitRecCounter if we enter new biome", async () => { game.override.enemySpecies(Species.MAGIKARP).startingWave(10); diff --git a/test/moves/u_turn.test.ts b/test/moves/u_turn.test.ts index f1d212f3f47..6a8f8bb2f50 100644 --- a/test/moves/u_turn.test.ts +++ b/test/moves/u_turn.test.ts @@ -65,7 +65,7 @@ describe("Moves - U-turn", () => { // assert const playerPkm = game.scene.getPlayerPokemon()!; expect(playerPkm.hp).not.toEqual(playerPkm.getMaxHp()); - expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); // proxy for asserting ability activated + expect(game.scene.getEnemyPokemon()!.waveData.abilityRevealed).toBe(true); // proxy for asserting ability activated expect(playerPkm.species.speciesId).toEqual(Species.RAICHU); expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); }, 20000); @@ -84,7 +84,7 @@ describe("Moves - U-turn", () => { const playerPkm = game.scene.getPlayerPokemon()!; expect(playerPkm.status?.effect).toEqual(StatusEffect.POISON); expect(playerPkm.species.speciesId).toEqual(Species.RAICHU); - expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); // proxy for asserting ability activated + expect(game.scene.getEnemyPokemon()!.waveData.abilityRevealed).toBe(true); // proxy for asserting ability activated expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); }, 20000); diff --git a/test/testUtils/helpers/moveHelper.ts b/test/testUtils/helpers/moveHelper.ts index a54028ebca0..984966314a4 100644 --- a/test/testUtils/helpers/moveHelper.ts +++ b/test/testUtils/helpers/moveHelper.ts @@ -103,6 +103,17 @@ export class MoveHelper extends GameManagerHelper { vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(null); } + /** + * Forces the Confusion status to activate on the next move by temporarily mocking {@linkcode Overrides.CONFUSION_ACTIVATION_OVERRIDE}, + * advancing to the next `MovePhase`, and then resetting the override to `null` + * @param activated - `true` to force the Pokemon to hit themself, `false` to forcibly disable it + */ + public async forceConfusionActivation(activated: boolean): Promise { + vi.spyOn(Overrides, "CONFUSION_ACTIVATION_OVERRIDE", "get").mockReturnValue(activated); + await this.game.phaseInterceptor.to("MovePhase"); + vi.spyOn(Overrides, "CONFUSION_ACTIVATION_OVERRIDE", "get").mockReturnValue(null); + } + /** * Changes a pokemon's moveset to the given move(s). * Used when the normal moveset override can't be used (such as when it's necessary to check or update properties of the moveset). diff --git a/test/testUtils/helpers/overridesHelper.ts b/test/testUtils/helpers/overridesHelper.ts index 0ed1511255b..124e543e7d0 100644 --- a/test/testUtils/helpers/overridesHelper.ts +++ b/test/testUtils/helpers/overridesHelper.ts @@ -491,6 +491,21 @@ export class OverridesHelper extends GameManagerHelper { return this; } + /** + * Override confusion to always or never activate + * @param activate - `true` to force activation, `false` to force no activation, `null` to disable the override + * @returns `this` + */ + public confusionActivation(activate: boolean | null): this { + vi.spyOn(Overrides, "CONFUSION_ACTIVATION_OVERRIDE", "get").mockReturnValue(activate); + if (activate !== null) { + this.log(`Confusion forced to ${activate ? "always" : "never"} activate!`); + } else { + this.log("Confusion activation override disabled!"); + } + return this; + } + /** * Override the encounter chance for a mystery encounter. * @param percentage the encounter chance in %