Harvest and Cud Chew fixes
Did stuff, squashed prior changes, test still failing help
This commit is contained in:
parent
bb37a728a3
commit
01336bae67
|
@ -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
|
||||
|
|
|
@ -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)**.
|
||||
|
||||

|
||||
$$
|
||||
\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}
|
||||
$$
|
||||
|
||||
%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:
|
||||
|
||||

|
||||
|
||||
$$
|
||||
\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:
|
||||
|
||||

|
||||
$$
|
||||
\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:
|
||||
|
||||

|
||||
$$
|
||||
\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:
|
||||
|
||||

|
||||
|
||||
$$
|
||||
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$
|
||||
|
|
|
@ -1483,24 +1483,22 @@ export default class BattleScene extends SceneBase {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
@ -2128,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;
|
||||
|
@ -3076,16 +3077,16 @@ export default class BattleScene extends SceneBase {
|
|||
return false;
|
||||
}
|
||||
|
||||
itemModifier.stackCount -= countTaken;
|
||||
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 and remove it
|
||||
if (itemModifier.stackCount <= 0 && source && !this.removeModifier(itemModifier, !source.isPlayer())) {
|
||||
// Can't remove the prior modifier for whatever reason
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -3108,38 +3109,32 @@ export default class BattleScene extends SceneBase {
|
|||
}
|
||||
|
||||
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> {
|
||||
|
@ -3274,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);
|
||||
|
@ -3315,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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";
|
||||
|
@ -3727,7 +3732,7 @@ function getAnticipationCondition(): AbAttrCondition {
|
|||
*/
|
||||
function getOncePerBattleCondition(ability: Abilities): AbAttrCondition {
|
||||
return (pokemon: Pokemon) => {
|
||||
return !pokemon.waveData.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,27 +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);
|
||||
console.log("apply check ran; worked: ", Math.max(Math.min(this.procChance(pokemon), 1), 0) >= pass && this.itemType === "EATEN_BERRIES");
|
||||
return Math.max(Math.min(this.procChance(pokemon), 1), 0) >= pass && this.itemType === "EATEN_BERRIES";
|
||||
return Math.max(Math.min(this.procChance(pokemon), 1), 0) >= pass;
|
||||
}
|
||||
|
||||
override applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
|
||||
|
@ -4077,60 +4094,119 @@ 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 berriesUnderCap = new Set(
|
||||
// get all berries we just ate that are under cap
|
||||
const cappedBerries = new Set(
|
||||
globalScene.getModifiers(BerryModifier, pokemon.isPlayer()).filter(
|
||||
// get all berry modifiers for this mon that are under cap
|
||||
(bm) => bm.pokemonId == pokemon.id && bm.getMaxStackCount() < bm.getStackCount()
|
||||
(bm) => bm.pokemonId === pokemon.id && bm.getCountUnderMax() < 1
|
||||
).map((bm) => bm.berryType)
|
||||
);
|
||||
|
||||
const berriesEaten = pokemon.battleData.berriesEaten.filter(
|
||||
(bt) => berriesUnderCap.has(bt)
|
||||
(bt) => !cappedBerries.has(bt)
|
||||
);
|
||||
|
||||
if (!berriesEaten.length) {
|
||||
console.log("NOPE!")
|
||||
return false;
|
||||
}
|
||||
console.log("YUP!")
|
||||
|
||||
if (simulated) {
|
||||
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.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() {
|
||||
|
@ -4216,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 {
|
||||
|
@ -4224,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
|
||||
|
@ -4518,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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5176,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))
|
||||
) {
|
||||
|
@ -5427,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.waveData.abilitiesApplied.includes(ability.id)) {
|
||||
pokemon.waveData.abilitiesApplied.push(ability.id);
|
||||
if (!simulated) {
|
||||
pokemon.waveData.abilitiesApplied.add(ability.id);
|
||||
}
|
||||
|
||||
globalScene.clearPhaseQueueSplice();
|
||||
|
@ -6755,8 +6831,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)
|
||||
)
|
||||
|
@ -7200,7 +7275,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)
|
||||
|
@ -7340,7 +7415,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)
|
||||
|
|
|
@ -73,76 +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) => {
|
||||
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.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) => {
|
||||
// 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) => {
|
||||
pokemon.addTag(BattlerTagType.CRIT_BOOST);
|
||||
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
|
||||
};
|
||||
case BerryType.STARF:
|
||||
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
|
||||
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) => {
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,26 +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[];
|
||||
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 ?? [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 a hungry munchlax...
|
||||
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,51 +2711,60 @@ export class EatBerryAttr extends MoveEffectAttr {
|
|||
}
|
||||
|
||||
eatBerry(consumer: Pokemon, berryOwner?: Pokemon) {
|
||||
// Update consumer's battle data to record berries eaten
|
||||
consumer.battleData.berriesEaten.push(this.chosenBerry!.berryType)
|
||||
consumer.battleData.berriesEatenLast.push(this.chosenBerry!.berryType)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -9382,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)
|
||||
|
@ -11128,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)
|
||||
|
|
|
@ -222,7 +222,7 @@ function endTrainerBattleAndShowDialogue(): Promise<void> {
|
|||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeAbilityTrigger);
|
||||
}
|
||||
|
||||
pokemon.resetBattleData();
|
||||
pokemon.resetBattleAndWaveData();
|
||||
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
|
||||
}
|
||||
|
||||
|
|
|
@ -322,7 +322,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
public fusionCustomPokemonData: CustomPokemonData | null;
|
||||
public fusionTeraType: PokemonType;
|
||||
|
||||
public customPokemonData: CustomPokemonData;
|
||||
public customPokemonData: CustomPokemonData = new CustomPokemonData();
|
||||
|
||||
/**
|
||||
* TODO: Figure out if we can remove this thing
|
||||
|
@ -332,13 +332,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
/* Pokemon data types, in vague order of precedence */
|
||||
|
||||
/** Data that resets on switch (stat stages, battler tags, etc.) */
|
||||
public summonData: PokemonSummonData;
|
||||
public summonData: PokemonSummonData = new PokemonSummonData;
|
||||
/** Wave data correponding to moves/ability information revealed */
|
||||
public waveData: PokemonWaveData;
|
||||
public waveData: PokemonWaveData = new PokemonWaveData;
|
||||
/** Data that resets only on battle end (hit count, harvest berries, etc.) */
|
||||
public battleData: PokemonBattleData;
|
||||
public battleData: PokemonBattleData = new PokemonBattleData;
|
||||
/** Per-turn data like hit count & flinch tracking */
|
||||
public turnData: PokemonTurnData;
|
||||
public turnData: PokemonTurnData = new PokemonTurnData;
|
||||
|
||||
/** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */
|
||||
public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void;
|
||||
|
@ -466,6 +466,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
this.customPokemonData = new CustomPokemonData(
|
||||
dataSource.customPokemonData,
|
||||
);
|
||||
this.summonData = dataSource.summonData;
|
||||
this.battleData = dataSource.battleData;
|
||||
this.teraType = dataSource.teraType;
|
||||
this.isTerastallized = dataSource.isTerastallized;
|
||||
this.stellarTypesBoosted = dataSource.stellarTypesBoosted ?? [];
|
||||
|
@ -494,8 +496,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
this.variant = this.shiny ? this.generateShinyVariant() : 0;
|
||||
}
|
||||
|
||||
this.customPokemonData = new CustomPokemonData();
|
||||
|
||||
if (nature !== undefined) {
|
||||
this.setNature(nature);
|
||||
} else {
|
||||
|
@ -545,6 +545,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
if (!dataSource) {
|
||||
this.calculateStats();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2223,8 +2224,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
* Accounts for all the various effects which can affect whether an ability will be present or
|
||||
* in effect, and both passive and non-passive.
|
||||
* @param attrType - {@linkcode AbAttr} The ability attribute to check for.
|
||||
* @param canApply - If `false`, it doesn't check whether the ability is currently active; Default `true`
|
||||
* @param ignoreOverride - If `true`, it ignores ability changing effects; Default `false`
|
||||
* @param canApply - Whether to check if the ability is currently active; Default `true`
|
||||
* @param ignoreOverride - Whether to ignore ability changing effects; Default `false`
|
||||
* @returns An array of all the ability attributes on this ability.
|
||||
*/
|
||||
public getAbilityAttrs<T extends AbAttr = AbAttr>(
|
||||
|
@ -2377,15 +2378,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
* Checks whether a pokemon has the specified ability and it's in effect. Accounts for all the various
|
||||
* effects which can affect whether an ability will be present or in effect, and both passive and
|
||||
* non-passive. This is the primary way to check whether a pokemon has a particular ability.
|
||||
* @param {Abilities} ability The ability to check for
|
||||
* @param {boolean} canApply If false, it doesn't check whether the ability is currently active
|
||||
* @param {boolean} ignoreOverride If true, it ignores ability changing effects
|
||||
* @returns {boolean} Whether the ability is present and active
|
||||
* @param ability The ability to check for
|
||||
* @param canApply - Whether to check if the ability is currently active; default `true`
|
||||
* @param ignoreOverride Whether to ignore ability changing effects; default `false`
|
||||
* @returns `true` if the ability is present and active
|
||||
*/
|
||||
public hasAbility(
|
||||
ability: Abilities,
|
||||
canApply = true,
|
||||
ignoreOverride?: boolean,
|
||||
ignoreOverride = false,
|
||||
): boolean {
|
||||
if (
|
||||
this.getAbility(ignoreOverride).id === ability &&
|
||||
|
@ -2408,15 +2409,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
* Accounts for all the various effects which can affect whether an ability will be present or
|
||||
* in effect, and both passive and non-passive. This is one of the two primary ways to check
|
||||
* whether a pokemon has a particular ability.
|
||||
* @param {AbAttr} attrType The ability attribute to check for
|
||||
* @param {boolean} canApply If false, it doesn't check whether the ability is currently active
|
||||
* @param {boolean} ignoreOverride If true, it ignores ability changing effects
|
||||
* @returns {boolean} Whether an ability with that attribute is present and active
|
||||
* @param attrType The {@link AbAttr | ability attribute} to check for
|
||||
* @param canApply - Whether to check if the ability is currently active; default `true`
|
||||
* @param ignoreOverride Whether to ignore ability changing effects; default `false`
|
||||
* @returns `true` if an ability with the given {@linkcode AbAttr} is present and active
|
||||
*/
|
||||
public hasAbilityWithAttr(
|
||||
attrType: Constructor<AbAttr>,
|
||||
canApply = true,
|
||||
ignoreOverride?: boolean,
|
||||
ignoreOverride = false,
|
||||
): boolean {
|
||||
if (
|
||||
(!canApply || this.canApplyAbility()) &&
|
||||
|
@ -5572,7 +5573,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
return !cancelImmunity.value;
|
||||
});
|
||||
|
||||
if (!typeImmune) {
|
||||
if (typeImmune) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
@ -5757,7 +5758,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
this.summonData = new PokemonSummonData();
|
||||
this.setSwitchOutStatus(false);
|
||||
if (!this.battleData) {
|
||||
this.resetBattleData();
|
||||
this.resetBattleAndWaveData();
|
||||
}
|
||||
if (this.getTag(BattlerTagType.SEEDED)) {
|
||||
this.lapseTag(BattlerTagType.SEEDED);
|
||||
|
@ -5800,20 +5801,21 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
}
|
||||
|
||||
/**
|
||||
Reset a {@linkcode Pokemon}'s {@linkcode PokemonBattleData | battleData},
|
||||
as well as any transient {@linkcode PokemonWaveData | waveData}.
|
||||
Reset a {@linkcode Pokemon}'s per-battle {@linkcode PokemonBattleData | battleData},
|
||||
as well as any transient {@linkcode PokemonWaveData | waveData} for the current wave.
|
||||
Called before a new battle starts.
|
||||
*/
|
||||
resetBattleData(): void {
|
||||
resetBattleAndWaveData(): void {
|
||||
this.battleData = new PokemonBattleData();
|
||||
this.resetWaveData();
|
||||
}
|
||||
|
||||
/**
|
||||
Reset a {@linkcode Pokemon}'s {@linkcode PokemonWaveData | waveData}.
|
||||
Called once per new wave start as well as by {@linkcode resetBattleData}.
|
||||
Called once per new wave start as well as by {@linkcode resetBattleAndWaveData}.
|
||||
*/
|
||||
resetWaveData(): void {
|
||||
console.log("Wave data reset for pokemon %s", this.name)
|
||||
this.waveData = new PokemonWaveData();
|
||||
}
|
||||
|
||||
|
@ -6421,7 +6423,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
heldItem: PokemonHeldItemModifier,
|
||||
forBattle = true,
|
||||
): boolean {
|
||||
if (heldItem.pokemonId === -1 || heldItem.pokemonId === this.id) {
|
||||
if (heldItem.pokemonId !== -1 && heldItem.pokemonId !== this.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
heldItem.stackCount--;
|
||||
if (heldItem.stackCount <= 0) {
|
||||
globalScene.removeModifier(heldItem, !this.isPlayer());
|
||||
|
@ -6429,10 +6434,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
if (forBattle) {
|
||||
applyPostItemLostAbAttrs(PostItemLostAbAttr, this, false);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a berry being eaten for ability and move triggers.
|
||||
* Only tracks things that proc _every_ time a berry is eaten.
|
||||
* @param berryType The type of berry being eaten.
|
||||
* @param updateHarvest Whether to track the berry for harvest; default `true`.
|
||||
*/
|
||||
public recordEatenBerry(berryType: BerryType, updateHarvest: boolean = true) {
|
||||
this.battleData.hasEatenBerry = true;
|
||||
if (updateHarvest) {
|
||||
// Only track for harvest if we actually consumed the berry
|
||||
this.battleData.berriesEaten.push(berryType)
|
||||
}
|
||||
this.turnData.berriesEaten.push(berryType);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7001,6 +7019,8 @@ export class PlayerPokemon extends Pokemon {
|
|||
if (partyMemberIndex > fusedPartyMemberIndex) {
|
||||
partyMemberIndex--;
|
||||
}
|
||||
|
||||
// combine the two mons' held items
|
||||
const fusedPartyMemberHeldModifiers = globalScene.findModifiers(
|
||||
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id,
|
||||
true,
|
||||
|
@ -7824,7 +7844,7 @@ export interface AttackMoveResult {
|
|||
|
||||
/**
|
||||
Persistent in-battle data for a {@linkcode Pokemon}.
|
||||
Resets on switch but not on new battle.
|
||||
Resets on switch or new battle.
|
||||
*/
|
||||
export class PokemonSummonData {
|
||||
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
|
||||
|
@ -7832,7 +7852,6 @@ export class PokemonSummonData {
|
|||
public moveQueue: TurnMove[] = [];
|
||||
public tags: BattlerTag[] = [];
|
||||
public abilitySuppressed = false;
|
||||
public abilitiesApplied: Abilities[] = [];
|
||||
public speciesForm: PokemonSpeciesForm | null;
|
||||
public fusionSpeciesForm: PokemonSpeciesForm;
|
||||
public ability: Abilities = Abilities.NONE;
|
||||
|
@ -7850,6 +7869,9 @@ export class PokemonSummonData {
|
|||
/** Data pertaining to this pokemon's illusion. */
|
||||
public illusion: IllusionData | null = null;
|
||||
|
||||
/** Array containing all berries eaten in the last turn; used by {@linkcode Abilities.CUD_CHEW} */
|
||||
public berriesEatenLast: BerryType[] = [];
|
||||
|
||||
/** The number of turns the pokemon has passed since entering the battle */
|
||||
public turnCount = 1;
|
||||
/** The number of turns the pokemon has passed since the start of the wave */
|
||||
|
@ -7859,7 +7881,7 @@ export class PokemonSummonData {
|
|||
}
|
||||
|
||||
/**
|
||||
Temporary data for a {@linkcode Pokemon}.
|
||||
Persistent data for a {@linkcode Pokemon}.
|
||||
Resets at the start of a new battle (but not on switch).
|
||||
*/
|
||||
export class PokemonBattleData {
|
||||
|
@ -7869,19 +7891,27 @@ export class PokemonBattleData {
|
|||
public hasEatenBerry: boolean = false;
|
||||
/** A list of all berries eaten in this current battle; used by {@linkcode Abilities.HARVEST} */
|
||||
public berriesEaten: BerryType[] = [];
|
||||
/** A list of all berries eaten in the last turn; used by {@linkcode Abilities.CUD_CHEW} */
|
||||
public berriesEatenLast: BerryType[] = [];
|
||||
}
|
||||
|
||||
/** Data related to a {@linkcode Pokemon} that resets per wave. */
|
||||
/**
|
||||
Temporary data for a {@linkcode Pokemon}.
|
||||
Resets on new wave.
|
||||
*/
|
||||
export class PokemonWaveData {
|
||||
/** whether the pokemon has endured due to a {@linkcode BattlerTagType.ENDURE_TOKEN} */
|
||||
public endured = false;
|
||||
public abilitiesApplied: Abilities[] = [];
|
||||
/**
|
||||
A set of all the abilities this {@linkcode Pokemon} has used in this wave.
|
||||
Used to track once per battle conditions, as well as (hopefully) by the updated AI.
|
||||
*/
|
||||
public abilitiesApplied: Set<Abilities> = new Set<Abilities>;
|
||||
public abilityRevealed = false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
Temporary data for a {@linkcode Pokemon}.
|
||||
Resets at the start of a new turn.
|
||||
*/
|
||||
export class PokemonTurnData {
|
||||
public flinched = false;
|
||||
public acted = false;
|
||||
|
@ -7909,6 +7939,12 @@ export class PokemonTurnData {
|
|||
* forced to act again in the same turn
|
||||
*/
|
||||
public extraTurns = 0;
|
||||
/**
|
||||
* All berries eaten by this pokemon in this turn.
|
||||
* Saved into {@linkcode PokemonBattleData | BattleData} by {@linkcode Pe at turn end.
|
||||
* @see {@linkcode PokemonsummonData.berriesEatenLast}
|
||||
*/
|
||||
public berriesEaten: BerryType[] = []
|
||||
}
|
||||
|
||||
export enum AiType {
|
||||
|
@ -8036,9 +8072,9 @@ export class PokemonMove {
|
|||
|
||||
/**
|
||||
* Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp}
|
||||
* @param {number} count Amount of PP to use
|
||||
* @param count Amount of PP to use
|
||||
*/
|
||||
usePp(count = 1) {
|
||||
usePp(count: number = 1) {
|
||||
this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp());
|
||||
}
|
||||
|
||||
|
|
|
@ -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,8 +748,8 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
|
|||
return this.getMaxHeldItemCount(pokemon);
|
||||
}
|
||||
|
||||
getCountUnderMax(forThreshold?: boolean): number {
|
||||
return this.getMaxStackCount(forThreshold) - this.getStackCount();
|
||||
getCountUnderMax(): number {
|
||||
return this.getMaxHeldItemCount() - this.getStackCount();
|
||||
}
|
||||
|
||||
abstract getMaxHeldItemCount(pokemon?: Pokemon): number;
|
||||
|
@ -1870,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;
|
||||
}
|
||||
|
|
|
@ -41,7 +41,13 @@ import { WeatherType } from "#enums/weather-type";
|
|||
* }
|
||||
* ```
|
||||
*/
|
||||
const overrides = {} satisfies Partial<InstanceType<typeof DefaultOverrides>>;
|
||||
const overrides = {
|
||||
STARTING_HELD_ITEMS_OVERRIDE: [{ name: "BERRY", type: BerryType.LUM, count: 2 }],
|
||||
WEATHER_OVERRIDE: WeatherType.SUNNY,
|
||||
ABILITY_OVERRIDE: Abilities.HARVEST,
|
||||
OPP_MOVESET_OVERRIDE: Moves.NUZZLE
|
||||
|
||||
} satisfies Partial<InstanceType<typeof DefaultOverrides>>;
|
||||
|
||||
/**
|
||||
* If you need to add Overrides values for local testing do that inside {@linkcode overrides}
|
||||
|
@ -75,9 +81,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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -329,7 +329,7 @@ export class EncounterPhase extends BattlePhase {
|
|||
|
||||
for (const pokemon of globalScene.getPlayerParty()) {
|
||||
if (pokemon) {
|
||||
pokemon.resetBattleData();
|
||||
pokemon.resetBattleAndWaveData();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase {
|
|||
// reset all battle data, perform form changes, etc.
|
||||
for (const pokemon of globalScene.getPlayerParty()) {
|
||||
if (pokemon) {
|
||||
pokemon.resetBattleData();
|
||||
pokemon.resetBattleAndWaveData();
|
||||
if (pokemon.isOnField()) {
|
||||
applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export class NextEncounterPhase extends EncounterPhase {
|
|||
doEncounter(): void {
|
||||
globalScene.playBgm(undefined, true);
|
||||
|
||||
// Reset all player transient wave data/intel
|
||||
// Reset all player transient wave data/intel.
|
||||
for (const pokemon of globalScene.getPlayerParty()) {
|
||||
if (pokemon) {
|
||||
pokemon.resetWaveData();
|
||||
|
|
|
@ -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.waveData.abilityRevealed = true;
|
||||
}
|
||||
pokemon.waveData.abilityRevealed = true;
|
||||
|
||||
this.end();
|
||||
});
|
||||
|
|
|
@ -54,7 +54,6 @@ export class TurnEndPhase extends FieldPhase {
|
|||
}
|
||||
|
||||
globalScene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon);
|
||||
|
||||
globalScene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon);
|
||||
|
||||
pokemon.summonData.turnCount++;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -62,11 +62,11 @@ 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;
|
||||
|
||||
|
@ -77,7 +77,7 @@ export default class PokemonData {
|
|||
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;
|
||||
|
@ -95,7 +95,7 @@ export default class PokemonData {
|
|||
this.gender = source.gender;
|
||||
this.stats = source.stats;
|
||||
this.ivs = source.ivs;
|
||||
this.nature = source.nature !== undefined ? source.nature : (0 as Nature);
|
||||
this.nature = source.nature !== undefined ? source.nature : (0 as Nature); // TODO?: I'm pretty sure this can become nullish coaclescing
|
||||
this.friendship =
|
||||
source.friendship !== undefined ? source.friendship : getPokemonSpecies(this.species).baseFriendship;
|
||||
this.metLevel = source.metLevel || 5;
|
||||
|
@ -127,6 +127,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)
|
||||
|
@ -146,6 +147,7 @@ export default class PokemonData {
|
|||
this.bossSegments = source.bossSegments;
|
||||
}
|
||||
|
||||
// TODO: Refactor to use nullish coaclescing in favor of big conditional
|
||||
if (sourcePokemon) {
|
||||
this.moveset = sourcePokemon.moveset;
|
||||
if (!forHistory) {
|
||||
|
@ -174,7 +176,6 @@ export default class PokemonData {
|
|||
this.summonData.statStages = source.summonData.statStages;
|
||||
this.summonData.moveQueue = source.summonData.moveQueue;
|
||||
this.summonData.abilitySuppressed = source.summonData.abilitySuppressed;
|
||||
this.summonData.abilitiesApplied = source.summonData.abilitiesApplied;
|
||||
|
||||
this.summonData.ability = source.summonData.ability;
|
||||
this.summonData.moveset = source.summonData.moveset?.map(m => PokemonMove.loadMove(m));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
import { RepeatBerryNextTurnAbAttr } from "#app/data/ability";
|
||||
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.SPLASH)
|
||||
.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("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 count non-eating removal", async () => {
|
||||
game.override.enemyMoveset(Moves.INCINERATE);
|
||||
await game.classicMode.startBattle([Species.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
const initHp = farigiraf.getMaxHp() / 4; // needed to allow sitrus procs without dying
|
||||
farigiraf.hp = initHp;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// only 1 berry eaten due to 2nd one being cooked
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -49,7 +49,7 @@ describe("Abilities - Good As Gold", () => {
|
|||
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(player.waveData.abilitiesApplied[0]).toBe(Abilities.GOOD_AS_GOLD);
|
||||
expect(player.waveData.abilitiesApplied).toContain(Abilities.GOOD_AS_GOLD);
|
||||
expect(player.getStatStage(Stat.ATK)).toBe(0);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { BattlerIndex } from "#app/battle";
|
||||
import { BerryModifier } from "#app/modifier/modifier";
|
||||
import type { ModifierOverride } from "#app/modifier/modifier-type";
|
||||
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";
|
||||
|
@ -14,15 +14,16 @@ describe("Abilities - Harvest", () => {
|
|||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
function expectBerries(battlerIndex: BattlerIndex, berries: ModifierOverride[]) {
|
||||
const actualBerries: ModifierOverride[] = game.scene
|
||||
.getModifiers(BerryModifier, battlerIndex < BattlerIndex.ENEMY)
|
||||
.filter(b => b.getPokemon()?.getBattlerIndex() === battlerIndex)
|
||||
.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).toBe(berries);
|
||||
const getPlayerBerries = () =>
|
||||
game.scene.getModifiers(BerryModifier, true).filter(b => b.pokemonId === game.scene.getPlayerPokemon()?.id);
|
||||
|
||||
/** Check whether the player's Modifiers contains AT LEAST 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(expect.arrayContaining(berries));
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
|
@ -33,92 +34,251 @@ describe("Abilities - Harvest", () => {
|
|||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([Moves.SPLASH, Moves.NATURAL_GIFT, Moves.DRAGON_RAGE, Moves.GASTRO_ACID])
|
||||
.moveset([Moves.SPLASH, Moves.NATURAL_GIFT, Moves.FALSE_SWIPE, Moves.GASTRO_ACID])
|
||||
.ability(Abilities.HARVEST)
|
||||
.enemyLevel(1)
|
||||
.startingLevel(100)
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.statusActivation(false) // Since we're using nuzzle to proc both enigma and sitrus berries
|
||||
.weather(WeatherType.SUNNY)
|
||||
.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("should replenish eaten berries", async () => {
|
||||
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("TurnEndPhase", true);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(getPlayerBerries()).toHaveLength(0);
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toHaveLength(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expectBerries(BattlerIndex.PLAYER, [{ name: "BERRY", type: BerryType.LUM, count: 1 }]);
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toBe([]);
|
||||
expectBerriesContaining({ name: "BERRY", type: BerryType.LUM, count: 1 });
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]);
|
||||
});
|
||||
|
||||
it("tracks berries eaten while disabled", async () => {
|
||||
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
|
||||
.enemyAbility(Abilities.NEUTRALIZING_GAS)
|
||||
.startingLevel(100)
|
||||
.startingHeldItems([
|
||||
{ name: "BERRY", type: BerryType.SITRUS, count: 3 },
|
||||
{ name: "BERRY", type: BerryType.LUM, count: 3 },
|
||||
]);
|
||||
await game.classicMode.startBattle([Species.FEEBAS]);
|
||||
{ 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()!;
|
||||
player.hp = player.getMaxHp() / 4 - 1; // low enough to proc sitruses twice guaranteed
|
||||
const player = game.scene.getPlayerPokemon();
|
||||
|
||||
// Spam splash a couple times while we chug sitruses and lums
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.NUZZLE);
|
||||
await game.toNextTurn();
|
||||
// Chug a few berries without harvest (should get tracked)
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.NUZZLE);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toBe([
|
||||
BerryType.SITRUS,
|
||||
BerryType.LUM,
|
||||
BerryType.SITRUS,
|
||||
BerryType.LUM,
|
||||
]);
|
||||
expect(player?.battleData.berriesEaten).toEqual(expect.arrayContaining([BerryType.ENIGMA, BerryType.LUM]));
|
||||
expect(getPlayerBerries()).toHaveLength(2);
|
||||
|
||||
// Disable neutralizing gas this turn and we _should_ get a berry back!
|
||||
// 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.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", true);
|
||||
await game.forceEnemyMove(Moves.NUZZLE);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
vi.spyOn(Phaser.Math.RND, "realInRange").mockReturnValue(0);
|
||||
|
||||
// we chugged 3 berries in total;
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toHaveLength(3);
|
||||
expect(game.scene.getModifiers(BerryModifier, true).reduce((ret, berry) => ret + berry.stackCount, 0)).toBe(3);
|
||||
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: 3 },
|
||||
{ name: "BERRY", type: BerryType.ENIGMA, count: 3 },
|
||||
{ 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.LUM, BerryType.ENIGMA];
|
||||
player.battleData.berriesEaten = [BerryType.LUM, BerryType.STARF];
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
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 the enigma berry left to grab),
|
||||
// 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", true);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expectBerries(BattlerIndex.PLAYER, initBerries);
|
||||
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 berries eaten by Pluck", async () => {
|
||||
const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.PETAYA, count: 1 }];
|
||||
game.override.startingHeldItems(initBerries).enemyMoveset(Moves.PLUCK);
|
||||
await game.classicMode.startBattle([Species.FEEBAS]);
|
||||
|
||||
// gobble gobble gobble
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
// pluck no trigger harvest so we have no berr
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]);
|
||||
expectBerriesContaining();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -68,7 +68,7 @@ describe("Abilities - Infiltrator", () => {
|
|||
const postScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage;
|
||||
|
||||
expect(postScreenDmg).toBe(preScreenDmg);
|
||||
expect(player.waveData.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.waveData.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.waveData.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.waveData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR);
|
||||
expect(player.waveData.abilitiesApplied).toContain(Abilities.INFILTRATOR);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -105,7 +105,7 @@ describe("Moves - Dive", () => {
|
|||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
|
||||
expect(enemyPokemon.waveData.abilitiesApplied[0]).toBe(Abilities.ROUGH_SKIN);
|
||||
expect(enemyPokemon.waveData.abilitiesApplied).toContain(Abilities.ROUGH_SKIN);
|
||||
});
|
||||
|
||||
it("should cancel attack after Harsh Sunlight is set", async () => {
|
||||
|
|
|
@ -81,7 +81,7 @@ describe("Moves - Order Up", () => {
|
|||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(dondozo.waveData.abilitiesApplied.includes(Abilities.SHEER_FORCE)).toBeTruthy();
|
||||
expect(dondozo.waveData.abilitiesApplied).toContain(Abilities.SHEER_FORCE);
|
||||
expect(dondozo.getStatStage(Stat.ATK)).toBe(3);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -62,7 +62,7 @@ describe("Moves - Rage Fist", () => {
|
|||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
game.move.select(Moves.RAGE_FIST);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
|
@ -93,8 +93,19 @@ describe("Moves - Rage Fist", () => {
|
|||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
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);
|
||||
|
@ -114,6 +125,7 @@ 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 hits recieved during trainer battles", async () => {
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -148,7 +149,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||
|
||||
/**
|
||||
* Override the player's starting modifiers.
|
||||
* ** SHOULD NOT BE USED ON ITEMS ** - use {@linkcode startingHeldItems} instead
|
||||
* For overriding held items, use {@linkcode startingHeldItems} instead
|
||||
* @param modifiers the modifiers to set
|
||||
* @returns `this`
|
||||
*/
|
||||
|
|
|
@ -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> {
|
||||
|
|
Loading…
Reference in New Issue