Applied review comments, cleaned up code a bit

This commit is contained in:
Bertie690 2025-04-29 10:04:38 -04:00
parent 4c3447c851
commit f82d3529ad
16 changed files with 168 additions and 137 deletions

View File

@ -874,8 +874,8 @@ export default class BattleScene extends SceneBase {
/** /**
* Returns an array of Pokemon on both sides of the battle - player first, then enemy. * Returns an array of Pokemon on both sides of the battle - player first, then enemy.
* Does not actually check if the pokemon are on the field or not, and always has length 4 regardless of battle type. * Does not actually check if the pokemon are on the field or not, and always has length 4 regardless of battle type.
* @param activeOnly Whether to consider only active pokemon * @param activeOnly - Whether to consider only active pokemon; default `false`
* @returns array of {@linkcode Pokemon} * @returns An array of {@linkcode Pokemon}, as described above.
*/ */
public getField(activeOnly = false): Pokemon[] { public getField(activeOnly = false): Pokemon[] {
const ret = new Array(4).fill(null); const ret = new Array(4).fill(null);

View File

@ -6,6 +6,10 @@ export abstract class AbAttr {
public showAbility: boolean; public showAbility: boolean;
private extraCondition: AbAttrCondition; private extraCondition: AbAttrCondition;
/**
* @param showAbility - Whether to show this ability as a flyout during battle; default `true`.
* Should be kept in parity with mainline where possible.
*/
constructor(showAbility = true) { constructor(showAbility = true) {
this.showAbility = showAbility; this.showAbility = showAbility;
} }

View File

@ -4045,7 +4045,13 @@ export class PostTurnResetStatusAbAttr extends PostTurnAbAttr {
*/ */
export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr { export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr {
/** /**
* @param procChance - Chance to create an item * Array containing all {@linkcode BerryType | BerryTypes} that are under cap and able to be restored.
* Stored inside the class for a minor performance boost
*/
private berriesUnderCap: BerryType[]
/**
* @param procChance - function providing chance to restore an item
* @see {@linkcode createEatenBerry()} * @see {@linkcode createEatenBerry()}
*/ */
constructor( constructor(
@ -4054,19 +4060,19 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr {
super(); super();
} }
override canApplyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { override canApplyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean {
// check if we have at least 1 recoverable berry (at least 1 berry in berriesEaten is not capped) // Ensure we have at least 1 recoverable berry (at least 1 berry in berriesEaten is not capped)
const cappedBerries = new Set( const cappedBerries = new Set(
globalScene.getModifiers(BerryModifier, pokemon.isPlayer()).filter( globalScene.getModifiers(BerryModifier, pokemon.isPlayer()).filter(
(bm) => bm.pokemonId === pokemon.id && bm.getCountUnderMax() < 1 bm => bm.pokemonId === pokemon.id && bm.getCountUnderMax() < 1
).map(bm => bm.berryType) ).map(bm => bm.berryType)
); );
const hasBerryUnderCap = pokemon.battleData.berriesEaten.some( this.berriesUnderCap = pokemon.battleData.berriesEaten.filter(
(bt) => !cappedBerries.has(bt) bt => !cappedBerries.has(bt)
); );
if (!hasBerryUnderCap) { if (!this.berriesUnderCap.length) {
return false; return false;
} }
@ -4076,41 +4082,22 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr {
} }
override applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void { override applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
this.createEatenBerry(pokemon, simulated); if (!simulated) {
this.createEatenBerry(pokemon);
}
} }
/** /**
* Create a new berry chosen randomly from the berries the pokemon ate this battle * Create a new berry chosen randomly from all berries the pokemon ate this battle
* @param pokemon The pokemon with this ability * @param pokemon - The {@linkcode Pokemon} with this ability
* @param simulated whether the associated ability call is simulated
* @returns `true` if a new berry was created * @returns `true` if a new berry was created
*/ */
createEatenBerry(pokemon: Pokemon, simulated: boolean): boolean { createEatenBerry(pokemon: Pokemon): boolean {
// get all berries we just ate that are under cap // Pick a random available berry to yoink
const cappedBerries = new Set( const randomIdx = randSeedInt(this.berriesUnderCap.length);
globalScene.getModifiers(BerryModifier, pokemon.isPlayer()).filter( const chosenBerryType = this.berriesUnderCap[randomIdx];
(bm) => bm.pokemonId === pokemon.id && bm.getCountUnderMax() < 1
).map((bm) => bm.berryType)
);
const berriesEaten = pokemon.battleData.berriesEaten.filter(
(bt) => !cappedBerries.has(bt)
);
if (!berriesEaten.length) {
return false;
}
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 pokemon.battleData.berriesEaten.splice(randomIdx, 1); // Remove berry from memory
const chosenBerry = new BerryModifierType(chosenBerryType); const chosenBerry = new BerryModifierType(chosenBerryType);
chosenBerry.id = "BERRY" // needed to prevent item deletion; remove after modifier rework
// Add the randomly chosen berry or update the existing one // Add the randomly chosen berry or update the existing one
const berryModifier = globalScene.findModifier( const berryModifier = globalScene.findModifier(
@ -4121,7 +4108,6 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr {
if (berryModifier) { if (berryModifier) {
berryModifier.stackCount++ berryModifier.stackCount++
} else { } else {
// make new modifier
const newBerry = new BerryModifier(chosenBerry, pokemon.id, chosenBerryType, 1); const newBerry = new BerryModifier(chosenBerry, pokemon.id, chosenBerryType, 1);
if (pokemon.isPlayer()) { if (pokemon.isPlayer()) {
globalScene.addModifier(newBerry); globalScene.addModifier(newBerry);
@ -4141,19 +4127,19 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr {
* Used by {@linkcode Abilities.CUD_CHEW}. * Used by {@linkcode Abilities.CUD_CHEW}.
*/ */
export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr { export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr {
// no need for constructor; all it does is set `showAbility` which we override before triggering anyways
/** /**
* @returns `true` if the pokemon ate anything last turn * @returns `true` if the pokemon ate anything last turn
*/ */
override canApply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean { override canApply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean {
this.showAbility = true; // force ability popup if ability triggers // force ability popup for ability triggers on normal turns.
// Still not used if ability doesn't proc
this.showAbility = true;
return !!pokemon.summonData.berriesEatenLast.length; return !!pokemon.summonData.berriesEatenLast.length;
} }
/** /**
* Cause this {@linkcode Pokemon} to regurgitate and eat all berries * Cause this {@linkcode Pokemon} to regurgitate and eat all berries inside its `berriesEatenLast` array.
* inside its `berriesEatenLast` array. * Triggers a berry use animation, but does *not* count for other berry or item-related abilities.
* @param pokemon - The {@linkcode Pokemon} having a bad tummy ache * @param pokemon - The {@linkcode Pokemon} having a bad tummy ache
* @param _passive - N/A * @param _passive - N/A
* @param _simulated - N/A * @param _simulated - N/A
@ -4161,13 +4147,12 @@ export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr {
* @param _args - N/A * @param _args - N/A
*/ */
override apply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: BooleanHolder | null, _args: any[]): void { override apply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: BooleanHolder | null, _args: any[]): void {
// play berry animation
globalScene.unshiftPhase( globalScene.unshiftPhase(
new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM), new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM),
); );
// Re-apply effects of all berries previously scarfed. // Re-apply effects of all berries previously scarfed.
// This technically doesn't count as "eating" a berry (for unnerve/stuff cheeks/unburden) // This doesn't count as "eating" a berry (for unnerve/stuff cheeks/unburden) as no item is consumed.
for (const berryType of pokemon.summonData.berriesEatenLast) { for (const berryType of pokemon.summonData.berriesEatenLast) {
getBerryEffectFunc(berryType)(pokemon); getBerryEffectFunc(berryType)(pokemon);
const bMod = new BerryModifier(new BerryModifierType(berryType), pokemon.id, berryType, 1); const bMod = new BerryModifier(new BerryModifierType(berryType), pokemon.id, berryType, 1);
@ -4179,7 +4164,7 @@ export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr {
} }
/** /**
* @returns always `true` * @returns always `true` as we always want to move berries into summon data
*/ */
override canApplyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean { override canApplyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean {
this.showAbility = false; // don't show popup for turn end berry moving (should ideally be hidden) this.showAbility = false; // don't show popup for turn end berry moving (should ideally be hidden)
@ -4188,11 +4173,12 @@ export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr {
/** /**
* Move this {@linkcode Pokemon}'s `berriesEaten` array from `PokemonTurnData` * Move this {@linkcode Pokemon}'s `berriesEaten` array from `PokemonTurnData`
* into `PokemonSummonData`. * into `PokemonSummonData` on turn end.
* @param pokemon The {@linkcode Pokemon} having a nice snack * Both arrays are cleared on switch.
* @param _passive N/A * @param pokemon - The {@linkcode Pokemon} having a nice snack
* @param _simulated N/A * @param _passive - N/A
* @param _args N/A * @param _simulated - N/A
* @param _args - N/A
*/ */
override applyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): void { override applyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): void {
pokemon.summonData.berriesEatenLast = pokemon.turnData.berriesEaten; pokemon.summonData.berriesEatenLast = pokemon.turnData.berriesEaten;
@ -4565,8 +4551,19 @@ export class DoubleBerryEffectAbAttr extends AbAttr {
} }
} }
/**
* Attribute to prevent opposing berry use while on the field.
* Used by {@linkcode Abilities.UNNERVE}, {@linkcode Abilities.AS_ONE_GLASTRIER} and {@linkcode Abilities.AS_ONE_SPECTRIER}
*/
export class PreventBerryUseAbAttr extends AbAttr { export class PreventBerryUseAbAttr extends AbAttr {
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: BooleanHolder, args: any[]): void { /**
* Prevent use of opposing berries.
* @param _pokemon - Unused
* @param _passive - Unused
* @param _simulated - Unused
* @param cancelled - {@linkcode BooleanHolder} containing whether to block berry use
*/
override apply(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, cancelled: BooleanHolder): void {
cancelled.value = true; cancelled.value = true;
} }
} }
@ -5156,7 +5153,7 @@ export class PostSummonStatStageChangeOnArenaAbAttr extends PostSummonStatStageC
/** /**
* Takes no damage from the first hit of a damaging move. * Takes no damage from the first hit of a damaging move.
* This is used in the Disguise and Ice Face abilities. * This is used in the Disguise and Ice Face abilities.
* *
* Does not apply to a user's substitute * Does not apply to a user's substitute
* @extends ReceivedMoveDamageMultiplierAbAttr * @extends ReceivedMoveDamageMultiplierAbAttr
*/ */

View File

@ -1132,7 +1132,7 @@ export abstract class BattleAnim {
if (priority === 0) { if (priority === 0) {
// Place the sprite in front of the pokemon on the field. // Place the sprite in front of the pokemon on the field.
targetSprite = globalScene.getEnemyField().find(p => p) ?? globalScene.getPlayerField().find(p => p); targetSprite = globalScene.getEnemyField().find(p => p) ?? globalScene.getPlayerField().find(p => p);
console.log(typeof targetSprite); // console.log(typeof targetSprite);
moveFunc = globalScene.field.moveBelow; moveFunc = globalScene.field.moveBelow;
} else if (priority === 2 && this.bgSprite) { } else if (priority === 2 && this.bgSprite) {
moveFunc = globalScene.field.moveAbove; moveFunc = globalScene.field.moveAbove;

View File

@ -2535,10 +2535,10 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
return !target.status && target.canSetStatus(user.status?.effect, true, false, user) ? -10 : 0; return !target.status && target.canSetStatus(user.status?.effect, true, false, user) ? -10 : 0;
} }
} }
/** /**
* The following needs to be implemented for Thief * Attribute to steal items upon this move's use.
* "If the user faints due to the target's Ability (Rough Skin or Iron Barbs) or held Rocky Helmet, it cannot remove the target's held item." * Used for {@linkcode Moves.THIEF} and {@linkcode Moves.COVET}.
* "If Knock Off causes a Pokémon with the Sticky Hold Ability to faint, it can now remove that Pokémon's held item."
*/ */
export class StealHeldItemChanceAttr extends MoveEffectAttr { export class StealHeldItemChanceAttr extends MoveEffectAttr {
private chance: number; private chance: number;
@ -2553,18 +2553,22 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
if (rand > this.chance) { if (rand > this.chance) {
return false; return false;
} }
const heldItems = this.getTargetHeldItems(target).filter((i) => i.isTransferable); const heldItems = this.getTargetHeldItems(target).filter((i) => i.isTransferable);
if (heldItems.length) { if (!heldItems.length) {
const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD; return false;
const highestItemTier = heldItems.map((m) => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct?
const tierHeldItems = heldItems.filter((m) => m.type.getOrInferTier(poolType) === highestItemTier);
const stolenItem = tierHeldItems[user.randSeedInt(tierHeldItems.length)];
if (globalScene.tryTransferHeldItemModifier(stolenItem, user, false)) {
globalScene.queueMessage(i18next.t("moveTriggers:stoleItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name }));
return true;
}
} }
return false;
const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD;
const highestItemTier = heldItems.map((m) => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct?
const tierHeldItems = heldItems.filter((m) => m.type.getOrInferTier(poolType) === highestItemTier);
const stolenItem = tierHeldItems[user.randSeedInt(tierHeldItems.length)];
if (!globalScene.tryTransferHeldItemModifier(stolenItem, user, false)) {
return false;
}
globalScene.queueMessage(i18next.t("moveTriggers:stoleItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name }));
return true;
} }
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
@ -2588,58 +2592,62 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
* Used for Incinerate and Knock Off. * Used for Incinerate and Knock Off.
* Not Implemented Cases: (Same applies for Thief) * Not Implemented Cases: (Same applies for Thief)
* "If the user faints due to the target's Ability (Rough Skin or Iron Barbs) or held Rocky Helmet, it cannot remove the target's held item." * "If the user faints due to the target's Ability (Rough Skin or Iron Barbs) or held Rocky Helmet, it cannot remove the target's held item."
* "If Knock Off causes a Pokémon with the Sticky Hold Ability to faint, it can now remove that Pokémon's held item." * "If the Pokémon is knocked out by the attack, Sticky Hold does not protect the held item.""
*/ */
export class RemoveHeldItemAttr extends MoveEffectAttr { export class RemoveHeldItemAttr extends MoveEffectAttr {
/** Optional restriction for item pool to berries only i.e. Differentiating Incinerate and Knock Off */ /** Optional restriction for item pool to berries only; i.e. Incinerate */
private berriesOnly: boolean; private berriesOnly: boolean;
constructor(berriesOnly: boolean) { constructor(berriesOnly: boolean = false) {
super(false); super(false);
this.berriesOnly = berriesOnly; this.berriesOnly = berriesOnly;
} }
/** /**
* * Attempt to permanently remove a held
* @param user {@linkcode Pokemon} that used the move * @param user - The {@linkcode Pokemon} that used the move
* @param target Target {@linkcode Pokemon} that the moves applies to * @param target - The {@linkcode Pokemon} targeted by the move
* @param move {@linkcode Move} that is used * @param move - N/A
* @param args N/A * @param args N/A
* @returns True if an item was removed * @returns `true` if an item was able to be removed
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!this.berriesOnly && target.isPlayer()) { // "Wild Pokemon cannot knock off Player Pokemon's held items" (See Bulbapedia) if (!this.berriesOnly && target.isPlayer()) { // "Wild Pokemon cannot knock off Player Pokemon's held items" (See Bulbapedia)
return false; return false;
} }
// Check for abilities that block item theft
// TODO: This should not trigger if the target would faint beforehand
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // Check for abilities that block item theft applyAbAttrs(BlockItemTheftAbAttr, target, cancelled);
if (cancelled.value === true) { if (cancelled.value) {
return false; return false;
} }
// Considers entire transferrable item pool by default (Knock Off). Otherwise berries only if specified (Incinerate). // Considers entire transferrable item pool by default (Knock Off).
// Otherwise only consider berries (Incinerate).
let heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable); let heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable);
if (this.berriesOnly) { if (this.berriesOnly) {
heldItems = heldItems.filter(m => m instanceof BerryModifier && m.pokemonId === target.id, target.isPlayer()); heldItems = heldItems.filter(m => m instanceof BerryModifier && m.pokemonId === target.id, target.isPlayer());
} }
if (heldItems.length) { if (!heldItems.length) {
const removedItem = heldItems[user.randSeedInt(heldItems.length)]; return false;
}
// Decrease item amount and update icon const removedItem = heldItems[user.randSeedInt(heldItems.length)];
target.loseHeldItem(removedItem);
globalScene.updateModifiers(target.isPlayer());
// Decrease item amount and update icon
target.loseHeldItem(removedItem);
globalScene.updateModifiers(target.isPlayer());
if (this.berriesOnly) { if (this.berriesOnly) {
globalScene.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); globalScene.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name }));
} else { } else {
globalScene.queueMessage(i18next.t("moveTriggers:knockedOffItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); globalScene.queueMessage(i18next.t("moveTriggers:knockedOffItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name }));
}
} }
return true; return true;
@ -6349,11 +6357,11 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
if (!allyPokemon?.isActive(true) && switchOutTarget.hp) { if (!allyPokemon?.isActive(true) && switchOutTarget.hp) {
globalScene.pushPhase(new BattleEndPhase(false)); globalScene.pushPhase(new BattleEndPhase(false));
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) { if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
globalScene.pushPhase(new SelectBiomePhase()); globalScene.pushPhase(new SelectBiomePhase());
} }
globalScene.pushPhase(new NewBattlePhase()); globalScene.pushPhase(new NewBattlePhase());
} }
} }
@ -8711,7 +8719,10 @@ export function initMoves() {
.attr(MultiHitPowerIncrementAttr, 3) .attr(MultiHitPowerIncrementAttr, 3)
.checkAllHits(), .checkAllHits(),
new AttackMove(Moves.THIEF, PokemonType.DARK, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 2) new AttackMove(Moves.THIEF, PokemonType.DARK, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 2)
.attr(StealHeldItemChanceAttr, 0.3), .attr(StealHeldItemChanceAttr, 0.3)
.edgeCase(),
// Should not be able to steal held item if user faints due to Rough Skin, Iron Barbs, etc.
// Should be able to steal items from pokemon with Sticky Hold if the damage causes them to faint
new StatusMove(Moves.SPIDER_WEB, PokemonType.BUG, -1, 10, -1, 0, 2) new StatusMove(Moves.SPIDER_WEB, PokemonType.BUG, -1, 10, -1, 0, 2)
.condition(failIfGhostTypeCondition) .condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
@ -9098,7 +9109,10 @@ export function initMoves() {
.reflectable(), .reflectable(),
new AttackMove(Moves.KNOCK_OFF, PokemonType.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) new AttackMove(Moves.KNOCK_OFF, PokemonType.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
.attr(RemoveHeldItemAttr, false), .attr(RemoveHeldItemAttr, false)
.edgeCase(),
// Should not be able to remove held item if user faints due to Rough Skin, Iron Barbs, etc.
// Should be able to remove items from pokemon with Sticky Hold if the damage causes them to faint
new AttackMove(Moves.ENDEAVOR, PokemonType.NORMAL, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 3) new AttackMove(Moves.ENDEAVOR, PokemonType.NORMAL, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 3)
.attr(MatchHpAttr) .attr(MatchHpAttr)
.condition(failOnBossCondition), .condition(failOnBossCondition),
@ -9286,7 +9300,10 @@ export function initMoves() {
.attr(HighCritAttr) .attr(HighCritAttr)
.attr(StatusEffectAttr, StatusEffect.POISON), .attr(StatusEffectAttr, StatusEffect.POISON),
new AttackMove(Moves.COVET, PokemonType.NORMAL, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 3) new AttackMove(Moves.COVET, PokemonType.NORMAL, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 3)
.attr(StealHeldItemChanceAttr, 0.3), .attr(StealHeldItemChanceAttr, 0.3)
.edgeCase(),
// Should not be able to steal held item if user faints due to Rough Skin, Iron Barbs, etc.
// Should be able to steal items from pokemon with Sticky Hold if the damage causes them to faint
new AttackMove(Moves.VOLT_TACKLE, PokemonType.ELECTRIC, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 3) new AttackMove(Moves.VOLT_TACKLE, PokemonType.ELECTRIC, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 3)
.attr(RecoilAttr, false, 0.33) .attr(RecoilAttr, false, 0.33)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
@ -9797,7 +9814,9 @@ export function initMoves() {
.hidesTarget(), .hidesTarget(),
new AttackMove(Moves.INCINERATE, PokemonType.FIRE, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5) new AttackMove(Moves.INCINERATE, PokemonType.FIRE, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5)
.target(MoveTarget.ALL_NEAR_ENEMIES) .target(MoveTarget.ALL_NEAR_ENEMIES)
.attr(RemoveHeldItemAttr, true), .attr(RemoveHeldItemAttr, true)
.edgeCase(),
// Should be able to remove items from pokemon with Sticky Hold if the damage causes them to faint
new StatusMove(Moves.QUASH, PokemonType.DARK, 100, 15, -1, 0, 5) new StatusMove(Moves.QUASH, PokemonType.DARK, 100, 15, -1, 0, 5)
.condition(failIfSingleBattle) .condition(failIfSingleBattle)
.condition((user, target, move) => !target.turnData.acted) .condition((user, target, move) => !target.turnData.acted)

View File

@ -222,6 +222,7 @@ function endTrainerBattleAndShowDialogue(): Promise<void> {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeAbilityTrigger); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeAbilityTrigger);
} }
// Each trainer battle is "supposed" to be a new fight, so reset all per-battle activation effects
pokemon.resetBattleAndWaveData(); pokemon.resetBattleAndWaveData();
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
} }

View File

@ -676,9 +676,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Checks if the pokemon is allowed in battle (ie: not fainted, and allowed under any active challenges). * Checks if this {@linkcode Pokemon} is allowed in battle (ie: not fainted, and allowed under any active challenges).
* @param onField `true` to also check if the pokemon is currently on the field, defaults to `false` * @param onField `true` to also check if the pokemon is currently on the field; default `false`
* @returns `true` if the pokemon is "active". Returns `false` if there is no active {@linkcode BattleScene} * @returns `true` if the pokemon is "active", as described above.
* Returns `false` if there is no active {@linkcode BattleScene} or the pokemon is disallowed.
*/ */
public isActive(onField = false): boolean { public isActive(onField = false): boolean {
if (!globalScene) { if (!globalScene) {
@ -5696,9 +5697,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
Reset a {@linkcode Pokemon}'s per-battle {@linkcode PokemonBattleData | battleData}, * Reset a {@linkcode Pokemon}'s per-battle {@linkcode PokemonBattleData | battleData},
as well as any transient {@linkcode PokemonWaveData | waveData} for the current wave. * as well as any transient {@linkcode PokemonWaveData | waveData} for the current wave.
Called before a new battle starts. * Should be called once per arena transition (new biome/trainer battle/Mystery Encounter).
*/ */
resetBattleAndWaveData(): void { resetBattleAndWaveData(): void {
this.battleData = new PokemonBattleData(); this.battleData = new PokemonBattleData();
@ -5707,7 +5708,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Reset a {@linkcode Pokemon}'s {@linkcode PokemonWaveData | waveData}. * Reset a {@linkcode Pokemon}'s {@linkcode PokemonWaveData | waveData}.
* Called once per new wave start as well as by {@linkcode resetBattleAndWaveData}. * Should be called upon starting a new wave in addition to whenever an arena transition occurs.
* @see {@linkcode resetBattleAndWaveData()}
*/ */
resetWaveData(): void { resetWaveData(): void {
this.waveData = new PokemonWaveData(); this.waveData = new PokemonWaveData();
@ -6046,7 +6048,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
let fusionPaletteColors: Map<number, number>; let fusionPaletteColors: Map<number, number>;
const originalRandom = Math.random; const originalRandom = Math.random;
Math.random = randSeedFloat; Math.random = () => randSeedFloat();
globalScene.executeWithSeedOffset( globalScene.executeWithSeedOffset(
() => { () => {
@ -7788,11 +7790,11 @@ export class PokemonSummonData {
* Resets at the start of a new battle (but not on switch). * Resets at the start of a new battle (but not on switch).
*/ */
export class PokemonBattleData { export class PokemonBattleData {
/** counts the hits the pokemon received during this battle; used for {@linkcode Moves.RAGE_FIST} */ /** Counter tracking direct hits this Pokemon has received during this battle; used for {@linkcode Moves.RAGE_FIST} */
public hitCount = 0; public hitCount = 0;
/** Whether this has eaten a berry this battle; used for {@linkcode Moves.BELCH} */ /** Whether this Pokemon has eaten a berry this battle; used for {@linkcode Moves.BELCH} */
public hasEatenBerry: boolean = false; public hasEatenBerry: boolean = false;
/** A list of all berries eaten in this current battle; used by {@linkcode Abilities.HARVEST} */ /** Array containing all berries eaten in this current battle; used by {@linkcode Abilities.HARVEST} */
public berriesEaten: BerryType[] = []; public berriesEaten: BerryType[] = [];
constructor(source?: PokemonBattleData | Partial<PokemonBattleData>) { constructor(source?: PokemonBattleData | Partial<PokemonBattleData>) {

View File

@ -789,6 +789,7 @@ export class BerryModifierType extends PokemonHeldItemModifierType implements Ge
); );
this.berryType = berryType; this.berryType = berryType;
this.id = "BERRY"; // needed to prevent harvest item deletion; remove after modifier rework
} }
get name(): string { get name(): string {

View File

@ -58,9 +58,8 @@ export class BattleEndPhase extends BattlePhase {
globalScene.unshiftPhase(new GameOverPhase(true)); globalScene.unshiftPhase(new GameOverPhase(true));
} }
// reset pokemon wave turn count, apply post battle effects, etc etc.
for (const pokemon of globalScene.getField()) { for (const pokemon of globalScene.getField()) {
if (pokemon?.summonData) { if (pokemon) {
pokemon.summonData.waveTurnCount = 1; pokemon.summonData.waveTurnCount = 1;
} }
} }
@ -82,7 +81,6 @@ export class BattleEndPhase extends BattlePhase {
} }
} }
// lapse all post battle modifiers that should lapse
const lapsingModifiers = globalScene.findModifiers( const lapsingModifiers = globalScene.findModifiers(
m => m instanceof LapsingPersistentModifier || m instanceof LapsingPokemonHeldItemModifier, m => m instanceof LapsingPersistentModifier || m instanceof LapsingPokemonHeldItemModifier,
) as (LapsingPersistentModifier | LapsingPokemonHeldItemModifier)[]; ) as (LapsingPersistentModifier | LapsingPokemonHeldItemModifier)[];

View File

@ -15,7 +15,10 @@ import { CommonAnimPhase } from "./common-anim-phase";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
/** The phase after attacks where the pokemon eat berries */ /**
* The phase after attacks where the pokemon eat berries.
* Also triggers Cud Chew's "repeat berry use" effects
*/
export class BerryPhase extends FieldPhase { export class BerryPhase extends FieldPhase {
start() { start() {
super.start(); super.start();
@ -30,18 +33,17 @@ export class BerryPhase extends FieldPhase {
/** /**
* Attempt to eat all of a given {@linkcode Pokemon}'s berries once. * Attempt to eat all of a given {@linkcode Pokemon}'s berries once.
* @param pokemon The {@linkcode Pokemon} to check * @param pokemon - The {@linkcode Pokemon} to check
*/ */
eatBerries(pokemon: Pokemon): void { eatBerries(pokemon: Pokemon): void {
// check if we even have anything to eat
const hasUsableBerry = !!globalScene.findModifier(m => { const hasUsableBerry = !!globalScene.findModifier(m => {
return m instanceof BerryModifier && m.shouldApply(pokemon); return m instanceof BerryModifier && m.shouldApply(pokemon);
}, pokemon.isPlayer()); }, pokemon.isPlayer());
if (!hasUsableBerry) { if (!hasUsableBerry) {
return; return;
} }
// Check if any opponents have unnerve to block us from eating berries
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
pokemon.getOpponents().map(opp => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled)); pokemon.getOpponents().map(opp => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled));
if (cancelled.value) { if (cancelled.value) {
@ -57,7 +59,6 @@ export class BerryPhase extends FieldPhase {
new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM), 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)) { for (const berryModifier of globalScene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon)) {
if (berryModifier.consumed) { if (berryModifier.consumed) {
berryModifier.consumed = false; berryModifier.consumed = false;
@ -66,8 +67,6 @@ export class BerryPhase extends FieldPhase {
// No need to track berries being eaten; already done inside applyModifiers // No need to track berries being eaten; already done inside applyModifiers
globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier));
} }
// update held modifiers and such
globalScene.updateModifiers(pokemon.isPlayer()); globalScene.updateModifiers(pokemon.isPlayer());
// Abilities.CHEEK_POUCH only works once per round of nom noms // Abilities.CHEEK_POUCH only works once per round of nom noms

View File

@ -7,7 +7,8 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase {
doEncounter(): void { doEncounter(): void {
globalScene.playBgm(undefined, true); globalScene.playBgm(undefined, true);
// reset all battle data, perform form changes, etc. // Reset all battle and wave data, perform form changes, etc.
// We do this because new biomes are considered "arena transitions" akin to MEs and trainer battles
for (const pokemon of globalScene.getPlayerParty()) { for (const pokemon of globalScene.getPlayerParty()) {
if (pokemon) { if (pokemon) {
pokemon.resetBattleAndWaveData(); pokemon.resetBattleAndWaveData();

View File

@ -1,6 +1,10 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { EncounterPhase } from "./encounter-phase"; import { EncounterPhase } from "./encounter-phase";
/**
* The phase between defeating an encounter and starting another wild wave.
* Handles generating, loading and preparing for it.
*/
export class NextEncounterPhase extends EncounterPhase { export class NextEncounterPhase extends EncounterPhase {
start() { start() {
super.start(); super.start();
@ -9,7 +13,9 @@ export class NextEncounterPhase extends EncounterPhase {
doEncounter(): void { doEncounter(): void {
globalScene.playBgm(undefined, true); globalScene.playBgm(undefined, true);
// Reset all player transient wave data/intel. // Reset all player transient wave data/intel before starting a new wild encounter.
// We exclusively reset wave data here as wild waves are considered one continuous "battle"
// for lack of an arena transition.
for (const pokemon of globalScene.getPlayerParty()) { for (const pokemon of globalScene.getPlayerParty()) {
if (pokemon) { if (pokemon) {
pokemon.resetWaveData(); pokemon.resetWaveData();

View File

@ -240,6 +240,7 @@ export class SwitchSummonPhase extends SummonPhase {
} }
} }
// No need (or particular use) resetting turn data here on initial send in
if (this.switchType !== SwitchType.INITIAL_SWITCH) { if (this.switchType !== SwitchType.INITIAL_SWITCH) {
pokemon.resetTurnData(); pokemon.resetTurnData();
pokemon.turnData.switchedInThisTurn = true; pokemon.turnData.switchedInThisTurn = true;

View File

@ -1198,8 +1198,6 @@ export class GameData {
} }
} }
// load modifier data
if (globalScene.modifiers.length) { if (globalScene.modifiers.length) {
console.warn("Existing modifiers not cleared on session load, deleting..."); console.warn("Existing modifiers not cleared on session load, deleting...");
globalScene.modifiers = []; globalScene.modifiers = [];

View File

@ -13,27 +13,23 @@ export function deepCopy(values: object): object {
* This copies all values from `source` that match properties inside `dest`, * This copies all values from `source` that match properties inside `dest`,
* checking recursively for non-null nested objects. * checking recursively for non-null nested objects.
* If a property in `src` does not exist in `dest` or its `typeof` evaluates differently, it is skipped. * If a property in `source` does not exist in `dest` or its `typeof` evaluates differently, it is skipped.
* If it is a non-array object, its properties are recursed into and checked in turn. * If it is a non-array object, its properties are recursed into and checked in turn.
* All other values are copied verbatim. * All other values are copied verbatim.
* @param dest The object to merge values into * @param dest - The object to merge values into
* @param source The object to source merged values from * @param source - The object to source merged values from
* @remarks Do not use for regular objects; this is specifically made for JSON copying. * @remarks Do not use for regular objects; this is specifically made for JSON copying.
* @see deepMergeObjects
*/ */
export function deepMergeSpriteData(dest: object, source: object) { export function deepMergeSpriteData(dest: object, source: object) {
// Grab all the keys present in both with similar types for (const key of Object.keys(source)) {
const matchingKeys = Object.keys(source).filter(key => { if (
const destVal = dest[key]; !(key in dest) ||
const sourceVal = source[key]; typeof source[key] !== typeof dest[key] ||
Array.isArray(source[key]) !== Array.isArray(dest[key])
) {
continue;
}
return (
// 1st part somewhat redundant, but makes it clear that we're explicitly interested in properties that exist in both
key in source && Array.isArray(sourceVal) === Array.isArray(destVal) && typeof sourceVal === typeof destVal
);
});
for (const key of matchingKeys) {
// Pure objects get recursed into; everything else gets overwritten // Pure objects get recursed into; everything else gets overwritten
if (typeof source[key] !== "object" || source[key] === null || Array.isArray(source[key])) { if (typeof source[key] !== "object" || source[key] === null || Array.isArray(source[key])) {
dest[key] = source[key]; dest[key] = source[key];

View File

@ -126,7 +126,7 @@ describe("Abilities - Cud Chew", () => {
game.move.select(Moves.STUFF_CHEEKS); game.move.select(Moves.STUFF_CHEEKS);
await game.toNextTurn(); await game.toNextTurn();
// Ate 2 petayas from moves + 1 of each at turn end; all 4 get moved on turn end // Ate 2 petayas from moves + 1 of each at turn end; all 4 get tallied on turn end
expect(farigiraf.summonData.berriesEatenLast).toEqual([ expect(farigiraf.summonData.berriesEatenLast).toEqual([
BerryType.PETAYA, BerryType.PETAYA,
BerryType.PETAYA, BerryType.PETAYA,
@ -145,7 +145,7 @@ describe("Abilities - Cud Chew", () => {
expect(farigiraf.getStatStage(Stat.ATK)).toBe(4); // 1+2+1 expect(farigiraf.getStatStage(Stat.ATK)).toBe(4); // 1+2+1
}); });
it("resets array on switch", async () => { it("should reset both arrays on switch", async () => {
await game.classicMode.startBattle([Species.FARIGIRAF, Species.GIRAFARIG]); await game.classicMode.startBattle([Species.FARIGIRAF, Species.GIRAFARIG]);
const farigiraf = game.scene.getPlayerPokemon()!; const farigiraf = game.scene.getPlayerPokemon()!;
@ -157,12 +157,20 @@ describe("Abilities - Cud Chew", () => {
const turn1Hp = farigiraf.hp; const turn1Hp = farigiraf.hp;
game.doSwitchPokemon(1); game.doSwitchPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase"); await game.toNextTurn();
// summonData got cleared due to switch, turnData got cleared due to turn end // summonData got cleared due to switch, turnData got cleared due to turn end
expect(farigiraf.summonData.berriesEatenLast).toEqual([]); expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
expect(farigiraf.turnData.berriesEaten).toEqual([]); expect(farigiraf.turnData.berriesEaten).toEqual([]);
expect(farigiraf.hp).toEqual(turn1Hp); expect(farigiraf.hp).toEqual(turn1Hp);
game.doSwitchPokemon(1);
await game.toNextTurn();
// TurnData gets cleared while switching in
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
expect(farigiraf.turnData.berriesEaten).toEqual([]);
expect(farigiraf.hp).toEqual(turn1Hp);
}); });
it("clears array if disabled", async () => { it("clears array if disabled", async () => {