This commit is contained in:
Bertie690 2025-04-16 03:46:31 +00:00 committed by GitHub
commit 91e72d33e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 1852 additions and 954 deletions

2
.gitignore vendored
View File

@ -13,7 +13,7 @@ dist-ssr
*.local
# Editor directories and files
.vscode/*
.vscode
.idea
.DS_Store
*.suo

View File

@ -72,14 +72,16 @@ async function runInteractive() {
const type = typeAnswer.selectedOption.toLowerCase();
// Convert fileName from kebab-case or camelCase to snake_case
// Ex: "Cud Chew/Cud-Chew" --> "cud_chew"
const fileName = fileNameAnswer.userInput
.replace(/-+/g, "_") // Convert kebab-case (dashes) to underscores
.replace(/([a-z])([A-Z])/g, "$1_$2") // Convert camelCase to snake_case
.replace(/\s+/g, "_") // Replace spaces with underscores
.toLowerCase(); // Ensure all lowercase
// Format the description for the test case
// Get proper english name for test names and data name for abilities/moves
const formattedName = fileName.replace(/_/g, " ").replace(/\b\w/g, char => char.toUpperCase());
const attrName = fileName.toUpperCase(); // Used for move/ability tests to override with ability/move being tested
// Determine the directory based on the type
let dir;
let description;
@ -130,8 +132,8 @@ describe("${description}", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.moveset(Moves.${type === "move" ? attrName : "SPLASH"})
.ability(Abilities.${type === "ability" ? attrName : "BALL_FETCH"})
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)

View File

@ -9,14 +9,14 @@
- The best example of this is JSDoc-style comments as seen below:
- When formatted this way, the comment gets shown by intellisense in VS Code or similar IDEs just by hovering over the text!
- Functions also show each the comment for parameter as you type them, making keeping track of what each one does in lengthy functions much more clear
```js
```ts
/**
* Changes the type-based weather modifier if this move's power would be reduced by it
* @param user {@linkcode Pokemon} using this move
* @param target {@linkcode Pokemon} target of this move
* @param move {@linkcode Move} being used
* @param args [0] {@linkcode Utils.NumberHolder} for arenaAttackTypeMultiplier
* @returns true if the function succeeds
* @param user The {@linkcode Pokemon} using this move
* @param target The {@linkcode Pokemon} being targeted by this move
* @param move The {@linkcode Move} being used
* @param args `[0]` {@linkcode Utils.NumberHolder} for arenaAttackTypeMultiplier
* @returns `true` if the function succeeds
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
}
@ -46,7 +46,7 @@ If you're interested in going more in depth, you can find a reference guide for
While other classes should be fully documented, Abilities and Moves heavily incoperate inheritance (i.e. the `extends` keyword). Because of this, much of the functionality in these classes is duplicated or only slightly changed between classes.
### With this in mind, there's a few more things to keep in mind for these:
- Do not document any parameters if the function mirrors the one they extend.
- Keep this in mind for functions that are not the `apply` function as they are usually sparce and mostly reused
- Keep this in mind for functions that are not the `apply` function as they are usually sparse and mostly reused
- The class itself must be documented
- This must include the `@extends BaseClass` and `@see {@linkcode apply}` tags
- Class member variables must be documented

View File

@ -75,7 +75,13 @@ In `getNextMove()`, the enemy Pokémon chooses a move to use in the following st
As part of the move selection process, the enemy Pokémon must compute a **target score (TS)** for each legal target for each move in its move pool. The base target score for all moves is a combination of the move's **user benefit score (UBS)** and **target benefit score (TBS)**.
![equation](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7D%5Ctext%7BTS%7D=%5Ctext%7BUBS%7D+%5Ctext%7BTBS%7D%5Ctimes%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D-1&%5Ctext%7Bif%20target%20is%20an%20opponent%7D%5C%5C1&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.)
$$
\text{TS} = \text{UBS} + \text{TBS} \times
\begin{cases}
-1 & \text{if target is an opponent} \\
1 & \text{otherwise}
\end{cases}
$$
A move's UBS and TBS are computed with the respective functions in the `Move` class:
@ -96,16 +102,37 @@ In addition to the base score from `Move.getTargetBenefitScore()`, attack moves
- The move's category (Physical/Special), and whether the user has a higher Attack or Special Attack stat.
More specifically, the following steps are taken to compute the move's `attackScore`:
1. Compute a multiplier based on the move's type effectiveness:
1. Compute a multiplier based on the move's type effectiveness:
$$
\text{typeMult} =
\begin{cases}
2 & \text{if move is super effective (or better)} \\
-2 & \text{otherwise}
\end{cases}
$$
![typeMultEqn](https://latex.codecogs.com/png.image?%5Cdpi%7B110%7D%5Cbg%7Bwhite%7D%5Ctext%7BtypeMult%7D=%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D2&&%5Ctext%7Bif%20move%20is%20super%20effective(or%20better)%7D%5C%5C-2&&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.)
2. Compute a multiplier based on the move's category and the user's offensive stats:
1. Compute the user's offensive stat ratio:
![statRatioEqn](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7D%5Ctext%7BstatRatio%7D=%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%5Cfrac%7B%5Ctext%7BuserSpAtk%7D%7D%7B%5Ctext%7BuserAtk%7D%7D&%5Ctext%7Bif%20move%20is%20physical%7D%5C%5C%5Cfrac%7B%5Ctext%7BuserAtk%7D%7D%7B%5Ctext%7BuserSpAtk%7D%7D&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.)
$$
\text{statRatio} =
\begin{cases}
\frac{\text{userSpAtk}}{\text{userAtk}} & \text{if move is physical} \\
\frac{\text{userAtk}}{\text{userSpAtk}} & \text{otherwise}
\end{cases}
$$
2. Compute the stat-based multiplier:
![statMultEqn](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7D%5Ctext%7BstatMult%7D=%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D2&%5Ctext%7Bif%20statRatio%7D%5Cle%200.75%5C%5C1.5&%5Ctext%7Bif%5C;%7D0.75%5Cle%5Ctext%7BstatRatio%7D%5Cle%200.875%5C%5C1&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.)
$$
\text{statMult} =
\begin{cases}
2 & \text{if statRatio} \leq 0.75 \\
1.5 & \text{if } 0.75 \leq \text{statRatio} \leq 0.875 \\
1 & \text{otherwise}
\end{cases}
$$
3. Calculate the move's `attackScore`:
$\text{attackScore} = (\text{typeMult}\times \text{statMult})+\lfloor \frac{\text{power}}{5} \rfloor$
@ -125,13 +152,26 @@ The final step to calculate an attack move's target score (TS) is to multiply th
The enemy's target selection for single-target moves works in a very similar way to its move selection. Each potential target is given a **target selection score (TSS)** which is based on the move's [target benefit score](#calculating-move-and-target-scores) for that target:
![TSSEqn](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7D%5Ctext%7BTSS%7D=%5Ctext%7BTBS%7D%5Ctimes%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D-1&%5Ctext%7Bif%20target%20is%20an%20opponent%7D%5C%5C1&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.)
$$
\text{TSS} = \text{TBS} \times
\begin{cases}
-1 & \text{if target is an opponent} \\
1 & \text{otherwise}
\end{cases}
$$
Once the TSS is calculated for each target, the target is selected as follows:
1. Sort the targets (indexes) in decreasing order of their target selection scores (or weights). Let $t_i$ be the index of the *i*-th target in the sorted list, and let $w_i$ be that target's corresponding TSS.
2. Normalize the weights. Let $w_n$ be the lowest-weighted target in the sorted list, then:
![normWeightEqn](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7DW_i=%5Cleft%5C%7B%5Cbegin%7Bmatrix%7Dw_i+%7Cw_n%7C&%5Ctext%7Bif%5C;%7Dw_n%5C;%5Ctext%7Bis%20negative%7D%5C%5Cw_i&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.)
$$
W_i =
\begin{cases}
w_i + |w_n| & \text{if } w_n \text{ is negative} \\
w_i & \text{otherwise}
\end{cases}
$$
3. Remove all weights from the list such that $W_i < \frac{W_0}{2}$
4. Generate a random integer $R=\text{rand}(0, W_{\text{total}})$ where $W_{\text{total}}$ is the sum of all the remaining weights after Step 3.
5. For each target $(t_i, W_i)$,
@ -172,13 +212,13 @@ Based on the enemy party's matchup scores, whether or not the trainer switches o
Now that the enemy Pokémon with the best matchup score is on the field (assuming it survives Dachsbun's attack on the last turn), the enemy will now decide to have Excadrill use one of its moves. Assuming all of its moves are usable, we'll go through the target score calculations for each move:
- **Earthquake**: In a single battle, this move is just a 100-power Ground-type physical attack with no additional effects. With no additional benefit score from attributes, the move's base target score against the player's Dachsbun is just the `attackScore` from `AttackMove.getTargetBenefitScore()`. In this case, Earthquake's `attackScore` is given by
$\text{attackScore}=(\text{typeMult}\times \text{statMult}) + \lfloor \frac{\text{power}}{5} \rfloor = -2\times 2 + 20 = 16$
Here, `typeMult` is -2 because the move is not super effective, and `statMult` is 2 because Excadrill's Attack is significantly higher than its Sp. Atk. Accounting for STAB thanks to Excadrill's typing, the final target score for this move is **24**
- **Iron Head**: This move is an 80-power Steel-type physical attack with an additional chance to cause the target to flinch. With these properties, Iron Head has a user benefit score of 0 and a target benefit score given by
$\text{TBS}=\text{getTargetBenefitScore(FlinchAttr)}-\text{attackScore}$
Under its current implementation, the target benefit score of `FlinchAttr` is -5. Calculating the move's `attackScore`, we get:
@ -198,7 +238,7 @@ Now that the enemy Pokémon with the best matchup score is on the field (assumin
where `levels` is the number of stat stages added by the attribute (in this case, +2). The final score for this move is **6** (Note: because this move is self-targeted, we don't flip the sign of TBS when computing the target score).
- **Crush Claw**: This move is a 75-power Normal-type physical attack with a 50 percent chance to lower the target's Defense by one stage. The additional effect is implemented by the same `StatStageChangeAttr` as Swords Dance, so we can use the same formulas from before to compute the total TBS and base target score.
$\text{TBS}=\text{getTargetBenefitScore(StatStageChangeAttr)}-\text{attackScore}$
$\text{TBS}=(-4 + 2)-(-2\times 2 + \lfloor \frac{75}{5} \rfloor)=-2-11=-13$

View File

@ -1465,10 +1465,12 @@ export default class BattleScene extends SceneBase {
const isWaveIndexMultipleOfFiftyMinusOne = lastBattle.waveIndex % 50 === 49;
const isNewBiome =
isWaveIndexMultipleOfTen || isEndlessFifthWave || (isEndlessOrDaily && isWaveIndexMultipleOfFiftyMinusOne);
/** Whether to reset and recall pokemon */
const resetArenaState =
isNewBiome ||
[BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.currentBattle.battleType) ||
this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS;
for (const enemyPokemon of this.getEnemyParty()) {
enemyPokemon.destroy();
}
@ -1476,27 +1478,27 @@ export default class BattleScene extends SceneBase {
if (!isNewBiome && newWaveIndex % 10 === 5) {
this.arena.updatePoolsForTimeOfDay();
}
// If this is a new trainer battle or biome, recall player pokemon and reset all temporary effects
if (resetArenaState) {
this.arena.resetArenaEffects();
for (const pokemon of playerField) {
pokemon.lapseTag(BattlerTagType.COMMANDED);
}
playerField.forEach((pokemon, p) => {
pokemon.lapseTag(BattlerTagType.COMMANDED);
if (pokemon.isOnField()) {
this.pushPhase(new ReturnPhase(p));
}
});
for (const pokemon of this.getPlayerParty()) {
pokemon.resetBattleData();
pokemon.resetBattleAndWaveData();
pokemon.resetTera();
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
if (
pokemon.hasSpecies(Species.TERAPAGOS) ||
(this.gameMode.isClassic && this.currentBattle.waveIndex > 180 && this.currentBattle.waveIndex <= 190)
) {
// Reset player teras used counter if playing with Terapagos or fighting E4
this.arena.playerTerasUsed = 0;
}
}
@ -2124,12 +2126,15 @@ export default class BattleScene extends SceneBase {
}
getMaxExpLevel(ignoreLevelCap = false): number {
if (Overrides.LEVEL_CAP_OVERRIDE > 0) {
return Overrides.LEVEL_CAP_OVERRIDE;
const capOverride = Overrides.LEVEL_CAP_OVERRIDE ?? 0;
if (capOverride > 0) {
return capOverride;
}
if (ignoreLevelCap || Overrides.LEVEL_CAP_OVERRIDE < 0) {
if (ignoreLevelCap || capOverride < 0) {
return Number.MAX_SAFE_INTEGER;
}
const waveIndex = Math.ceil((this.currentBattle?.waveIndex || 1) / 10) * 10;
const difficultyWaveIndex = this.gameMode.getWaveForDifficulty(waveIndex);
const baseLevel = (1 + difficultyWaveIndex / 2 + Math.pow(difficultyWaveIndex / 25, 2)) * 1.2;
@ -2855,7 +2860,7 @@ export default class BattleScene extends SceneBase {
// adds to the end of PhaseQueuePrepend
this.unshiftPhase(phase);
} else {
//remember that pushPhase adds it to nextCommandPhaseQueue
// remember that pushPhase adds it to nextCommandPhaseQueue
this.pushPhase(phase);
}
}
@ -3043,105 +3048,93 @@ export default class BattleScene extends SceneBase {
itemLost = true,
): boolean {
const source = itemModifier.pokemonId ? itemModifier.getPokemon() : null;
const cancelled = new BooleanHolder(false);
// Check for effects that might block us from stealing
const cancelled = new BooleanHolder(false);
if (source && source.isPlayer() !== target.isPlayer()) {
applyAbAttrs(BlockItemTheftAbAttr, source, cancelled);
}
if (cancelled.value) {
return false;
}
// Create a new modifier for the reciever with the extra items
const newItemModifier = itemModifier.clone() as PokemonHeldItemModifier;
newItemModifier.pokemonId = target.id;
const matchingModifier = this.findModifier(
m => m instanceof PokemonHeldItemModifier && m.matchType(itemModifier) && m.pokemonId === target.id,
target.isPlayer(),
) as PokemonHeldItemModifier;
) as PokemonHeldItemModifier | undefined;
if (matchingModifier) {
const maxStackCount = matchingModifier.getMaxStackCount();
if (matchingModifier.stackCount >= maxStackCount) {
return false;
}
const countTaken = Math.min(
transferQuantity,
itemModifier.stackCount,
maxStackCount - matchingModifier.stackCount,
);
itemModifier.stackCount -= countTaken;
newItemModifier.stackCount = matchingModifier.stackCount + countTaken;
const countTaken = Math.min(
transferQuantity,
itemModifier.stackCount,
matchingModifier?.getCountUnderMax() ?? Number.MAX_SAFE_INTEGER,
);
if (countTaken <= 0) {
// Can't transfer negative items
return false;
}
newItemModifier.stackCount = (matchingModifier?.stackCount ?? 0) + countTaken;
// TODO: Do we need this? IDK what it does (if anything)
if (source && source.isPlayer() !== target.isPlayer() && !ignoreUpdate) {
this.updateModifiers(source.isPlayer(), instant);
}
// If the old modifier is at 0 stacks, try to remove it
if (itemModifier.stackCount <= countTaken && source && !this.removeModifier(itemModifier, !source.isPlayer())) {
// Oops! Something went wrong! **BSOD**
return false;
}
// Add the new modifier to the recieving pokemon, overriding the prior one as applicable
if (matchingModifier && !this.removeModifier(matchingModifier, !target.isPlayer())) {
return false;
}
if (target.isPlayer()) {
this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant);
} else {
const countTaken = Math.min(transferQuantity, itemModifier.stackCount);
itemModifier.stackCount -= countTaken;
newItemModifier.stackCount = countTaken;
this.addEnemyModifier(newItemModifier, ignoreUpdate, instant);
}
const removeOld = itemModifier.stackCount === 0;
if (!removeOld || !source || this.removeModifier(itemModifier, !source.isPlayer())) {
const addModifier = () => {
if (!matchingModifier || this.removeModifier(matchingModifier, !target.isPlayer())) {
if (target.isPlayer()) {
this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant);
if (source && itemLost) {
applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false);
}
return true;
}
this.addEnemyModifier(newItemModifier, ignoreUpdate, instant);
if (source && itemLost) {
applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false);
}
return true;
}
return false;
};
if (source && source.isPlayer() !== target.isPlayer() && !ignoreUpdate) {
this.updateModifiers(source.isPlayer(), instant);
addModifier();
} else {
addModifier();
}
return true;
if (source && itemLost) {
applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false);
}
return false;
return true;
}
canTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, transferQuantity = 1): boolean {
const mod = itemModifier.clone() as PokemonHeldItemModifier;
const source = mod.pokemonId ? mod.getPokemon() : null;
const cancelled = new BooleanHolder(false);
if (source && source.isPlayer() !== target.isPlayer()) {
applyAbAttrs(BlockItemTheftAbAttr, source, cancelled);
const source = itemModifier.pokemonId ? itemModifier.getPokemon() : null;
if (!source) {
// TODO: WHY DO WE RETURN TRUE IF THE ITEM HAS NO OWNER
return true;
}
const cancelled = new BooleanHolder(false);
if (source.isPlayer() !== target.isPlayer()) {
applyAbAttrs(BlockItemTheftAbAttr, source, cancelled);
}
if (cancelled.value) {
return false;
}
const matchingModifier = this.findModifier(
m => m instanceof PokemonHeldItemModifier && m.matchType(mod) && m.pokemonId === target.id,
m => m instanceof PokemonHeldItemModifier && m.matchType(itemModifier) && m.pokemonId === target.id,
target.isPlayer(),
) as PokemonHeldItemModifier;
) as PokemonHeldItemModifier | undefined;
if (matchingModifier) {
const maxStackCount = matchingModifier.getMaxStackCount();
if (matchingModifier.stackCount >= maxStackCount) {
return false;
}
const countTaken = Math.min(transferQuantity, mod.stackCount, maxStackCount - matchingModifier.stackCount);
mod.stackCount -= countTaken;
} else {
const countTaken = Math.min(transferQuantity, mod.stackCount);
mod.stackCount -= countTaken;
}
const countTaken = Math.min(
transferQuantity,
itemModifier.stackCount,
matchingModifier?.getCountUnderMax() ?? Number.MAX_SAFE_INTEGER,
);
const removeOld = mod.stackCount === 0;
return !removeOld || !source || this.hasModifier(itemModifier, !source.isPlayer());
return itemModifier.stackCount !== countTaken || this.hasModifier(itemModifier, !source.isPlayer());
}
removePartyMemberModifiers(partyMemberIndex: number): Promise<void> {
@ -3276,7 +3269,7 @@ export default class BattleScene extends SceneBase {
}
}
const modifiersClone = modifiers.slice(0);
const modifiersClone = modifiers.slice();
for (const modifier of modifiersClone) {
if (!modifier.getStackCount()) {
modifiers.splice(modifiers.indexOf(modifier), 1);
@ -3317,28 +3310,49 @@ export default class BattleScene extends SceneBase {
removeModifier(modifier: PersistentModifier, enemy = false): boolean {
const modifiers = !enemy ? this.modifiers : this.enemyModifiers;
const modifierIndex = modifiers.indexOf(modifier);
if (modifierIndex > -1) {
modifiers.splice(modifierIndex, 1);
if (modifier instanceof PokemonFormChangeItemModifier) {
const pokemon = this.getPokemonById(modifier.pokemonId);
if (pokemon) {
modifier.apply(pokemon, false);
}
}
return true;
if (modifierIndex === -1) {
return false;
}
return false;
modifiers.splice(modifierIndex, 1);
if (modifier instanceof PokemonFormChangeItemModifier) {
const pokemon = this.getPokemonById(modifier.pokemonId);
if (pokemon) {
modifier.apply(pokemon, false);
}
}
return true;
}
/**
* Get all of the modifiers that match `modifierType`
* @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier}
* @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true`
* @returns the list of all modifiers that matched `modifierType`.
* Get all modifiers of all {@linkcode Pokemon} in the given party,
* optionally filtering based on `modifierType` if provided.
* @param player Whether to search the player (`true`) or enemy (`false`) party; Defaults to `true`
* @returns a list of all modifiers on the given side of the field.
* @overload
*/
getModifiers<T extends PersistentModifier>(modifierType: Constructor<T>, player = true): T[] {
return (player ? this.modifiers : this.enemyModifiers).filter((m): m is T => m instanceof modifierType);
getModifiers(player?: boolean): PersistentModifier[];
/**
* Get all modifiers of all {@linkcode Pokemon} in the given party,
* optionally filtering based on `modifierType` if provided.
* @param modifierType The type of modifier to check against; must extend {@linkcode PersistentModifier}.
* If omitted, will return all {@linkcode PersistentModifier}s regardless of type.
* @param player Whether to search the player (`true`) or enemy (`false`) party; Defaults to `true`
* @returns a list of all modifiers matching `modifierType` on the given side of the field.
* @overload
*/
getModifiers<T extends PersistentModifier>(modifierType: Constructor<T>, player?: boolean): T[];
// NOTE: Boolean typing on 1st parameter needed to satisfy "bool only" overload
getModifiers<T extends PersistentModifier>(modifierType?: Constructor<T> | boolean, player?: boolean) {
const usePlayer: boolean = player ?? (typeof modifierType !== "boolean" || modifierType); // non-bool in 1st position = true by default
const mods = usePlayer ? this.modifiers : this.enemyModifiers;
if (typeof modifierType === "undefined" || typeof modifierType === "boolean") {
return mods;
}
return mods.filter((m): m is T => m instanceof modifierType);
}
/**

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;
}
@ -3278,13 +3283,13 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta
/**
* Conditionally provides immunity to stat drop effects to the user's field.
*
*
* Used by {@linkcode Abilities.FLOWER_VEIL | Flower Veil}.
*/
export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbAttr {
/** {@linkcode BattleStat} to protect or `undefined` if **all** {@linkcode BattleStat} are protected */
protected protectedStat?: BattleStat;
/** If the method evaluates to true, the stat will be protected. */
protected condition: (target: Pokemon) => boolean;
@ -3301,7 +3306,7 @@ export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbA
* @param stat The stat being affected
* @param cancelled Holds whether the stat change was already prevented.
* @param args Args[0] is the target pokemon of the stat change.
* @returns
* @returns
*/
override canApplyPreStatStageChange(pokemon: Pokemon, passive: boolean, simulated: boolean, stat: BattleStat, cancelled: BooleanHolder, args: [Pokemon, ...any]): boolean {
const target = args[0];
@ -3433,7 +3438,7 @@ export class BonusCritAbAttr extends AbAttr {
/**
* Apply the bonus crit ability by increasing the value in the provided number holder by 1
*
*
* @param pokemon The pokemon with the BonusCrit ability (unused)
* @param passive Unused
* @param simulated Unused
@ -3586,7 +3591,7 @@ export class PreWeatherEffectAbAttr extends AbAttr {
args: any[]): boolean {
return true;
}
applyPreWeatherEffect(
pokemon: Pokemon,
passive: boolean,
@ -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,97 @@ 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 funni animation
globalScene.unshiftPhase(
new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM),
);
// re-apply effect of all berries previously scarfed
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() {
@ -4204,7 +4292,8 @@ export class PostTurnFormChangeAbAttr extends PostTurnAbAttr {
/**
* Attribute used for abilities (Bad Dreams) that damages the opponents for being asleep
* Attribute used for abilities (Bad Dreams) that damages the opponents for being asleep.
* @extends PostTurnAbAttr
*/
export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
override canApplyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
@ -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
@ -4506,17 +4595,19 @@ export class HealFromBerryUseAbAttr extends AbAttr {
}
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, ...args: [BooleanHolder, any[]]): void {
if (simulated) {
return;
}
const { name: abilityName } = passive ? pokemon.getPassiveAbility() : pokemon.getAbility();
if (!simulated) {
globalScene.unshiftPhase(
new PokemonHealPhase(
pokemon.getBattlerIndex(),
toDmgValue(pokemon.getMaxHp() * this.healPercent),
i18next.t("abilityTriggers:healFromBerryUse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }),
true
globalScene.unshiftPhase(
new PokemonHealPhase(
pokemon.getBattlerIndex(),
toDmgValue(pokemon.getMaxHp() * this.healPercent),
i18next.t("abilityTriggers:healFromBerryUse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }),
true
)
);
}
}
}
@ -4548,7 +4639,8 @@ export class CheckTrappedAbAttr extends AbAttr {
simulated: boolean,
trapped: BooleanHolder,
otherPokemon: Pokemon,
args: any[]): boolean {
args: any[],
): boolean {
return true;
}
@ -4978,7 +5070,8 @@ export class IgnoreTypeImmunityAbAttr extends AbAttr {
}
/**
* Ignores the type immunity to Status Effects of the defender if the defender is of a certain type
* Attribute to ignore type-based immunities to Status Effect if the defender is of a certain type.
* Used by {@linkcode Abilities.CORROSION}.
*/
export class IgnoreTypeStatusEffectImmunityAbAttr extends AbAttr {
private statusEffect: StatusEffect[];
@ -5154,7 +5247,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;
@ -5162,7 +5255,7 @@ export class IllusionPreSummonAbAttr extends PreSummonAbAttr {
// If the last conscious Pokémon in the party is a Terastallized Ogerpon or Terapagos, Illusion will not activate.
// Illusion will also not activate if the Pokémon with Illusion is Terastallized and the last Pokémon in the party is Ogerpon or Terapagos.
if (
if (
lastPokemon === pokemon ||
((speciesId === Species.OGERPON || speciesId === Species.TERAPAGOS) && (lastPokemon.isTerastallized || pokemon.isTerastallized))
) {
@ -5192,7 +5285,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 +5506,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 +6371,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 qll Pokemon on field with weather-based forms
*/
function getPokemonWithWeatherBasedForms() {
return globalScene.getField(true).filter(p =>
@ -6741,8 +6828,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 +6949,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 +7195,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)),
@ -7186,7 +7272,7 @@ export function initAbilities() {
new Ability(Abilities.WANDERING_SPIRIT, 8)
.attr(PostDefendAbilitySwapAbAttr)
.bypassFaint()
.edgeCase(), // interacts incorrectly with rock head. It's meant to switch abilities before recoil would apply so that a pokemon with rock head would lose rock head first and still take the recoil
.edgeCase(), // interacts incorrectly with rock head. It's meant to switch abilities before recoil would apply so that a pokemon with rock head would lose rock head first and still take the recoil
new Ability(Abilities.GORILLA_TACTICS, 8)
.attr(GorillaTacticsAbAttr),
new Ability(Abilities.NEUTRALIZING_GAS, 8)
@ -7326,7 +7412,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,6 +91,10 @@ export class BattlerTag {
onOverlap(_pokemon: Pokemon): void {}
/**
* Tick down a given BattlerTag.
* @returns `true` if the tag should be removed (`turnsLeft <= 0`)
*/
lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
return --this.turnCount > 0;
}
@ -739,31 +744,33 @@ export class ConfusedTag extends BattlerTag {
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
const ret = lapseType !== BattlerTagLapseType.CUSTOM && super.lapse(pokemon, lapseType);
const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM && super.lapse(pokemon, lapseType);
if (ret) {
globalScene.queueMessage(
i18next.t("battlerTags:confusedLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
globalScene.unshiftPhase(new CommonAnimPhase(pokemon.getBattlerIndex(), undefined, CommonAnim.CONFUSION));
// 1/3 chance of hitting self with a 40 base power move
if (pokemon.randSeedInt(3) === 0) {
const atk = pokemon.getEffectiveStat(Stat.ATK);
const def = pokemon.getEffectiveStat(Stat.DEF);
const damage = toDmgValue(
((((2 * pokemon.level) / 5 + 2) * 40 * atk) / def / 50 + 2) * (pokemon.randSeedIntRange(85, 100) / 100),
);
globalScene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
pokemon.damageAndUpdate(damage, { result: HitResult.CONFUSION });
pokemon.battleData.hitCount++;
(globalScene.getCurrentPhase() as MovePhase).cancel();
}
if (!shouldLapse) {
return false;
}
return ret;
globalScene.queueMessage(
i18next.t("battlerTags:confusedLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
globalScene.unshiftPhase(new CommonAnimPhase(pokemon.getBattlerIndex(), undefined, CommonAnim.CONFUSION));
// 1/3 chance of hitting self with a 40 base power move
if (pokemon.randSeedInt(3) === 0 || Overrides.CONFUSION_ACTIVATION_OVERRIDE === true) {
const atk = pokemon.getEffectiveStat(Stat.ATK);
const def = pokemon.getEffectiveStat(Stat.DEF);
const damage = toDmgValue(
((((2 * pokemon.level) / 5 + 2) * 40 * atk) / def / 50 + 2) * (pokemon.randSeedIntRange(85, 100) / 100),
);
// Intentionally don't increment rage fist's hitCount
globalScene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
pokemon.damageAndUpdate(damage, { result: HitResult.CONFUSION });
(globalScene.getCurrentPhase() as MovePhase).cancel();
}
return true;
}
getDescriptor(): string {

View File

@ -73,94 +73,92 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate {
export type BerryEffectFunc = (pokemon: Pokemon, berryOwner?: Pokemon) => void;
export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
switch (berryType) {
case BerryType.SITRUS:
case BerryType.ENIGMA:
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType);
}
const hpHealed = new NumberHolder(toDmgValue(pokemon.getMaxHp() / 4));
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed);
globalScene.unshiftPhase(
new PokemonHealPhase(
pokemon.getBattlerIndex(),
hpHealed.value,
i18next.t("battle:hpHealBerry", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
berryName: getBerryName(berryType),
}),
true,
),
);
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
};
case BerryType.LUM:
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType);
}
if (pokemon.status) {
globalScene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon)));
}
pokemon.resetStatus(true, true);
pokemon.updateInfo();
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
};
case BerryType.LIECHI:
case BerryType.GANLON:
case BerryType.PETAYA:
case BerryType.APICOT:
case BerryType.SALAC:
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType);
}
// Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth
const stat: BattleStat = berryType - BerryType.ENIGMA;
const statStages = new NumberHolder(1);
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages);
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, [stat], statStages.value));
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
};
case BerryType.LANSAT:
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType);
}
pokemon.addTag(BattlerTagType.CRIT_BOOST);
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
};
case BerryType.STARF:
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType);
}
const randStat = randSeedInt(Stat.SPD, Stat.ATK);
const stages = new NumberHolder(2);
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages);
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, [randStat], stages.value));
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
};
case BerryType.LEPPA:
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType);
}
const ppRestoreMove = pokemon.getMoveset().find(m => !m.getPpRatio())
? pokemon.getMoveset().find(m => !m.getPpRatio())
: pokemon.getMoveset().find(m => m.getPpRatio() < 1);
if (ppRestoreMove !== undefined) {
ppRestoreMove!.ppUsed = Math.max(ppRestoreMove!.ppUsed - 10, 0);
globalScene.queueMessage(
i18next.t("battle:ppHealBerry", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
moveName: ppRestoreMove!.getName(),
berryName: getBerryName(berryType),
}),
return (consumer: Pokemon, berryOwner: Pokemon = consumer) => {
// Apply an effect pertaining to what berry we're using
switch (berryType) {
case BerryType.SITRUS:
case BerryType.ENIGMA:
{
const hpHealed = new NumberHolder(toDmgValue(consumer.getMaxHp() / 4));
applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, hpHealed);
globalScene.unshiftPhase(
new PokemonHealPhase(
consumer.getBattlerIndex(),
hpHealed.value,
i18next.t("battle:hpHealBerry", {
pokemonNameWithAffix: getPokemonNameWithAffix(consumer),
berryName: getBerryName(berryType),
}),
true,
),
);
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
}
};
}
break;
case BerryType.LUM:
{
if (consumer.status) {
globalScene.queueMessage(
getStatusEffectHealText(consumer.status.effect, getPokemonNameWithAffix(consumer)),
);
}
consumer.resetStatus(true, true);
consumer.updateInfo();
}
break;
case BerryType.LIECHI:
case BerryType.GANLON:
case BerryType.PETAYA:
case BerryType.APICOT:
case BerryType.SALAC:
{
// Offset BerryType such that LIECHI --> Stat.ATK = 1, GANLON --> Stat.DEF = 2, so on and so forth
const stat: BattleStat = berryType - BerryType.ENIGMA;
const statStages = new NumberHolder(1);
applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, statStages);
globalScene.unshiftPhase(
new StatStageChangePhase(consumer.getBattlerIndex(), true, [stat], statStages.value),
);
}
break;
case BerryType.LANSAT:
{
consumer.addTag(BattlerTagType.CRIT_BOOST);
}
break;
case BerryType.STARF:
{
const randStat = randSeedInt(Stat.SPD, Stat.ATK);
const stages = new NumberHolder(2);
applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, stages);
globalScene.unshiftPhase(
new StatStageChangePhase(consumer.getBattlerIndex(), true, [randStat], stages.value),
);
}
break;
case BerryType.LEPPA:
{
const ppRestoreMove =
consumer.getMoveset().find(m => !m.getPpRatio()) ?? consumer.getMoveset().find(m => m.getPpRatio() < 1);
if (ppRestoreMove !== undefined) {
ppRestoreMove!.ppUsed = Math.max(ppRestoreMove!.ppUsed - 10, 0);
globalScene.queueMessage(
i18next.t("battle:ppHealBerry", {
pokemonNameWithAffix: getPokemonNameWithAffix(consumer),
moveName: ppRestoreMove!.getName(),
berryName: getBerryName(berryType),
}),
);
}
}
break;
default:
console.error("Incorrect BerryType %d passed to GetBerryEffectFunc", berryType);
}
// Trigger unburden on the mon losing the berry
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner, false);
};
}

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

@ -649,7 +649,7 @@ export default class Move implements Localizable {
break;
case MoveFlags.IGNORE_ABILITIES:
if (user.hasAbilityWithAttr(MoveAbilityBypassAbAttr)) {
const abilityEffectsIgnored = new BooleanHolder(false);
const abilityEffectsIgnored = new BooleanHolder(false);
applyAbAttrs(MoveAbilityBypassAbAttr, user, abilityEffectsIgnored, false, this);
if (abilityEffectsIgnored.value) {
return true;
@ -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;
}
@ -2706,48 +2711,60 @@ export class EatBerryAttr extends MoveEffectAttr {
}
eatBerry(consumer: Pokemon, berryOwner?: Pokemon) {
getBerryEffectFunc(this.chosenBerry!.berryType)(consumer, berryOwner); // consumer eats the berry
// consumer eats the berry
getBerryEffectFunc(this.chosenBerry!.berryType)(consumer, berryOwner);
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 +4136,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 +8044,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 +8686,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 +9396,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 +10020,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 +11147,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

@ -60,7 +60,7 @@ export const modifierSortFunc = (a: Modifier, b: Modifier): number => {
const aId = a instanceof PokemonHeldItemModifier && a.pokemonId ? a.pokemonId : 4294967295;
const bId = b instanceof PokemonHeldItemModifier && b.pokemonId ? b.pokemonId : 4294967295;
//First sort by pokemonID
// First sort by pokemonID
if (aId < bId) {
return 1;
}
@ -734,7 +734,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
return 1;
}
getMaxStackCount(forThreshold?: boolean): number {
getMaxStackCount(forThreshold = false): number {
const pokemon = this.getPokemon();
if (!pokemon) {
return 0;
@ -748,6 +748,10 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
return this.getMaxHeldItemCount(pokemon);
}
getCountUnderMax(): number {
return this.getMaxHeldItemCount() - this.getStackCount();
}
abstract getMaxHeldItemCount(pokemon?: Pokemon): number;
}
@ -1644,8 +1648,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 +1870,14 @@ 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 time!
getBerryEffectFunc(this.berryType)(pokemon);
if (!preserve.value) {
this.consumed = true;
}
// 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 +1916,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 +3720,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

@ -75,9 +75,12 @@ class DefaultOverrides {
readonly ARENA_TINT_OVERRIDE: TimeOfDay | null = null;
/** Multiplies XP gained by this value including 0. Set to null to ignore the override. */
readonly XP_MULTIPLIER_OVERRIDE: number | null = null;
/** Sets the level cap to this number during experience gain calculations. Set to `0` to disable override & use normal wave-based level caps,
or any negative number to set it to 9 quadrillion (effectively disabling it). */
readonly LEVEL_CAP_OVERRIDE: number = 0;
/**
Sets the level cap to this number during experience gain calculations.
Set to `0` or `null` to disable override & use normal wave-based level caps,
or any negative number to set it to 9 quadrillion (effectively disabling it).
*/
readonly LEVEL_CAP_OVERRIDE: number | null = null;
readonly NEVER_CRIT_OVERRIDE: boolean = false;
/** default 1000 */
readonly STARTING_MONEY_OVERRIDE: number = 0;
@ -102,8 +105,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 => {
const hasUsableBerry = !!globalScene.findModifier(m => {
return m instanceof BerryModifier && m.shouldApply(pokemon);
}, pokemon.isPlayer());
if (hasUsableBerry) {
const cancelled = new BooleanHolder(false);
pokemon.getOpponents().map(opp => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled));
if (cancelled.value) {
globalScene.queueMessage(
i18next.t("abilityTriggers:preventBerryUse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
} else {
globalScene.unshiftPhase(
new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM),
);
for (const berryModifier of globalScene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon)) {
if (berryModifier.consumed) {
berryModifier.consumed = false;
pokemon.loseHeldItem(berryModifier);
}
globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); // Announce a berry was used
}
globalScene.updateModifiers(pokemon.isPlayer());
applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new BooleanHolder(false));
}
}
this.eatBerries(pokemon);
applyAbAttrs(RepeatBerryNextTurnAbAttr, pokemon, null);
});
this.end();
}
/**
* Attempt to eat all of a given {@linkcode Pokemon}'s berries once.
* @param pokemon The {@linkcode Pokemon} to check
*/
eatBerries(pokemon: Pokemon): void {
// check if we even have anything to eat
const hasUsableBerry = !!globalScene.findModifier(m => {
return m instanceof BerryModifier && m.shouldApply(pokemon);
}, pokemon.isPlayer());
if (!hasUsableBerry) {
return;
}
// Check if any opponents have unnerve to block us from eating berries
const cancelled = new BooleanHolder(false);
pokemon.getOpponents().map(opp => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled));
if (cancelled.value) {
globalScene.queueMessage(
i18next.t("abilityTriggers:preventBerryUse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
return;
}
// Play every endless player's least favorite animation
globalScene.unshiftPhase(
new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM),
);
// try to apply all berry modifiers for this pokemon
for (const berryModifier of globalScene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon)) {
if (berryModifier.consumed) {
berryModifier.consumed = false;
pokemon.loseHeldItem(berryModifier);
}
// No need to track berries being eaten; already done inside applyModifiers
globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier));
}
// update held modifiers and such
globalScene.updateModifiers(pokemon.isPlayer());
// Abilities.CHEEK_POUCH only works once per round of nom noms
applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new BooleanHolder(false));
}
}

View File

@ -107,12 +107,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 +128,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 +329,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

@ -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,17 +7,16 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase {
doEncounter(): void {
globalScene.playBgm(undefined, true);
// reset all battle data, perform form changes, etc.
for (const pokemon of globalScene.getPlayerParty()) {
if (pokemon) {
pokemon.resetBattleData();
pokemon.customPokemonData.resetHitReceivedCount();
pokemon.resetBattleAndWaveData();
if (pokemon.isOnField()) {
applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null);
}
}
}
for (const pokemon of globalScene.getPlayerParty().filter(p => p.isOnField())) {
applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null);
}
const enemyField = globalScene.getEnemyField();
const moveTargets: any[] = [globalScene.arenaEnemy, enemyField];
const mysteryEncounter = globalScene.currentBattle?.mysteryEncounter?.introVisuals;

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

@ -1014,8 +1014,8 @@ export class GameData {
gameMode: globalScene.gameMode.modeId,
party: globalScene.getPlayerParty().map(p => new PokemonData(p)),
enemyParty: globalScene.getEnemyParty().map(p => new PokemonData(p)),
modifiers: globalScene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)),
enemyModifiers: globalScene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)),
modifiers: globalScene.getModifiers().map(m => new PersistentModifierData(m, true)),
enemyModifiers: globalScene.getModifiers(false).map(m => new PersistentModifierData(m, false)),
arena: new ArenaData(globalScene.arena),
pokeballCounts: globalScene.pokeballCounts,
money: Math.floor(globalScene.money),

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,61 @@ 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;
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 ?? false;
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 +124,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 +133,44 @@ export default class PokemonData {
? new CustomPokemonData(source.fusionMysteryEncounterPokemonData)
: null;
this.moveset =
sourcePokemon?.moveset ??
(source.moveset || [new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL)])
.filter((m: any) => !!m)
.map((m: any) => new PokemonMove(m.moveId, m.ppUsed, m.ppUp, m.virtual, m.maxPpOverride));
if (!forHistory) {
this.levelExp = source.levelExp;
this.hp = source.hp;
this.pauseEvolutions = !!source.pauseEvolutions;
this.evoCounter = source.evoCounter ?? 0;
this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss);
this.bossSegments = source.bossSegments;
}
if (sourcePokemon) {
this.moveset = sourcePokemon.moveset;
if (!forHistory) {
this.status = sourcePokemon.status;
if (this.player && sourcePokemon.summonData) {
this.summonData = sourcePokemon.summonData;
this.summonDataSpeciesFormIndex = this.getSummonDataSpeciesFormIndex();
}
}
} else {
this.moveset = (source.moveset || [new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL)])
.filter(m => m)
.map((m: any) => new PokemonMove(m.moveId, m.ppUsed, m.ppUp, m.virtual, m.maxPpOverride));
if (!forHistory) {
this.status = source.status
this.status =
sourcePokemon?.status ??
(source.status
? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining)
: null;
: null);
// enemy pokemon don't use instantized summon data
if (this.player) {
this.summonData = sourcePokemon?.summonData ?? source.summonData;
} else {
console.log("GPIGIPGIPGIOPGIPIGPIGPTOGEw");
this.summonData = new PokemonSummonData();
}
this.summonData = new PokemonSummonData();
if (!forHistory && source.summonData) {
this.summonData.stats = source.summonData.stats;
this.summonData.statStages = source.summonData.statStages;
this.summonData.moveQueue = source.summonData.moveQueue;
this.summonData.abilitySuppressed = source.summonData.abilitySuppressed;
this.summonData.abilitiesApplied = source.summonData.abilitiesApplied;
this.summonData.ability = source.summonData.ability;
if (!sourcePokemon) {
this.summonData.moveset = source.summonData.moveset?.map(m => PokemonMove.loadMove(m));
this.summonData.types = source.summonData.types;
this.summonData.speciesForm = source.summonData.speciesForm;
this.summonDataSpeciesFormIndex = source.summonDataSpeciesFormIndex;
this.summonData.illusionBroken = source.summonData.illusionBroken;
if (source.summonData.tags) {
this.summonData.tags = source.summonData.tags?.map(t => loadBattlerTag(t));
} else {
this.summonData.tags = [];
}
this.summonData.tags = source.tags.map((t: any) => loadBattlerTag(t));
}
this.summonDataSpeciesFormIndex = sourcePokemon
? this.getSummonDataSpeciesFormIndex()
: source.summonDataSpeciesFormIndex;
this.battleData = sourcePokemon?.battleData ?? source.battleData;
}
}

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,7 +305,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
const effectiveness = opponent.getMoveEffectiveness(
pokemon,
pokemonMove.getMove(),
!opponent.battleData?.abilityRevealed,
!opponent.waveData.abilityRevealed,
undefined,
undefined,
true
@ -353,7 +353,7 @@ 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

@ -591,7 +591,7 @@ export function getLocalizedSpriteKey(baseKey: string) {
* @returns `true` if number is **inclusive** between min and max
*/
export function isBetween(num: number, min: number, max: number): boolean {
return num >= min && num <= max;
return min <= num && num <= max;
}
/**

View File

@ -22,7 +22,7 @@ describe("Abilities - Corrosion", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.SPLASH])
.moveset(Moves.SPLASH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.GRIMER)
@ -30,17 +30,28 @@ describe("Abilities - Corrosion", () => {
.enemyMoveset(Moves.TOXIC);
});
it("If a Poison- or Steel-type Pokémon with this Ability poisons a target with Synchronize, Synchronize does not gain the ability to poison Poison- or Steel-type Pokémon.", async () => {
game.override.ability(Abilities.SYNCHRONIZE);
it("allows poisoning of Poison and Steel-type Pokémon", async () => {
await game.classicMode.startBattle([Species.FEEBAS]);
const playerPokemon = game.scene.getPlayerPokemon();
const enemyPokemon = game.scene.getEnemyPokemon();
expect(playerPokemon!.status).toBeUndefined();
const playerPokemon = game.scene.getPlayerPokemon()!;
expect(playerPokemon.status).toBeUndefined();
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(playerPokemon!.status).toBeDefined();
expect(enemyPokemon!.status).toBeUndefined();
expect(playerPokemon.status).toBeDefined();
});
it("does not grant Synchronize the ability to poison Poison- or Steel-type Pokémon", async () => {
game.override.ability(Abilities.SYNCHRONIZE);
await game.classicMode.startBattle([Species.FEEBAS]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(playerPokemon.status).toBeUndefined();
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(playerPokemon.status).toBeDefined();
expect(enemyPokemon.status).toBeUndefined();
});
});

View File

@ -0,0 +1,272 @@
import { RepeatBerryNextTurnAbAttr } from "#app/data/abilities/ability";
import { getBerryEffectFunc } from "#app/data/berry";
import Pokemon from "#app/field/pokemon";
import { toDmgValue } 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 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.phaseInterceptor.to("BerryPhase");
// berries tracked in turnData; not moved to battleData yet
expect(farigiraf.summonData.berriesEatenLast).toEqual([
BerryType.PETAYA,
BerryType.PETAYA,
BerryType.PETAYA,
BerryType.LIECHI,
]);
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("resets array on switch", async () => {
await game.classicMode.startBattle([Species.FARIGIRAF, Species.GIRAFARIG]);
const farigiraf = game.scene.getPlayerPokemon()!;
farigiraf.hp = 1; // needed to allow sitrus procs
// 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; // needed to allow sitrus procs
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; // needed to allow sitrus procs
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; // needed to allow sitrus procs
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; // needed to allow sitrus procs without dying
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();
expect(farigiraf.hp).toBeGreaterThanOrEqual(4 * toDmgValue(farigiraf.getMaxHp() / 4));
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; // needed to allow sitrus procs without dying
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(Math.trunc(farigiraf.getMaxHp() / 4));
// reload and the berry should still be there...?
await game.reload.reloadSession();
const farigirafReloaded = game.scene.getPlayerPokemon()!;
expect(farigirafReloaded.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]);
// blow up next wave to make sure it works funsies
game.move.select(Moves.HYPER_VOICE);
await game.toNextWave();
expect(farigirafReloaded.hp).toBeGreaterThanOrEqual(2 * toDmgValue(farigiraf.getMaxHp() / 4));
});
});
});

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

@ -23,7 +23,7 @@ describe("Abilities - Immunity", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.moveset([Moves.SPLASH])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
@ -33,12 +33,12 @@ describe("Abilities - Immunity", () => {
});
it("should remove poison when gained", async () => {
game.override.ability(Abilities.IMMUNITY)
game.override
.ability(Abilities.IMMUNITY)
.enemyAbility(Abilities.BALL_FETCH)
.moveset(Moves.SKILL_SWAP)
.enemyMoveset(Moves.SPLASH),
await game.classicMode.startBattle([ Species.FEEBAS ]);
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.FEEBAS]);
const enemy = game.scene.getEnemyPokemon();
enemy?.trySetStatus(StatusEffect.POISON);
expect(enemy?.status?.effect).toBe(StatusEffect.POISON);

View File

@ -4,7 +4,7 @@ import GameManager from "#test/testUtils/gameManager";
import { Species } from "#enums/species";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { Stat, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
import { Stat, EFFECTIVE_STATS } from "#enums/stat";
import { Abilities } from "#enums/abilities";
// TODO: Add more tests once Imposter is fully implemented
@ -53,25 +53,11 @@ describe("Abilities - Imposter", () => {
expect(player.getStat(s, false)).toBe(enemy.getStat(s, false));
}
for (const s of BATTLE_STATS) {
expect(player.getStatStage(s)).toBe(enemy.getStatStage(s));
}
expect(player.getStatStages()).toEqual(enemy.getStatStages());
const playerMoveset = player.getMoveset();
const enemyMoveset = player.getMoveset();
expect(player.getMoveset().map(m => m.moveId)).toEqual(enemy.getMoveset().map(m => m.moveId));
expect(playerMoveset.length).toBe(enemyMoveset.length);
for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) {
expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId);
}
const playerTypes = player.getTypes();
const enemyTypes = enemy.getTypes();
expect(playerTypes.length).toBe(enemyTypes.length);
for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) {
expect(playerTypes[i]).toBe(enemyTypes[i]);
}
expect(player.getTypes()).toEqual(enemy.getTypes());
});
it("should copy in-battle overridden stats", async () => {
@ -133,7 +119,6 @@ describe("Abilities - Imposter", () => {
await game.classicMode.startBattle([Species.DITTO]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SPLASH);
@ -146,7 +131,7 @@ describe("Abilities - Imposter", () => {
await game.reload.reloadSession();
const playerReloaded = game.scene.getPlayerPokemon()!;
const playerMoveset = player.getMoveset();
const playerMoveset = playerReloaded.getMoveset();
expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId);
expect(playerReloaded.getAbility()).toBe(enemy.getAbility());

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

@ -23,7 +23,7 @@ describe("Abilities - Insomnia", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.moveset([Moves.SPLASH])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
@ -33,12 +33,12 @@ describe("Abilities - Insomnia", () => {
});
it("should remove sleep when gained", async () => {
game.override.ability(Abilities.INSOMNIA)
game.override
.ability(Abilities.INSOMNIA)
.enemyAbility(Abilities.BALL_FETCH)
.moveset(Moves.SKILL_SWAP)
.enemyMoveset(Moves.SPLASH),
await game.classicMode.startBattle([ Species.FEEBAS ]);
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.FEEBAS]);
const enemy = game.scene.getEnemyPokemon();
enemy?.trySetStatus(StatusEffect.SLEEP);
expect(enemy?.status?.effect).toBe(StatusEffect.SLEEP);

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

@ -23,7 +23,7 @@ describe("Abilities - Limber", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.moveset([Moves.SPLASH])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
@ -33,12 +33,12 @@ describe("Abilities - Limber", () => {
});
it("should remove paralysis when gained", async () => {
game.override.ability(Abilities.LIMBER)
game.override
.ability(Abilities.LIMBER)
.enemyAbility(Abilities.BALL_FETCH)
.moveset(Moves.SKILL_SWAP)
.enemyMoveset(Moves.SPLASH),
await game.classicMode.startBattle([ Species.FEEBAS ]);
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.FEEBAS]);
const enemy = game.scene.getEnemyPokemon();
enemy?.trySetStatus(StatusEffect.PARALYSIS);
expect(enemy?.status?.effect).toBe(StatusEffect.PARALYSIS);

View File

@ -23,7 +23,7 @@ describe("Abilities - Magma Armor", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.moveset([Moves.SPLASH])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
@ -33,12 +33,12 @@ describe("Abilities - Magma Armor", () => {
});
it("should remove freeze when gained", async () => {
game.override.ability(Abilities.MAGMA_ARMOR)
game.override
.ability(Abilities.MAGMA_ARMOR)
.enemyAbility(Abilities.BALL_FETCH)
.moveset(Moves.SKILL_SWAP)
.enemyMoveset(Moves.SPLASH),
await game.classicMode.startBattle([ Species.FEEBAS ]);
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.FEEBAS]);
const enemy = game.scene.getEnemyPokemon();
enemy?.trySetStatus(StatusEffect.FREEZE);
expect(enemy?.status?.effect).toBe(StatusEffect.FREEZE);

View File

@ -24,7 +24,7 @@ describe("Abilities - Mold Breaker", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.moveset([Moves.SPLASH])
.ability(Abilities.MOLD_BREAKER)
.battleType("single")
.disableCrits()
@ -34,17 +34,18 @@ describe("Abilities - Mold Breaker", () => {
});
it("should turn off the ignore abilities arena variable after the user's move", async () => {
game.override.enemyMoveset(Moves.SPLASH)
game.override
.enemyMoveset(Moves.SPLASH)
.ability(Abilities.MOLD_BREAKER)
.moveset([ Moves.ERUPTION ])
.moveset([Moves.ERUPTION])
.startingLevel(100)
.enemyLevel(2);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
await game.classicMode.startBattle([Species.MAGIKARP]);
const enemy = game.scene.getEnemyPokemon()!;
expect(enemy.isFainted()).toBe(false);
game.move.select(Moves.SPLASH);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEndPhase", true);
expect(globalScene.arena.ignoreAbilities).toBe(false);
});

View File

@ -23,7 +23,7 @@ describe("Abilities - Oblivious", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.moveset([Moves.SPLASH])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
@ -33,12 +33,12 @@ describe("Abilities - Oblivious", () => {
});
it("should remove taunt when gained", async () => {
game.override.ability(Abilities.OBLIVIOUS)
game.override
.ability(Abilities.OBLIVIOUS)
.enemyAbility(Abilities.BALL_FETCH)
.moveset(Moves.SKILL_SWAP)
.enemyMoveset(Moves.SPLASH),
await game.classicMode.startBattle([ Species.FEEBAS ]);
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.FEEBAS]);
const enemy = game.scene.getEnemyPokemon();
enemy?.addTag(BattlerTagType.TAUNT);
expect(enemy?.getTag(BattlerTagType.TAUNT)).toBeTruthy();
@ -50,12 +50,12 @@ describe("Abilities - Oblivious", () => {
});
it("should remove infatuation when gained", async () => {
game.override.ability(Abilities.OBLIVIOUS)
game.override
.ability(Abilities.OBLIVIOUS)
.enemyAbility(Abilities.BALL_FETCH)
.moveset(Moves.SKILL_SWAP)
.enemyMoveset(Moves.SPLASH),
await game.classicMode.startBattle([ Species.FEEBAS ]);
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.FEEBAS]);
const enemy = game.scene.getEnemyPokemon();
vi.spyOn(enemy!, "isOppositeGender").mockReturnValue(true);
enemy?.addTag(BattlerTagType.INFATUATED, 5, Moves.JUDGMENT, game.scene.getPlayerPokemon()?.id); // sourceID needs to be defined

View File

@ -23,7 +23,7 @@ describe("Abilities - Own Tempo", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.moveset([Moves.SPLASH])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
@ -33,12 +33,12 @@ describe("Abilities - Own Tempo", () => {
});
it("should remove confusion when gained", async () => {
game.override.ability(Abilities.OWN_TEMPO)
game.override
.ability(Abilities.OWN_TEMPO)
.enemyAbility(Abilities.BALL_FETCH)
.moveset(Moves.SKILL_SWAP)
.enemyMoveset(Moves.SPLASH),
await game.classicMode.startBattle([ Species.FEEBAS ]);
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.FEEBAS]);
const enemy = game.scene.getEnemyPokemon();
enemy?.addTag(BattlerTagType.CONFUSED);
expect(enemy?.getTag(BattlerTagType.CONFUSED)).toBeTruthy();

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

@ -23,7 +23,7 @@ describe("Abilities - Thermal Exchange", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.moveset([Moves.SPLASH])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
@ -33,12 +33,12 @@ describe("Abilities - Thermal Exchange", () => {
});
it("should remove burn when gained", async () => {
game.override.ability(Abilities.THERMAL_EXCHANGE)
game.override
.ability(Abilities.THERMAL_EXCHANGE)
.enemyAbility(Abilities.BALL_FETCH)
.moveset(Moves.SKILL_SWAP)
.enemyMoveset(Moves.SPLASH),
await game.classicMode.startBattle([ Species.FEEBAS ]);
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.FEEBAS]);
const enemy = game.scene.getEnemyPokemon();
enemy?.trySetStatus(StatusEffect.BURN);
expect(enemy?.status?.effect).toBe(StatusEffect.BURN);

View File

@ -23,7 +23,7 @@ describe("Abilities - Vital Spirit", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.moveset([Moves.SPLASH])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
@ -33,12 +33,12 @@ describe("Abilities - Vital Spirit", () => {
});
it("should remove sleep when gained", async () => {
game.override.ability(Abilities.INSOMNIA)
game.override
.ability(Abilities.INSOMNIA)
.enemyAbility(Abilities.BALL_FETCH)
.moveset(Moves.SKILL_SWAP)
.enemyMoveset(Moves.SPLASH),
await game.classicMode.startBattle([ Species.FEEBAS ]);
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.FEEBAS]);
const enemy = game.scene.getEnemyPokemon();
enemy?.trySetStatus(StatusEffect.SLEEP);
expect(enemy?.status?.effect).toBe(StatusEffect.SLEEP);

View File

@ -23,7 +23,7 @@ describe("Abilities - Water Bubble", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.moveset([Moves.SPLASH])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
@ -33,12 +33,12 @@ describe("Abilities - Water Bubble", () => {
});
it("should remove burn when gained", async () => {
game.override.ability(Abilities.THERMAL_EXCHANGE)
game.override
.ability(Abilities.THERMAL_EXCHANGE)
.enemyAbility(Abilities.BALL_FETCH)
.moveset(Moves.SKILL_SWAP)
.enemyMoveset(Moves.SPLASH),
await game.classicMode.startBattle([ Species.FEEBAS ]);
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.FEEBAS]);
const enemy = game.scene.getEnemyPokemon();
enemy?.trySetStatus(StatusEffect.BURN);
expect(enemy?.status?.effect).toBe(StatusEffect.BURN);

View File

@ -6,6 +6,7 @@ import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
// TODO? Do we _really_ need 10 test files that just test for status immunity using the same attribute?
describe("Abilities - Water Veil", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
@ -23,7 +24,7 @@ describe("Abilities - Water Veil", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.moveset([Moves.SPLASH])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
@ -33,12 +34,12 @@ describe("Abilities - Water Veil", () => {
});
it("should remove burn when gained", async () => {
game.override.ability(Abilities.THERMAL_EXCHANGE)
game.override
.ability(Abilities.THERMAL_EXCHANGE)
.enemyAbility(Abilities.BALL_FETCH)
.moveset(Moves.SKILL_SWAP)
.enemyMoveset(Moves.SPLASH),
await game.classicMode.startBattle([ Species.FEEBAS ]);
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.FEEBAS]);
const enemy = game.scene.getEnemyPokemon();
enemy?.trySetStatus(StatusEffect.BURN);
expect(enemy?.status?.effect).toBe(StatusEffect.BURN);

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");
@ -534,12 +534,12 @@ describe("Abilities - Wimp Out", () => {
.enemyAbility(Abilities.WIMP_OUT)
.startingLevel(50)
.enemyLevel(1)
.enemyMoveset([ Moves.SPLASH, Moves.ENDURE ])
.enemyMoveset([Moves.SPLASH, Moves.ENDURE])
.battleType("double")
.moveset([ Moves.DRAGON_ENERGY, Moves.SPLASH ])
.moveset([Moves.DRAGON_ENERGY, Moves.SPLASH])
.startingWave(wave);
await game.classicMode.startBattle([ Species.REGIDRAGO, Species.MAGIKARP ]);
await game.classicMode.startBattle([Species.REGIDRAGO, Species.MAGIKARP]);
// turn 1
game.move.select(Moves.DRAGON_ENERGY, 0);
@ -549,6 +549,5 @@ describe("Abilities - Wimp Out", () => {
await game.phaseInterceptor.to("SelectModifierPhase");
expect(game.scene.currentBattle.waveIndex).toBe(wave + 1);
});
});

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

@ -347,7 +347,10 @@ export default class GameManager {
}
}
/** Emulate selecting a modifier (item) */
/**
* Emulate selecting a modifier (item) the next time the {@linkcode SelectModifierPhase} occurs.
* Does not actually take anything (in fact, it just skips grabbing an item).
*/
doSelectModifier() {
this.onNextPrompt(
"SelectModifierPhase",
@ -421,7 +424,9 @@ export default class GameManager {
await this.phaseInterceptor.to(CommandPhase);
}
/** Emulate selecting a modifier (item) and transition to the next upcoming {@linkcode CommandPhase} */
/**
* Emulate selecting a modifier (item) and transition to the next upcoming {@linkcode CommandPhase}.
*/
async toNextWave() {
this.doSelectModifier();
@ -512,8 +517,9 @@ export default class GameManager {
}
/**
* Command an in-battle switch to another Pokemon via the main battle menu.
* @param pokemonIndex the index of the pokemon in your party to switch to
* Command an in-battle switch to another {@linkcode Pokemon} via the main battle menu.
* @param pokemonIndex the 0-indexed position of the party pokemon to switch to.
* Should generally never be called with 0 as that will just select your current active pokemon.
*/
doSwitchPokemon(pokemonIndex: number) {
this.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
@ -540,7 +546,7 @@ export default class GameManager {
* of the party UI, where you just need to navigate to a party slot and press
* Action twice - navigating any menus that come up after you select a party member
* is not supported.
* @param slot the index of the pokemon in your party to switch to
* @param slot the 0-indexed position of the pokemon in your party to switch to
* @param inPhase Which phase to expect the selection to occur in. Typically
* non-command switch actions happen in SwitchPhase.
*/
@ -575,7 +581,8 @@ export default class GameManager {
/**
* Intercepts `TurnStartPhase` and mocks {@linkcode TurnStartPhase.getSpeedOrder}'s return value.
* Used to manually modify Pokemon turn order.
* Note: This *DOES NOT* account for priority, only speed.
* Note: This *DOES NOT* account for priority.
* @param {BattlerIndex[]} order The turn order to set
* @example
* ```ts

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

@ -71,19 +71,20 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the wave level cap
* @param cap the level cap value to set; 0 uses normal level caps and negative values
* disable it completely
* @param cap the level cap value to set; negative values disable it completely
*
* Set to `0` or `null` to disable.
* @returns `this`
*/
public levelCap(cap: number): this {
public levelCap(cap: number | null): this {
vi.spyOn(Overrides, "LEVEL_CAP_OVERRIDE", "get").mockReturnValue(cap);
let capStr: string;
if (cap > 0) {
capStr = `Level cap set to ${cap}!`;
if (!cap) {
capStr = "Level cap reset to default value for wave.";
} else if (cap < 0) {
capStr = "Level cap disabled!";
} else {
capStr = "Level cap reset to default value for wave.";
capStr = `Level cap set to ${cap}!`;
}
this.log(capStr);
return this;
@ -96,7 +97,7 @@ export class OverridesHelper extends GameManagerHelper {
*/
public startingHeldItems(items: ModifierOverride[]): this {
vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items);
this.log("Player Pokemon starting held items set to:", items);
this.log(`Player Pokemon starting held items set to: ${items}`);
return this;
}
@ -147,7 +148,8 @@ export class OverridesHelper extends GameManagerHelper {
}
/**
* Override the player's starting modifiers
* Override the player's starting modifiers.
* For overriding held items, use {@linkcode startingHeldItems} instead
* @param modifiers the modifiers to set
* @returns `this`
*/
@ -491,6 +493,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 %

View File

@ -73,6 +73,6 @@ export class ReloadHelper extends GameManagerHelper {
}
await this.game.phaseInterceptor.to(CommandPhase);
console.log("==================[New Turn]==================");
console.log("==================[New Turn (Reloaded)]==================");
}
}

View File

@ -326,7 +326,7 @@ export default class PhaseInterceptor {
/**
* Method to transition to a target phase.
* @param phaseTo - The phase to transition to.
* @param runTarget - Whether or not to run the target phase.
* @param runTarget - Whether or not to run the target phase; default `true`.
* @returns A promise that resolves when the transition is complete.
*/
async to(phaseTo: PhaseInterceptorPhase, runTarget = true): Promise<void> {