Fixed modifier code, removed instances of "0 ID = no mon"

This commit is contained in:
Bertie690 2025-05-21 13:54:58 -04:00
parent dd2f475ded
commit 4dfcca1501
10 changed files with 601 additions and 315 deletions

View File

@ -932,7 +932,16 @@ export default class BattleScene extends SceneBase {
return activeOnly ? this.infoToggles.filter(t => t?.isActive()) : this.infoToggles;
}
getPokemonById(pokemonId: number): Pokemon | null {
/**
* Return the {@linkcode Pokemon} associated with a given ID.
* @param pokemonId - The ID whose Pokemon will be retrieved.
* @returns The {@linkcode Pokemon} associated with the given id.
* Returns `null` if the ID is `undefined` or not present in either party.
*/
getPokemonById(pokemonId: number | undefined): Pokemon | null {
if (isNullOrUndefined(pokemonId)) {
return null;
}
const findInParty = (party: Pokemon[]) => party.find(p => p.id === pokemonId);
return (findInParty(this.getPlayerParty()) || findInParty(this.getEnemyParty())) ?? null;
}
@ -3033,14 +3042,14 @@ export default class BattleScene extends SceneBase {
* If the recepient already has the maximum amount allowed for this item, the transfer is cancelled.
* The quantity to transfer is automatically capped at how much the recepient can take before reaching the maximum stack size for the item.
* A transfer that moves a quantity smaller than what is specified in the transferQuantity parameter is still considered successful.
* @param itemModifier {@linkcode PokemonHeldItemModifier} item to transfer (represents the whole stack)
* @param target {@linkcode Pokemon} recepient in this transfer
* @param playSound `true` to play a sound when transferring the item
* @param transferQuantity How many items of the stack to transfer. Optional, defaults to `1`
* @param instant ??? (Optional)
* @param ignoreUpdate ??? (Optional)
* @param itemLost If `true`, treat the item's current holder as losing the item (for now, this simply enables Unburden). Default is `true`.
* @returns `true` if the transfer was successful
* @param itemModifier - {@linkcode PokemonHeldItemModifier} to transfer (represents whole stack)
* @param target - Recipient {@linkcode Pokemon} recieving items
* @param playSound - Whether to play a sound when transferring the item
* @param transferQuantity - How many items of the stack to transfer. Optional, defaults to `1`
* @param instant - ??? (Optional)
* @param ignoreUpdate - ??? (Optional)
* @param itemLost - Whether to treat the item's current holder as losing the item (for now, this simply enables Unburden). Default: `true`.
* @returns Whether the transfer was successful
*/
tryTransferHeldItemModifier(
itemModifier: PokemonHeldItemModifier,
@ -3051,106 +3060,107 @@ export default class BattleScene extends SceneBase {
ignoreUpdate?: boolean,
itemLost = true,
): boolean {
const source = itemModifier.pokemonId ? itemModifier.getPokemon() : null;
const cancelled = new BooleanHolder(false);
const source = itemModifier.getPokemon();
// Check if source even exists and error if not.
// Almost certainly redundant due to checking inside condition, but better log twice than not at all
if (isNullOrUndefined(source)) {
console.error(
`Pokemon ${target.getNameToRender()} tried to transfer %d items from nonexistent source; item: `,
transferQuantity,
itemModifier,
);
return false;
}
// Check for effects that might block us from stealing
const cancelled = new BooleanHolder(false);
if (source && source.isPlayer() !== target.isPlayer()) {
applyAbAttrs(BlockItemTheftAbAttr, source, cancelled);
}
if (cancelled.value) {
return false;
}
const newItemModifier = itemModifier.clone() as PokemonHeldItemModifier;
newItemModifier.pokemonId = target.id;
// check if we have an item already and calc how much to transfer
const matchingModifier = this.findModifier(
m => m instanceof PokemonHeldItemModifier && m.matchType(itemModifier) && m.pokemonId === target.id,
target.isPlayer(),
) as PokemonHeldItemModifier;
) as PokemonHeldItemModifier | undefined;
const countTaken = Math.min(
transferQuantity,
itemModifier.stackCount,
matchingModifier?.getCountUnderMax() ?? Number.MAX_SAFE_INTEGER,
);
if (countTaken <= 0) {
// Can't transfer negative items
return false;
}
itemModifier.stackCount -= countTaken;
// If the old modifier is at 0 stacks, try to remove it
if (itemModifier.stackCount <= 0 && !this.removeModifier(itemModifier, !source.isPlayer())) {
return false;
}
// TODO: what does this do and why is it here
if (source.isPlayer() !== target.isPlayer() && !ignoreUpdate) {
this.updateModifiers(source.isPlayer(), instant);
}
// Add however much we took to the recieving pokemon, creating a new modifier if the target lacked one prio
if (matchingModifier) {
const maxStackCount = matchingModifier.getMaxStackCount();
if (matchingModifier.stackCount >= maxStackCount) {
return false;
}
const countTaken = Math.min(
transferQuantity,
itemModifier.stackCount,
maxStackCount - matchingModifier.stackCount,
);
itemModifier.stackCount -= countTaken;
newItemModifier.stackCount = matchingModifier.stackCount + countTaken;
matchingModifier.stackCount += countTaken;
} else {
const countTaken = Math.min(transferQuantity, itemModifier.stackCount);
itemModifier.stackCount -= countTaken;
const newItemModifier = itemModifier.clone() as PokemonHeldItemModifier;
newItemModifier.pokemonId = target.id;
newItemModifier.stackCount = countTaken;
}
const removeOld = itemModifier.stackCount === 0;
if (!removeOld || !source || this.removeModifier(itemModifier, !source.isPlayer())) {
const addModifier = () => {
if (!matchingModifier || this.removeModifier(matchingModifier, !target.isPlayer())) {
if (target.isPlayer()) {
this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant);
if (source && itemLost) {
applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false);
}
return true;
}
this.addEnemyModifier(newItemModifier, ignoreUpdate, instant);
if (source && itemLost) {
applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false);
}
return true;
}
return false;
};
if (source && source.isPlayer() !== target.isPlayer() && !ignoreUpdate) {
this.updateModifiers(source.isPlayer(), instant);
addModifier();
if (target.isPlayer()) {
this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant);
} else {
addModifier();
this.addEnemyModifier(newItemModifier, ignoreUpdate, instant);
}
return true;
}
return false;
if (itemLost) {
applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false);
}
return true;
}
canTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, transferQuantity = 1): boolean {
const mod = itemModifier.clone() as PokemonHeldItemModifier;
const source = mod.pokemonId ? mod.getPokemon() : null;
const cancelled = new BooleanHolder(false);
if (source && source.isPlayer() !== target.isPlayer()) {
applyAbAttrs(BlockItemTheftAbAttr, source, cancelled);
const source = itemModifier.getPokemon();
if (!source) {
console.error(
`Pokemon ${target.getNameToRender()} tried to transfer %d items from nonexistent source; item: `,
transferQuantity,
itemModifier,
);
return false;
}
// Check theft prevention
// TODO: Verify whether sticky hold procs on friendly fire ally theft
const cancelled = new BooleanHolder(false);
if (source.isPlayer() !== target.isPlayer()) {
applyAbAttrs(BlockItemTheftAbAttr, source, cancelled);
}
if (cancelled.value) {
return false;
}
// figure out if we can take anything
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;
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 removeOld = mod.stackCount === 0;
return !removeOld || !source || this.hasModifier(itemModifier, !source.isPlayer());
) as PokemonHeldItemModifier | undefined;
const countTaken = Math.min(
transferQuantity,
itemModifier.stackCount,
matchingModifier?.getCountUnderMax() ?? Number.MAX_SAFE_INTEGER,
);
return countTaken > 0 && this.hasModifier(itemModifier, !source.isPlayer());
}
removePartyMemberModifiers(partyMemberIndex: number): Promise<void> {
@ -3286,6 +3296,7 @@ export default class BattleScene extends SceneBase {
}
}
// Why do we silently delete missing modifiers?
const modifiersClone = modifiers.slice(0);
for (const modifier of modifiersClone) {
if (!modifier.getStackCount()) {
@ -3311,44 +3322,71 @@ export default class BattleScene extends SceneBase {
});
}
/**
* Check whether a given {@linkcode PersistentModifier} exists on a given side of the field.
* @param modifier - The {@linkcode PersistentModifier} to check the existence of.
* @param enemy - Whether to check the enemy (`true`) or player (`false`) party. Default is `false`.
* @returns Whether the specified modifier exists on the given side of the field.
* @remarks This also compares `pokemonId`s to confirm a match (and therefore owners).
*/
hasModifier(modifier: PersistentModifier, enemy = false): boolean {
const modifiers = !enemy ? this.modifiers : this.enemyModifiers;
return modifiers.indexOf(modifier) > -1;
return (!enemy ? this.modifiers : this.enemyModifiers).includes(modifier);
}
/**
* Removes a currently owned item. If the item is stacked, the entire item stack
* Remove a currently owned item. If the item is stacked, the entire item stack
* gets removed. This function does NOT apply in-battle effects, such as Unburden.
* If in-battle effects are needed, use {@linkcode Pokemon.loseHeldItem} instead.
* @param modifier The item to be removed.
* @param enemy `true` to remove an item owned by the enemy rather than the player; default `false`.
* @returns `true` if the item exists and was successfully removed, `false` otherwise
* @param modifier - The item to be removed.
* @param enemy - Whether to remove from the enemy (`true`) or player (`false`) party; default `false`.
* @returns Whether the item exists and was successfully removed
*/
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 An array containing all {@linkcode PersistentModifier}s 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 An array containing all modifiers matching `modifierType` on the given side of the field.
* @overload
*/
getModifiers<T extends PersistentModifier>(modifierType: Constructor<T>, player?: boolean): T[];
// NOTE: Boolean typing on 1st parameter needed to satisfy "bool only" overload
getModifiers<T extends PersistentModifier>(modifierType?: Constructor<T> | boolean, player?: boolean) {
const usePlayer: boolean = player ?? (typeof modifierType !== "boolean" || modifierType); // non-bool in 1st position = true by default
const mods = usePlayer ? this.modifiers : this.enemyModifiers;
if (typeof modifierType === "undefined" || typeof modifierType === "boolean") {
return mods;
}
return mods.filter((m): m is T => m instanceof modifierType);
}
/**

View File

@ -1246,7 +1246,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
/**
* Determine if the move type change attribute can be applied
*
*
* Can be applied if:
* - The ability's condition is met, e.g. pixilate only boosts normal moves,
* - The move is not forbidden from having its type changed by an ability, e.g. {@linkcode Moves.MULTI_ATTACK}
@ -1262,7 +1262,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
*/
override canApplyPreAttack(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _defender: Pokemon | null, move: Move, _args: [NumberHolder?, NumberHolder?, ...any]): boolean {
return (!this.condition || this.condition(pokemon, _defender, move)) &&
!noAbilityTypeOverrideMoves.has(move.id) &&
!noAbilityTypeOverrideMoves.has(move.id) &&
(!pokemon.isTerastallized ||
(move.id !== Moves.TERA_BLAST &&
(move.id !== Moves.TERA_STARSTORM || pokemon.getTeraType() !== PokemonType.STELLAR || !pokemon.hasSpecies(Species.TERAPAGOS))));
@ -1716,7 +1716,7 @@ export class GorillaTacticsAbAttr extends PostAttackAbAttr {
export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr {
private stealCondition: PokemonAttackCondition | null;
private stolenItem?: PokemonHeldItemModifier;
private stolenItem: PokemonHeldItemModifier;
constructor(stealCondition?: PokemonAttackCondition) {
super();
@ -1731,39 +1731,37 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr {
defender: Pokemon,
move: Move,
hitResult: HitResult,
args: any[]): boolean {
args: any[]
): boolean {
// Check which items to steal
const heldItems = this.getTargetHeldItems(defender).filter((i) => i.isTransferable);
if (
super.canApplyPostAttack(pokemon, passive, simulated, defender, move, hitResult, args) &&
!simulated &&
hitResult < HitResult.NO_EFFECT &&
(!this.stealCondition || this.stealCondition(pokemon, defender, move))
!super.canApplyPostAttack(pokemon, passive, simulated, defender, move, hitResult, args) ||
heldItems.length === 0 || // no items to steal
hitResult >= HitResult.NO_EFFECT || // move was ineffective/protected against
(this.stealCondition && !this.stealCondition(pokemon, defender, move)) // no condition = pass
) {
const heldItems = this.getTargetHeldItems(defender).filter((i) => i.isTransferable);
if (heldItems.length) {
// Ensure that the stolen item in testing is the same as when the effect is applied
this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)];
if (globalScene.canTransferHeldItemModifier(this.stolenItem, pokemon)) {
return true;
}
}
return false;
}
this.stolenItem = undefined;
return false;
// pick random item and check if we can steal it
this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)];
return globalScene.canTransferHeldItemModifier(this.stolenItem, pokemon)
}
override applyPostAttack(
pokemon: Pokemon,
passive: boolean,
_passive: boolean,
simulated: boolean,
defender: Pokemon,
move: Move,
hitResult: HitResult,
args: any[],
_move: Move,
_hitResult: HitResult,
_args: any[],
): void {
const heldItems = this.getTargetHeldItems(defender).filter((i) => i.isTransferable);
if (!this.stolenItem) {
this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)];
if (simulated) {
return;
}
if (globalScene.tryTransferHeldItemModifier(this.stolenItem, pokemon, false)) {
globalScene.queueMessage(
i18next.t("abilityTriggers:postAttackStealHeldItem", {
@ -1773,7 +1771,6 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr {
}),
);
}
this.stolenItem = undefined;
}
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
@ -6629,7 +6626,8 @@ export function initAbilities() {
new Ability(Abilities.STICKY_HOLD, 3)
.attr(BlockItemTheftAbAttr)
.bypassFaint()
.ignorable(),
.ignorable()
.edgeCase(), // may or may not proc incorrectly on user's allies
new Ability(Abilities.SHED_SKIN, 3)
.conditionalAttr(pokemon => !randSeedInt(3), PostTurnResetStatusAbAttr),
new Ability(Abilities.GUTS, 3)

View File

@ -87,10 +87,11 @@ export abstract class ArenaTag {
/**
* Helper function that retrieves the source Pokemon
* @returns The source {@linkcode Pokemon} or `null` if none is found
* @returns - The source {@linkcode Pokemon} for this tag.
* Returns `null` if `this.sourceId` is `undefined`
*/
public getSourcePokemon(): Pokemon | null {
return this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
return globalScene.getPokemonById(this.sourceId);
}
/**
@ -122,19 +123,22 @@ export class MistTag extends ArenaTag {
onAdd(arena: Arena, quiet = false): void {
super.onAdd(arena);
if (this.sourceId) {
const source = globalScene.getPokemonById(this.sourceId);
if (!quiet && source) {
globalScene.queueMessage(
i18next.t("arenaTag:mistOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
} else if (!quiet) {
console.warn("Failed to get source for MistTag onAdd");
}
// We assume `quiet=true` means "just add the bloody tag no questions asked"
if (quiet) {
return;
}
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for MistTag on add message; id: ${this.sourceId}`);
return;
}
globalScene.queueMessage(
i18next.t("arenaTag:mistOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
}
/**
@ -455,18 +459,18 @@ class MatBlockTag extends ConditionalProtectTag {
}
onAdd(_arena: Arena) {
if (this.sourceId) {
const source = globalScene.getPokemonById(this.sourceId);
if (source) {
globalScene.queueMessage(
i18next.t("arenaTag:matBlockOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
} else {
console.warn("Failed to get source for MatBlockTag onAdd");
}
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for Mat Block message; id: ${this.sourceId}`);
return;
}
super.onAdd(_arena);
globalScene.queueMessage(
i18next.t("arenaTag:matBlockOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
}
}
@ -526,7 +530,12 @@ export class NoCritTag extends ArenaTag {
/** Queues a message upon removing this effect from the field */
onRemove(_arena: Arena): void {
const source = globalScene.getPokemonById(this.sourceId!); // TODO: is this bang correct?
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for NoCritTag on remove message; id: ${this.sourceId}`);
return;
}
globalScene.queueMessage(
i18next.t("arenaTag:noCritOnRemove", {
pokemonNameWithAffix: getPokemonNameWithAffix(source ?? undefined),
@ -537,7 +546,7 @@ export class NoCritTag extends ArenaTag {
}
/**
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Wish_(move) Wish}.
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Wish_(move) | Wish}.
* Heals the Pokémon in the user's position the turn after Wish is used.
*/
class WishTag extends ArenaTag {
@ -550,18 +559,20 @@ class WishTag extends ArenaTag {
}
onAdd(_arena: Arena): void {
if (this.sourceId) {
const user = globalScene.getPokemonById(this.sourceId);
if (user) {
this.battlerIndex = user.getBattlerIndex();
this.triggerMessage = i18next.t("arenaTag:wishTagOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(user),
});
this.healHp = toDmgValue(user.getMaxHp() / 2);
} else {
console.warn("Failed to get source for WishTag onAdd");
}
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for WishTag on add message; id: ${this.sourceId}`);
return;
}
super.onAdd(_arena);
this.healHp = toDmgValue(source.getMaxHp() / 2);
globalScene.queueMessage(
i18next.t("arenaTag:wishTagOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
}
onRemove(_arena: Arena): void {
@ -756,15 +767,23 @@ class SpikesTag extends ArenaTrapTag {
onAdd(arena: Arena, quiet = false): void {
super.onAdd(arena);
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
if (!quiet && source) {
globalScene.queueMessage(
i18next.t("arenaTag:spikesOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
// We assume `quiet=true` means "just add the bloody tag no questions asked"
if (quiet) {
return;
}
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for SpikesTag on add message; id: ${this.sourceId}`);
return;
}
globalScene.queueMessage(
i18next.t("arenaTag:spikesOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
}
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
@ -809,15 +828,23 @@ class ToxicSpikesTag extends ArenaTrapTag {
onAdd(arena: Arena, quiet = false): void {
super.onAdd(arena);
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
if (!quiet && source) {
globalScene.queueMessage(
i18next.t("arenaTag:toxicSpikesOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
if (quiet) {
// We assume `quiet=true` means "just add the bloody tag no questions asked"
return;
}
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for ToxicSpikesTag on add message; id: ${this.sourceId}`);
return;
}
globalScene.queueMessage(
i18next.t("arenaTag:toxicSpikesOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
}
onRemove(arena: Arena): void {
@ -915,7 +942,11 @@ class StealthRockTag extends ArenaTrapTag {
onAdd(arena: Arena, quiet = false): void {
super.onAdd(arena);
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
if (quiet) {
return;
}
const source = this.getSourcePokemon();
if (!quiet && source) {
globalScene.queueMessage(
i18next.t("arenaTag:stealthRockOnAdd", {
@ -999,15 +1030,24 @@ class StickyWebTag extends ArenaTrapTag {
onAdd(arena: Arena, quiet = false): void {
super.onAdd(arena);
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
if (!quiet && source) {
globalScene.queueMessage(
i18next.t("arenaTag:stickyWebOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
// We assume `quiet=true` means "just add the bloody tag no questions asked"
if (quiet) {
return;
}
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for SpikesTag on add message; id: ${this.sourceId}`);
return;
}
globalScene.queueMessage(
i18next.t("arenaTag:spikesOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
}
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
@ -1072,14 +1112,20 @@ export class TrickRoomTag extends ArenaTag {
}
onAdd(_arena: Arena): void {
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
if (source) {
globalScene.queueMessage(
i18next.t("arenaTag:trickRoomOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
super.onAdd(_arena);
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for TrickRoomTag on add message; id: ${this.sourceId}`);
return;
}
globalScene.queueMessage(
i18next.t("arenaTag:spikesOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
}
onRemove(_arena: Arena): void {
@ -1126,6 +1172,13 @@ class TailwindTag extends ArenaTag {
}
onAdd(_arena: Arena, quiet = false): void {
const source = this.getSourcePokemon();
if (!source) {
return;
}
super.onAdd(_arena, quiet);
if (!quiet) {
globalScene.queueMessage(
i18next.t(
@ -1134,10 +1187,9 @@ class TailwindTag extends ArenaTag {
);
}
const source = globalScene.getPokemonById(this.sourceId!); //TODO: this bang is questionable!
const party = (source?.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField()) ?? [];
const field = source.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
for (const pokemon of party) {
for (const pokemon of field) {
// Apply the CHARGED tag to party members with the WIND_POWER ability
if (pokemon.hasAbility(Abilities.WIND_POWER) && !pokemon.getTag(BattlerTagType.CHARGED)) {
pokemon.addTag(BattlerTagType.CHARGED);
@ -1225,24 +1277,25 @@ class ImprisonTag extends ArenaTrapTag {
}
/**
* This function applies the effects of Imprison to the opposing Pokemon already present on the field.
* @param arena
* Apply the effects of Imprison to all opposing on-field Pokemon.
*/
override onAdd() {
const source = this.getSourcePokemon();
if (source) {
const party = this.getAffectedPokemon();
party?.forEach((p: Pokemon) => {
if (p.isAllowedInBattle()) {
p.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId);
}
});
globalScene.queueMessage(
i18next.t("battlerTags:imprisonOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
if (!source) {
return;
}
const party = this.getAffectedPokemon();
party.forEach((p: Pokemon) => {
if (p.isAllowedInBattle()) {
p.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId);
}
});
globalScene.queueMessage(
i18next.t("battlerTags:imprisonOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
}
/**

View File

@ -129,7 +129,7 @@ export class BattlerTag {
* @returns The source {@linkcode Pokemon}, or `null` if none is found
*/
public getSourcePokemon(): Pokemon | null {
return this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
return globalScene.getPokemonById(this.sourceId);
}
}
@ -585,9 +585,13 @@ export class TrappedTag extends BattlerTag {
}
canAdd(pokemon: Pokemon): boolean {
const source = globalScene.getPokemonById(this.sourceId!)!;
const move = allMoves[this.sourceMove];
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for TrappedTag canAdd; id: ${this.sourceId}`);
return false;
}
const move = allMoves[this.sourceMove];
const isGhost = pokemon.isOfType(PokemonType.GHOST);
const isTrapped = pokemon.getTag(TrappedTag);
const hasSubstitute = move.hitsSubstitute(source, pokemon);
@ -809,12 +813,20 @@ export class DestinyBondTag extends BattlerTag {
if (lapseType !== BattlerTagLapseType.CUSTOM) {
return super.lapse(pokemon, lapseType);
}
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
if (!source?.isFainted()) {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for DestinyBondTag lapse; id: ${this.sourceId}`);
return false;
}
// Destiny bond stays active until the user faints
if (!source.isFainted()) {
return true;
}
if (source?.getAlly() === pokemon) {
// Don't kill allies
if (source.getAlly() === pokemon) {
return false;
}
@ -827,6 +839,7 @@ export class DestinyBondTag extends BattlerTag {
return false;
}
// Drag the foe down with the user
globalScene.queueMessage(
i18next.t("battlerTags:destinyBondLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
@ -844,17 +857,13 @@ export class InfatuatedTag extends BattlerTag {
}
canAdd(pokemon: Pokemon): boolean {
if (this.sourceId) {
const pkm = globalScene.getPokemonById(this.sourceId);
if (pkm) {
return pokemon.isOppositeGender(pkm);
}
console.warn("canAdd: this.sourceId is not a valid pokemon id!", this.sourceId);
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for InfatuatedTag canAdd; id: ${this.sourceId}`);
return false;
}
console.warn("canAdd: this.sourceId is undefined");
return false;
return pokemon.isOppositeGender(source);
}
onAdd(pokemon: Pokemon): void {
@ -863,7 +872,7 @@ export class InfatuatedTag extends BattlerTag {
globalScene.queueMessage(
i18next.t("battlerTags:infatuatedOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
sourcePokemonName: getPokemonNameWithAffix(this.getSourcePokemon()!), // Tag not added + console warns if no source
}),
);
}
@ -881,26 +890,35 @@ export class InfatuatedTag extends BattlerTag {
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
if (ret) {
globalScene.queueMessage(
i18next.t("battlerTags:infatuatedLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
}),
);
globalScene.unshiftPhase(new CommonAnimPhase(pokemon.getBattlerIndex(), undefined, CommonAnim.ATTRACT));
if (pokemon.randBattleSeedInt(2)) {
globalScene.queueMessage(
i18next.t("battlerTags:infatuatedLapseImmobilize", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
(globalScene.getCurrentPhase() as MovePhase).cancel();
}
if (!ret) {
return false;
}
return ret;
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for InfatuatedTag lapse; id: ${this.sourceId}`);
return false;
}
globalScene.queueMessage(
i18next.t("battlerTags:infatuatedLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonName: getPokemonNameWithAffix(source),
}),
);
globalScene.unshiftPhase(new CommonAnimPhase(pokemon.getBattlerIndex(), undefined, CommonAnim.ATTRACT));
// 50% chance to disrupt the target's action
if (pokemon.randBattleSeedInt(2) === 0) {
globalScene.queueMessage(
i18next.t("battlerTags:infatuatedLapseImmobilize", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
(globalScene.getCurrentPhase() as MovePhase).cancel();
}
return true;
}
onRemove(pokemon: Pokemon): void {
@ -943,6 +961,12 @@ export class SeedTag extends BattlerTag {
}
onAdd(pokemon: Pokemon): void {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for SeedTag onAdd; id: ${this.sourceId}`);
return;
}
super.onAdd(pokemon);
globalScene.queueMessage(
@ -950,45 +974,52 @@ export class SeedTag extends BattlerTag {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
this.sourceIndex = globalScene.getPokemonById(this.sourceId!)!.getBattlerIndex(); // TODO: are those bangs correct?
this.sourceIndex = source.getBattlerIndex();
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
if (ret) {
const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex);
if (source) {
const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) {
globalScene.unshiftPhase(
new CommonAnimPhase(source.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.LEECH_SEED),
);
const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT });
const reverseDrain = pokemon.hasAbilityWithAttr(ReverseDrainAbAttr, false);
globalScene.unshiftPhase(
new PokemonHealPhase(
source.getBattlerIndex(),
!reverseDrain ? damage : damage * -1,
!reverseDrain
? i18next.t("battlerTags:seededLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
})
: i18next.t("battlerTags:seededLapseShed", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
false,
true,
),
);
}
}
if (!ret) {
return false;
}
return ret;
// Check which opponent to restore HP to
const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex);
if (!source) {
console.warn(`Failed to get source Pokemon for SeedTag lapse; id: ${this.sourceId}`);
return false;
}
const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (cancelled.value) {
return true;
}
globalScene.unshiftPhase(
new CommonAnimPhase(source.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.LEECH_SEED),
);
const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT });
const reverseDrain = pokemon.hasAbilityWithAttr(ReverseDrainAbAttr, false);
globalScene.unshiftPhase(
new PokemonHealPhase(
source.getBattlerIndex(),
!reverseDrain ? damage : damage * -1,
!reverseDrain
? i18next.t("battlerTags:seededLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
})
: i18next.t("battlerTags:seededLapseShed", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
false,
true,
),
);
return true;
}
getDescriptor(): string {
@ -1245,9 +1276,15 @@ export class HelpingHandTag extends BattlerTag {
}
onAdd(pokemon: Pokemon): void {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for HelpingHandTag onAdd; id: ${this.sourceId}`);
return;
}
globalScene.queueMessage(
i18next.t("battlerTags:helpingHandOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
pokemonNameWithAffix: getPokemonNameWithAffix(source),
pokemonName: getPokemonNameWithAffix(pokemon),
}),
);
@ -1459,15 +1496,22 @@ export abstract class DamagingTrapTag extends TrappedTag {
}
}
// TODO: Condense all these tags into 1 singular tag with a modified message func
export class BindTag extends DamagingTrapTag {
constructor(turnCount: number, sourceId: number) {
super(BattlerTagType.BIND, CommonAnim.BIND, turnCount, Moves.BIND, sourceId);
}
getTrapMessage(pokemon: Pokemon): string {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for BindTag getTrapMessage; id: ${this.sourceId}`);
return "ERROR - CHECK CONSOLE AND REPORT";
}
return i18next.t("battlerTags:bindOnTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
sourcePokemonName: getPokemonNameWithAffix(source),
moveName: this.getMoveName(),
});
}
@ -1479,9 +1523,16 @@ export class WrapTag extends DamagingTrapTag {
}
getTrapMessage(pokemon: Pokemon): string {
return i18next.t("battlerTags:wrapOnTrap", {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for ClampTag getTrapMessage; id: ${this.sourceId}`);
return "ERROR - CHECK CONSOLE AND REPORT";
}
return i18next.t("battlerTags:clampOnTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
sourcePokemonName: getPokemonNameWithAffix(source),
moveName: this.getMoveName(),
});
}
}
@ -1516,8 +1567,14 @@ export class ClampTag extends DamagingTrapTag {
}
getTrapMessage(pokemon: Pokemon): string {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for ClampTag getTrapMessage; id: ${this.sourceId}`);
return "ERROR - CHECK CONSOLE AND REPORT ASAP";
}
return i18next.t("battlerTags:clampOnTrap", {
sourcePokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
sourcePokemonNameWithAffix: getPokemonNameWithAffix(source),
pokemonName: getPokemonNameWithAffix(pokemon),
});
}
@ -1566,9 +1623,15 @@ export class ThunderCageTag extends DamagingTrapTag {
}
getTrapMessage(pokemon: Pokemon): string {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for ThunderCageTag getTrapMessage; id: ${this.sourceId}`);
return "ERROR - PLEASE REPORT ASAP";
}
return i18next.t("battlerTags:thunderCageOnTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
sourcePokemonNameWithAffix: getPokemonNameWithAffix(source),
});
}
}
@ -1579,9 +1642,15 @@ export class InfestationTag extends DamagingTrapTag {
}
getTrapMessage(pokemon: Pokemon): string {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for InfestationTag getTrapMessage; id: ${this.sourceId}`);
return "ERROR - CHECK CONSOLE AND REPORT";
}
return i18next.t("battlerTags:infestationOnTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
sourcePokemonNameWithAffix: getPokemonNameWithAffix(source),
});
}
}
@ -2253,14 +2322,19 @@ export class SaltCuredTag extends BattlerTag {
}
onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon);
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for SaltCureTag onAdd; id: ${this.sourceId}`);
return;
}
super.onAdd(pokemon);
globalScene.queueMessage(
i18next.t("battlerTags:saltCuredOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
this.sourceIndex = globalScene.getPokemonById(this.sourceId!)!.getBattlerIndex(); // TODO: are those bangs correct?
this.sourceIndex = source.getBattlerIndex();
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
@ -2310,8 +2384,14 @@ export class CursedTag extends BattlerTag {
}
onAdd(pokemon: Pokemon): void {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for CursedTag onAdd; id: ${this.sourceId}`);
return;
}
super.onAdd(pokemon);
this.sourceIndex = globalScene.getPokemonById(this.sourceId!)!.getBattlerIndex(); // TODO: are those bangs correct?
this.sourceIndex = source.getBattlerIndex();
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
@ -2922,7 +3002,13 @@ export class SubstituteTag extends BattlerTag {
/** Sets the Substitute's HP and queues an on-add battle animation that initializes the Substitute's sprite. */
onAdd(pokemon: Pokemon): void {
this.hp = Math.floor(globalScene.getPokemonById(this.sourceId!)!.getMaxHp() / 4);
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for SubstituteTag onAdd; id: ${this.sourceId}`);
return;
}
this.hp = Math.floor(source.getMaxHp() / 4);
this.sourceInFocus = false;
// Queue battle animation and message
@ -3205,13 +3291,14 @@ export class ImprisonTag extends MoveRestrictionBattlerTag {
*/
public override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
const source = this.getSourcePokemon();
if (source) {
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
return super.lapse(pokemon, lapseType) && source.isActive(true);
}
return source.isActive(true);
if (!source) {
console.warn(`Failed to get source Pokemon for ImprisonTag lapse; id: ${this.sourceId}`);
return false;
}
return false;
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
return super.lapse(pokemon, lapseType) && source.isActive(true);
}
return source.isActive(true);
}
/**
@ -3271,12 +3358,20 @@ export class SyrupBombTag extends BattlerTag {
* Applies the single-stage speed down to the target Pokemon and decrements the tag's turn count
* @param pokemon - The target {@linkcode Pokemon}
* @param _lapseType - N/A
* @returns `true` if the `turnCount` is still greater than `0`; `false` if the `turnCount` is `0` or the target or source Pokemon has been removed from the field
* @returns Whether the tag should persist (`turnsRemaining > 0` and source still on field)
*/
override lapse(pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
if (this.sourceId && !globalScene.getPokemonById(this.sourceId)?.isActive(true)) {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for SyrupBombTag lapse; id: ${this.sourceId}`);
return false;
}
// Syrup bomb clears immediately if source leaves field/faints
if (!source.isActive(true)) {
return false;
}
// Custom message in lieu of an animation in mainline
globalScene.queueMessage(
i18next.t("battlerTags:syrupBombLapse", {
@ -3286,7 +3381,7 @@ export class SyrupBombTag extends BattlerTag {
globalScene.unshiftPhase(
new StatStageChangePhase(pokemon.getBattlerIndex(), true, [Stat.SPD], -1, true, false, true),
);
return --this.turnCount > 0;
return super.lapse(pokemon, _lapseType);
}
}

View File

@ -6822,17 +6822,17 @@ export class RandomMovesetMoveAttr extends CallMoveAttr {
// includeParty will be true for Assist, false for Sleep Talk
let allies: Pokemon[];
if (this.includeParty) {
allies = user.isPlayer() ? globalScene.getPlayerParty().filter(p => p !== user) : globalScene.getEnemyParty().filter(p => p !== user);
allies = (user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p !== user);
} else {
allies = [ user ];
}
const partyMoveset = allies.map(p => p.moveset).flat();
const moves = partyMoveset.filter(m => !this.invalidMoves.has(m!.moveId) && !m!.getMove().name.endsWith(" (N)"));
const partyMoveset = allies.flatMap(p => p.moveset);
const moves = partyMoveset.filter(m => !this.invalidMoves.has(m.moveId) && !m.getMove().name.endsWith(" (N)"));
if (moves.length === 0) {
return false;
}
this.moveId = moves[user.randBattleSeedInt(moves.length)]!.moveId;
this.moveId = moves[user.randBattleSeedInt(moves.length)].moveId;
return true;
};
}

View File

@ -327,7 +327,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder
.withOptionPhase(async () => {
// Show the Oricorio a dance, and recruit it
const encounter = globalScene.currentBattle.mysteryEncounter!;
const oricorio = encounter.misc.oricorioData.toPokemon();
const oricorio = encounter.misc.oricorioData.toPokemon() as EnemyPokemon;
oricorio.passive = true;
// Ensure the Oricorio's moveset gains the Dance move the player used

View File

@ -311,6 +311,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* TODO: Stop treating this like a unique ID and stop treating 0 as no pokemon
*/
public id: number;
public pid: number;
public name: string;
public nickname: string;
public species: PokemonSpecies;
@ -4172,7 +4173,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
getTag<T extends BattlerTag>(tagType: Constructor<T>): T | undefined;
getTag(tagType: BattlerTagType | Constructor<BattlerTag>): BattlerTag | undefined {
return tagType instanceof Function
return typeof tagType === "function"
? this.summonData.tags.find(t => t instanceof tagType)
: this.summonData.tags.find(t => t.tagType === tagType);
}
@ -6702,7 +6703,7 @@ export class EnemyPokemon extends Pokemon {
return ret;
}
/**
* Show or hide the type effectiveness multiplier window
* Passing undefined will hide the window

View File

@ -738,7 +738,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
}
getPokemon(): Pokemon | undefined {
return this.pokemonId ? (globalScene.getPokemonById(this.pokemonId) ?? undefined) : undefined;
return globalScene.getPokemonById(this.pokemonId) ?? undefined;
}
getScoreMultiplier(): number {

View File

@ -23,10 +23,7 @@ describe("Abilities - Unburden", () => {
*/
function getHeldItemCount(pokemon: Pokemon): number {
const stackCounts = pokemon.getHeldItems().map(m => m.getStackCount());
if (stackCounts.length) {
return stackCounts.reduce((a, b) => a + b);
}
return 0;
return stackCounts.reduce((a, b) => a + b, 0);
}
beforeAll(() => {
@ -318,7 +315,7 @@ describe("Abilities - Unburden", () => {
});
it("should activate when a reviver seed is used", async () => {
game.override.startingHeldItems([{ name: "REVIVER_SEED" }]).enemyMoveset([Moves.WING_ATTACK]);
game.override.startingHeldItems([{ name: "REVIVER_SEED" }]).enemyMoveset(Moves.WING_ATTACK);
await game.classicMode.startBattle([Species.TREECKO]);
const playerPokemon = game.scene.getPlayerPokemon()!;

View File

@ -0,0 +1,104 @@
import { allMoves } from "#app/data/moves/move";
import type Pokemon from "#app/field/pokemon";
import { BerryModifier } from "#app/modifier/modifier";
import { Abilities } from "#enums/abilities";
import { BattleType } from "#enums/battle-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Field - Pokemon ID Checks", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset(Moves.SPLASH)
.ability(Abilities.NO_GUARD)
.battleStyle("single")
.disableCrits()
.enemyLevel(100)
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
function onlyUnique<T>(array: T[]): T[] {
return [...new Set<T>(array)];
}
// TODO: We currently generate IDs as a pure random integer; remove once unique UUIDs are added
it.todo("2 Pokemon should not be able to generate with the same ID during 1 encounter", async () => {
game.override.battleType(BattleType.TRAINER); // enemy generates 2 mons
await game.classicMode.startBattle([Species.FEEBAS, Species.ABRA]);
const ids = (game.scene.getPlayerParty() as Pokemon[]).concat(game.scene.getEnemyParty()).map((p: Pokemon) => p.id);
const uniqueIds = onlyUnique(ids);
expect(ids).toHaveLength(uniqueIds.length);
});
it("should not prevent item theft with PID of 0", async () => {
game.override
.moveset([Moves.THIEF, Moves.SPLASH])
.enemyHeldItems([{ name: "BERRY", count: 1, type: BerryType.APICOT }]);
vi.spyOn(allMoves[Moves.THIEF], "chance", "get").mockReturnValue(100);
await game.classicMode.startBattle([Species.TREECKO]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
// Override enemy pokemon PID to be 0
enemy.id = 0;
game.scene.getModifiers(BerryModifier, false).forEach(modifier => {
modifier.pokemonId = enemy.id;
});
expect(enemy.getHeldItems()).toHaveLength(1);
expect(player.getHeldItems()).toHaveLength(0);
// Player uses Thief and steals the opponent's item
game.move.select(Moves.THIEF);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.getHeldItems()).toHaveLength(0);
expect(player.getHeldItems()).toHaveLength(1);
});
it("should not prevent Syrup Bomb triggering if user has PID of 0", async () => {
game.override.moveset(Moves.SYRUP_BOMB);
await game.classicMode.startBattle([Species.TREECKO]);
const player = game.scene.getPlayerPokemon()!;
// Override player pokemon PID to be 0
player.id = 0;
const enemy = game.scene.getEnemyPokemon()!;
expect(enemy.getTag(BattlerTagType.SYRUP_BOMB)).toBeUndefined();
game.move.select(Moves.SYRUP_BOMB);
await game.phaseInterceptor.to("TurnEndPhase");
const syrupTag = enemy.getTag(BattlerTagType.SYRUP_BOMB)!;
expect(syrupTag).toBeDefined();
expect(syrupTag.getSourcePokemon()).toBe(player);
expect(enemy.getStatStage(Stat.SPD)).toBe(-1);
});
});