Grabbed reverted changes from stuff

This commit is contained in:
Bertie690 2025-04-20 17:17:25 -04:00
parent 18c4dddcf0
commit 14b47c0eef
51 changed files with 1680 additions and 827 deletions

View File

@ -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,8 +4595,11 @@ 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(),
@ -4518,7 +4610,6 @@ export class HealFromBerryUseAbAttr extends AbAttr {
);
}
}
}
export class RunSuccessAbAttr extends AbAttr {
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: BooleanHolder, args: any[]): void {
@ -4548,7 +4639,8 @@ export class CheckTrappedAbAttr extends AbAttr {
simulated: boolean,
trapped: BooleanHolder,
otherPokemon: Pokemon,
args: any[]): boolean {
args: any[],
): boolean {
return true;
}
@ -5154,7 +5246,7 @@ export class IllusionPreSummonAbAttr extends PreSummonAbAttr {
}
override canApplyPreSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean {
pokemon.initSummondata()
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;
@ -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<TAttr extends AbAttr>(
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)

View File

@ -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,9 +745,12 @@ 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 (!shouldLapse) {
return false;
}
if (ret) {
globalScene.queueMessage(
i18next.t("battlerTags:confusedLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
@ -750,20 +759,19 @@ export class ConfusedTag extends BattlerTag {
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) {
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 });
pokemon.battleData.hitCount++;
(globalScene.getCurrentPhase() as MovePhase).cancel();
}
}
return ret;
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;

View File

@ -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 {
return (consumer: Pokemon) => {
// Apply an effect pertaining to what berry we're using
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);
{
const hpHealed = new NumberHolder(toDmgValue(consumer.getMaxHp() / 4));
applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, hpHealed);
globalScene.unshiftPhase(
new PokemonHealPhase(
pokemon.getBattlerIndex(),
consumer.getBattlerIndex(),
hpHealed.value,
i18next.t("battle:hpHealBerry", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
pokemonNameWithAffix: getPokemonNameWithAffix(consumer),
berryName: getBerryName(berryType),
}),
true,
),
);
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
};
}
break;
case BerryType.LUM:
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType);
{
if (consumer.status) {
globalScene.queueMessage(
getStatusEffectHealText(consumer.status.effect, getPokemonNameWithAffix(consumer)),
);
}
if (pokemon.status) {
globalScene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon)));
consumer.resetStatus(true, true);
consumer.updateInfo();
}
pokemon.resetStatus(true, true);
pokemon.updateInfo();
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
};
break;
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
{
// 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, pokemon, null, false, statStages);
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, [stat], statStages.value));
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
};
applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, statStages);
globalScene.unshiftPhase(
new StatStageChangePhase(consumer.getBattlerIndex(), true, [stat], statStages.value),
);
}
break;
case BerryType.LANSAT:
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType);
{
consumer.addTag(BattlerTagType.CRIT_BOOST);
}
pokemon.addTag(BattlerTagType.CRIT_BOOST);
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
};
break;
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);
applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, stages);
globalScene.unshiftPhase(
new StatStageChangePhase(consumer.getBattlerIndex(), true, [randStat], stages.value),
);
}
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);
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(pokemon),
moveName: ppRestoreMove!.getName(),
pokemonNameWithAffix: getPokemonNameWithAffix(consumer),
moveName: ppRestoreMove.getName(),
berryName: getBerryName(berryType),
}),
);
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
}
}
break;
default:
console.error("Incorrect BerryType %d passed to GetBerryEffectFunc", berryType);
}
};
}
}

View File

@ -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<CustomPokemonData>) {
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;
}
}

View File

@ -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)

View File

@ -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];

View File

@ -222,7 +222,7 @@ function endTrainerBattleAndShowDialogue(): Promise<void> {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeAbilityTrigger);
}
pokemon.resetBattleData();
pokemon.resetBattleAndWaveData();
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
}

View File

@ -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;

View File

@ -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];

File diff suppressed because it is too large Load Diff

View File

@ -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;
}
@ -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;
}

View File

@ -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

View File

@ -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)[];

View File

@ -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 => {
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;
}
if (hasUsableBerry) {
// 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),
}),
);
} else {
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);
}
globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); // Announce a berry was used
// 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));
}
}
});
this.end();
}
}

View File

@ -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();
}
}

View File

@ -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];

View File

@ -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];

View File

@ -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());
}

View File

@ -7,16 +7,15 @@ 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();
}
}
for (const pokemon of globalScene.getPlayerParty().filter(p => p.isOnField())) {
pokemon.resetBattleAndWaveData();
if (pokemon.isOnField()) {
applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null);
}
}
}
const enemyField = globalScene.getEnemyField();
const moveTargets: any[] = [globalScene.arenaEnemy, enemyField];

View File

@ -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();
}
}

View File

@ -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];

View File

@ -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();
});

View File

@ -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());

View File

@ -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) {

View File

@ -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);

View File

@ -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") {
// TODO: Move this into migrate script
switch (k) {
case "party":
case "enemyParty": {
const ret: PokemonData[] = [];
if (v === null) {
v = [];
}
for (const pd of v) {
for (const pd of v ?? []) {
ret.push(new PokemonData(pd));
}
return ret;
}
if (k === "trainer") {
case "trainer":
return v ? new TrainerData(v) : null;
}
if (k === "modifiers" || k === "enemyModifiers") {
const player = k === "modifiers";
case "modifiers":
case "enemyModifiers": {
const ret: PersistentModifierData[] = [];
if (v === null) {
v = [];
}
for (const md of 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);
}
if (
(md instanceof Modifier.EnemyAttackStatusEffectChanceModifier && md.effect === StatusEffect.FREEZE) ||
md.effect === StatusEffect.SLEEP
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, player));
ret.push(new PersistentModifierData(md, k === "modifiers"));
}
return ret;
}
if (k === "arena") {
case "arena":
return new ArenaData(v);
}
if (k === "challenges") {
case "challenges": {
const ret: ChallengeData[] = [];
if (v === null) {
v = [];
}
for (const c of v) {
for (const c of v ?? []) {
ret.push(new ChallengeData(c));
}
return ret;
}
if (k === "mysteryEncounterType") {
case "mysteryEncounterType":
return v as MysteryEncounterType;
}
if (k === "mysteryEncounterSaveData") {
case "mysteryEncounterSaveData":
return new MysteryEncounterSaveData(v);
}
default:
return v;
}
}) as SessionSaveData;
applySessionVersionMigration(sessionData);

View File

@ -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);
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;
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));
// enemy pokemon don't use instantized summon data
if (this.player) {
this.summonData = sourcePokemon?.summonData ?? source.summonData;
} else {
this.summonData.tags = [];
console.log("this.player false!");
this.summonData = new PokemonSummonData();
}
if (!sourcePokemon) {
this.summonData.moveset = source.summonData.moveset?.map(m => PokemonMove.loadMove(m));
this.summonData.tags = source.tags.map((t: any) => loadBattlerTag(t));
}
this.summonDataSpeciesFormIndex = sourcePokemon
? this.getSummonDataSpeciesFormIndex()
: source.summonDataSpeciesFormIndex;
this.battleData = sourcePokemon?.battleData ?? source.battleData;
}
}

View File

@ -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) -

View File

@ -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"));

View File

@ -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);

View File

@ -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),
);
}

View File

@ -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;
}

View File

@ -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);
});
});
});

View File

@ -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);
});

View File

@ -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);
});
});
});

View File

@ -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 () => {

View File

@ -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);
});
});

View File

@ -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];

View File

@ -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];

View File

@ -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);
});

View File

@ -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");
@ -549,6 +549,5 @@ describe("Abilities - Wimp Out", () => {
await game.phaseInterceptor.to("SelectModifierPhase");
expect(game.scene.currentBattle.waveIndex).toBe(wave + 1);
});
});

View File

@ -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 () => {

View File

@ -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;

View File

@ -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 () => {

View File

@ -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],

View File

@ -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);
});
});

View File

@ -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 () => {

View File

@ -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);

View File

@ -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);

View File

@ -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<void> {
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).

View File

@ -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 %